Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
d6050c8615 | ||
|
|
7ffac522e3 | ||
|
|
6c4208d9e7 | ||
|
|
98bf4d861d | ||
|
|
bfcd289855 | ||
|
|
7c0fbf9b3f | ||
|
|
6fae07241f | ||
|
|
a4f687fdfd | ||
|
|
b1a101c253 | ||
|
|
10c363b588 | ||
|
|
9a52632024 | ||
|
|
f57caf0987 | ||
|
|
f02904a959 | ||
|
|
8b41b71db7 | ||
|
|
d4cf8e7abb | ||
|
|
9134171b95 | ||
|
|
328c3453f8 | ||
|
|
bb27eb57a9 | ||
|
|
dbc3aa28c4 | ||
|
|
ad92c207f2 | ||
|
|
dea6c5eb92 | ||
|
|
3baefa1d96 | ||
|
|
c0546e0e46 | ||
|
|
d5f1d1b1ad | ||
|
|
1c19bbde93 | ||
|
|
3da5a2875e | ||
|
|
2e6a094d48 | ||
|
|
178c5d54d5 | ||
|
|
a7c396e464 | ||
|
|
24db4a1e25 | ||
|
|
ae77cf9a08 | ||
|
|
4a0a37588f | ||
|
|
5838214910 | ||
|
|
b609d8491e | ||
|
|
18538707ab | ||
|
|
e59c72cff3 | ||
|
|
da573a1805 | ||
|
|
e24a7d005a | ||
|
|
79399686db | ||
|
|
f6ce93f2c8 | ||
|
|
8c35c9d711 | ||
|
|
be674eb41d | ||
|
|
052843ac9b | ||
|
|
c524531784 | ||
|
|
82fa1d8812 | ||
|
|
2b61a63686 | ||
|
|
18e45659ea | ||
|
|
426637a47c | ||
|
|
ababf7d4fa | ||
|
|
691bb5640d | ||
|
|
a80eb2ff0e | ||
|
|
fe59f9bedf | ||
|
|
0f8838623b | ||
|
|
5b5945e427 | ||
|
|
dea57bd1be | ||
|
|
3e81a38438 | ||
|
|
dfdf3d30b3 | ||
|
|
60b64d3e81 | ||
|
|
962fba4d16 | ||
|
|
de218e900b | ||
|
|
b75e7cef90 | ||
|
|
9b1224827a | ||
|
|
c7f78fa7fc | ||
|
|
7dade7f466 | ||
|
|
b20e1150a5 | ||
|
|
aa0d6a6831 | ||
|
|
663faa1f82 | ||
|
|
99b13fa25f | ||
|
|
e12c1319b6 | ||
|
|
7f365bf60b | ||
|
|
2e6c78d723 | ||
|
|
f9c64d9f65 | ||
|
|
4ef6926791 | ||
|
|
9645f1011c | ||
|
|
9847e60dca | ||
|
|
cb3d9ab625 | ||
|
|
db41fb454e | ||
|
|
3b133d5554 | ||
|
|
80f763448f | ||
|
|
a3701f5de8 | ||
|
|
ed2bfc09a6 | ||
|
|
337f821e00 | ||
|
|
f2b126f147 | ||
|
|
1173bc277b | ||
|
|
50f2cc16ab | ||
|
|
eebe319bfd | ||
|
|
75a42b77ea | ||
|
|
d71d3450af | ||
|
|
f080159268 | ||
|
|
0183d32c7f | ||
|
|
94f5894d7f | ||
|
|
81e5a62f25 | ||
|
|
186381bab2 | ||
|
|
e044488f73 | ||
|
|
b077974ebc | ||
|
|
200540dfc3 | ||
|
|
3c00f46e36 | ||
|
|
620721dea7 | ||
|
|
68cbe34c11 | ||
|
|
e21b010c6e | ||
|
|
0846d4d7b2 | ||
|
|
2183a2a1ae | ||
|
|
e8475b18d3 | ||
|
|
cdc1177762 | ||
|
|
97035e0b8b | ||
|
|
aaf62c1c96 | ||
|
|
4d0661fd9b | ||
|
|
713b214c9a | ||
|
|
681388631b | ||
|
|
ae9e27a0c7 | ||
|
|
e83144ce7f | ||
|
|
1c54c7130a | ||
|
|
1a8d5fed8a | ||
|
|
57ebf43e85 | ||
|
|
b87734343e | ||
|
|
c80c9ef726 | ||
|
|
d5758523f3 | ||
|
|
ee40e7e0d7 | ||
|
|
53b606e1c1 | ||
|
|
47c0141c49 | ||
|
|
5b8a17e366 | ||
|
|
64a86b899f | ||
|
|
312e295a47 | ||
|
|
e7544687d1 | ||
|
|
291de5bc1c | ||
|
|
c37a9f5ec3 | ||
|
|
f5cef205f1 | ||
|
|
afb7266f17 | ||
|
|
e639c77aa2 | ||
|
|
ea97055449 | ||
|
|
64dc6ab9ac | ||
|
|
25dedd1098 | ||
|
|
617bf7b6a3 | ||
|
|
e3a53de8a6 | ||
|
|
4cc91fd5b1 | ||
|
|
296a12e394 | ||
|
|
da7aa1d72f | ||
|
|
367ad8ea43 | ||
|
|
2c3bc2d75e | ||
|
|
3cce6ca02b |
@@ -1,367 +0,0 @@
|
||||
version: 2
|
||||
|
||||
build-distro-bin: &build-distro-bin
|
||||
machine: true
|
||||
steps:
|
||||
- checkout
|
||||
# cannot interpolate env var and use as a cache key so just copy the Dockerfile to another filename
|
||||
- run: cp docker/distro_release/Dockerfile.$CIRCLE_JOB docker/distro_release/Dockerfile
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-{{ .Environment.CIRCLE_JOB }}-image-{{ checksum "docker/distro_release/Dockerfile" }}
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-{{ .Environment.CIRCLE_JOB }}-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
||||
- run:
|
||||
name: load or build docker image
|
||||
command: |
|
||||
if [[ -e ~/image.tar ]]; then
|
||||
docker load -i ~/image.tar
|
||||
else
|
||||
docker build --rm=false -t $CIRCLE_JOB -f docker/distro_release/Dockerfile.$CIRCLE_JOB docker/distro_release/
|
||||
docker save $CIRCLE_JOB > ~/image.tar
|
||||
fi
|
||||
- run:
|
||||
name: build binary
|
||||
command: |
|
||||
docker run -it \
|
||||
-v $HOME/.stack:/root/.stack \
|
||||
-v $(pwd):/source \
|
||||
-v $HOME/bin/:/root/.local/bin/ \
|
||||
$CIRCLE_JOB build --allow-different-user --install-ghc --copy-bins
|
||||
# volumes owned by root if chown is not done the save_cache step fails silently
|
||||
sudo chown -R circleci:circleci ~/.stack .stack-work
|
||||
- run:
|
||||
name: compress binary
|
||||
command: |
|
||||
mkdir -p /tmp/workspace/bin
|
||||
cd /tmp/workspace/bin
|
||||
tar cvJf postgrest-$CIRCLE_TAG-$CIRCLE_JOB.tar.xz -C ~/bin postgrest
|
||||
- persist_to_workspace:
|
||||
root: /tmp/workspace
|
||||
paths:
|
||||
- bin/*
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/image.tar
|
||||
key: v1-{{ .Environment.CIRCLE_JOB }}-image-{{ checksum "docker/distro_release/Dockerfile" }}
|
||||
- save_cache:
|
||||
paths:
|
||||
- "~/.stack"
|
||||
- ".stack-work"
|
||||
key: v1-{{ .Environment.CIRCLE_JOB }}-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
||||
|
||||
jobs:
|
||||
build-test-9.4:
|
||||
docker:
|
||||
- image: circleci/buildpack-deps:trusty
|
||||
environment:
|
||||
- PGHOST=localhost
|
||||
- image: circleci/postgres:9.4.14
|
||||
environment:
|
||||
- POSTGRES_USER=circleci
|
||||
- POSTGRES_DB=circleci
|
||||
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/v1.9.3/stack-1.9.3-linux-x86_64.tar.gz | tar zx -C /tmp
|
||||
sudo mv /tmp/stack-1.9.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
|
||||
rm -rf $(stack path --dist-dir) $(stack path --local-install-root)
|
||||
stack install hlint stylish-haskell
|
||||
- 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
|
||||
- run:
|
||||
name: run linter
|
||||
command: git ls-files | grep '\.l\?hs$' | xargs stack exec -- hlint -X QuasiQuotes -X NoPatternSynonyms "$@"
|
||||
- run:
|
||||
name: run styler
|
||||
command: git ls-files | grep '\.l\?hs$' | xargs stack exec -- stylish-haskell -i && git diff-index --exit-code HEAD --
|
||||
- save_cache:
|
||||
paths:
|
||||
- "~/.stack"
|
||||
- ".stack-work"
|
||||
key: v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
||||
|
||||
build-test-9.6:
|
||||
docker:
|
||||
- image: circleci/buildpack-deps:trusty
|
||||
environment:
|
||||
- PGHOST=localhost
|
||||
- image: circleci/postgres:9.6.2
|
||||
environment:
|
||||
- POSTGRES_USER=circleci
|
||||
- POSTGRES_DB=circleci
|
||||
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/v1.9.3/stack-1.9.3-linux-x86_64.tar.gz | tar zx -C /tmp
|
||||
sudo mv /tmp/stack-1.9.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
|
||||
|
||||
build-test-10:
|
||||
docker:
|
||||
- image: circleci/buildpack-deps:trusty
|
||||
environment:
|
||||
- PGHOST=localhost
|
||||
- image: circleci/postgres:10.5
|
||||
environment:
|
||||
- POSTGRES_USER=circleci
|
||||
- POSTGRES_DB=circleci
|
||||
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/v1.9.3/stack-1.9.3-linux-x86_64.tar.gz | tar zx -C /tmp
|
||||
sudo mv /tmp/stack-1.9.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
|
||||
|
||||
build-test-11:
|
||||
docker:
|
||||
- image: circleci/buildpack-deps:trusty
|
||||
environment:
|
||||
- PGHOST=localhost
|
||||
- image: circleci/postgres:11
|
||||
environment:
|
||||
- POSTGRES_USER=circleci
|
||||
- POSTGRES_DB=circleci
|
||||
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/v1.9.3/stack-1.9.3-linux-x86_64.tar.gz | tar zx -C /tmp
|
||||
sudo mv /tmp/stack-1.9.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
|
||||
|
||||
build-prof-test:
|
||||
docker:
|
||||
- image: circleci/buildpack-deps:trusty
|
||||
environment:
|
||||
- PGHOST=localhost
|
||||
- TERM=xterm
|
||||
- image: circleci/postgres:9.6.2
|
||||
environment:
|
||||
- POSTGRES_USER=circleci
|
||||
- POSTGRES_DB=circleci
|
||||
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/v1.9.3/stack-1.9.3-linux-x86_64.tar.gz | tar zx -C /tmp
|
||||
sudo mv /tmp/stack-1.9.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 with profiling enabled
|
||||
command: |
|
||||
stack build --profile -j1
|
||||
- run:
|
||||
name: run memory usage tests
|
||||
command: |
|
||||
test/create_test_db "postgres://circleci@localhost" postgrest_test
|
||||
psql "postgres:///postgrest_test" -f test/fixtures/database.sql
|
||||
psql "postgres:///postgrest_test" -f test/fixtures/roles.sql
|
||||
psql "postgres:///postgrest_test" -f test/fixtures/schema.sql
|
||||
psql "postgres:///postgrest_test" -f test/fixtures/jwt.sql
|
||||
psql "postgres:///postgrest_test" -f test/fixtures/jsonschema.sql
|
||||
psql "postgres:///postgrest_test" -f test/fixtures/privileges.sql
|
||||
test/memory-tests.sh
|
||||
- save_cache:
|
||||
paths:
|
||||
- "~/.stack"
|
||||
- ".stack-work"
|
||||
key: v1-stack-prof-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
||||
|
||||
centos6:
|
||||
<<: *build-distro-bin
|
||||
|
||||
centos7:
|
||||
<<: *build-distro-bin
|
||||
|
||||
ubuntu:
|
||||
<<: *build-distro-bin
|
||||
|
||||
ubuntui386:
|
||||
<<: *build-distro-bin
|
||||
|
||||
release:
|
||||
docker:
|
||||
- image: circleci/golang:1.8
|
||||
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
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build-test-release:
|
||||
jobs:
|
||||
- build-test-9.4:
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
- build-test-9.6:
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
- build-test-10:
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
- build-test-11:
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
- build-prof-test:
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
- centos6:
|
||||
requires:
|
||||
- build-test-9.4
|
||||
- build-test-9.6
|
||||
- build-test-10
|
||||
- build-test-11
|
||||
- build-prof-test
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
- centos7:
|
||||
requires:
|
||||
- build-test-9.4
|
||||
- build-test-9.6
|
||||
- build-test-10
|
||||
- build-test-11
|
||||
- build-prof-test
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
- ubuntu:
|
||||
requires:
|
||||
- build-test-9.4
|
||||
- build-test-9.6
|
||||
- build-test-10
|
||||
- build-test-11
|
||||
- build-prof-test
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
- ubuntui386:
|
||||
requires:
|
||||
- build-test-9.4
|
||||
- build-test-9.6
|
||||
- build-test-10
|
||||
- build-test-11
|
||||
- build-prof-test
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
- release:
|
||||
requires:
|
||||
- centos6
|
||||
- centos7
|
||||
- ubuntu
|
||||
- ubuntui386
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
freebsd_instance:
|
||||
image_family: freebsd-13-1
|
||||
|
||||
build_task:
|
||||
name: Build FreeBSD (Stack)
|
||||
install_script: pkg install -y postgresql13-client hs-stack
|
||||
|
||||
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
|
||||
+15
-18
@@ -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,27 +32,24 @@ 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
|
||||
you create a pull request your code will automatically be tested.
|
||||
|
||||
* All code must also pass [hlint](http://community.haskell.org/~ndm/hlint/)
|
||||
with no warnings. This helps enforce a uniform style for all
|
||||
committers. Continuous integration will check this as well on every
|
||||
pull request.
|
||||
|
||||
* 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)
|
||||
wiki page.
|
||||
* 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 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).
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
patreon: postgrest
|
||||
@@ -1,3 +1,9 @@
|
||||
<!--
|
||||
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/stable/admin.html#schema-reloading)
|
||||
to ensure the schema cache is not stale. This sometimes fixes apparent bugs.
|
||||
-->
|
||||
### Environment
|
||||
|
||||
* PostgreSQL version: (if using docker, specify the image)
|
||||
|
||||
@@ -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@v16
|
||||
- uses: cachix/cachix-action@v10
|
||||
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,476 @@
|
||||
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 14)
|
||||
run: postgrest-coverage
|
||||
- name: Upload coverage to codecov
|
||||
uses: codecov/codecov-action@v3.1.0
|
||||
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]
|
||||
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 & test
|
||||
runs-on: ubuntu-latest
|
||||
cache: |
|
||||
~/.stack
|
||||
.stack-work
|
||||
artifact: postgrest-ubuntu-x64
|
||||
|
||||
- name: MacOS & test
|
||||
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-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@v2.4.0
|
||||
- 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.2.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
|
||||
+11
@@ -12,3 +12,14 @@ site
|
||||
*~
|
||||
*#*
|
||||
.#*
|
||||
*.swp
|
||||
result*
|
||||
dist-newstyle
|
||||
postgrest.hp
|
||||
postgrest.prof
|
||||
__pycache__
|
||||
*.tix
|
||||
coverage
|
||||
.hpc
|
||||
loadtest
|
||||
.history
|
||||
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
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:
|
||||
- gtimeout 1800 stack build --no-terminal --only-snapshot --install-ghc || true
|
||||
- |
|
||||
if test ! "$TRAVIS_TAG"
|
||||
then
|
||||
echo 'No tag pushed. Skipping build.'
|
||||
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
|
||||
+35
-7
@@ -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,17 +46,25 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
||||
|
||||
## Lead Backers
|
||||
|
||||
- [Daniel Babiak](https://github.com/d-babiak)
|
||||
- [Michel Pelletier](https://github.com/michelp/)
|
||||
- Evans Fernandes
|
||||
- [Jan Sommer](https://github.com/nerfpops)
|
||||
- [Franz Gusenbauer](https://www.igutech.at/)
|
||||
|
||||
## Backers
|
||||
|
||||
- Tsingson Qin
|
||||
- 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
|
||||
|
||||
@@ -55,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
|
||||
|
||||
+262
-1
@@ -5,15 +5,275 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## Unreleased
|
||||
|
||||
## [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
|
||||
|
||||
### 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
|
||||
|
||||
- #1473, Fix overloaded computed columns on RPC - @wolfgangwalther
|
||||
- #1471, Fix POST, PATCH, DELETE with ?select= and return=minimal and PATCH with empty body - @wolfgangwalther
|
||||
- #1500, Fix missing `openapi-server-proxy-uri` config option - @steve-chavez
|
||||
- #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
|
||||
|
||||
- #1417, `Accept: application/vnd.pgrst.object+json` behavior is now enforced for POST/PATCH/DELETE regardless of `Prefer: return=representation/minimal` - @dwagin
|
||||
- #1415, Add support for user defined socket permission via `server-unix-socket-mode` config option - @Dansvidania
|
||||
- #1383, Add support for HEAD request - @steve-chavez
|
||||
- #1378, Add support for `Prefer: count=planned` and `Prefer: count=estimated` on GET /table - @steve-chavez, @LorenzHenk
|
||||
- #1327, Add support for optional query parameter `on_conflict` to upsert with specified keys for POST - @ykst
|
||||
- #1430, Allow specifying the foreign key constraint name(`/source?select=fk_constraint(*)`) to disambiguate an embedding - @steve-chavez
|
||||
- #1168, Allow access to the `Authorization` header through the `request.header.authorization` GUC - @steve-chavez
|
||||
- #1435, Add `request.method` and `request.path` GUCs - @steve-chavez
|
||||
- #1088, Allow adding headers to GET/POST/PATCH/PUT/DELETE responses through the `response.headers` GUC - @steve-chavez
|
||||
- #1427, Allow overriding provided headers(Location, Content-Type, etc) through the `response.headers` GUC - @steve-chavez
|
||||
- #1450, Allow multiple schemas to be exposed in one instance. The schema to use can be selected through the headers `Accept-Profile` for GET/HEAD and `Content-Profile` for POST/PATCH/PUT/DELETE - @steve-chavez, @mahmoudkassem
|
||||
|
||||
### Fixed
|
||||
|
||||
- #1301, Fix self join resource embedding on PATCH - @herulume, @steve-chavez
|
||||
- #1389, Fix many to many resource embedding on RPC/PATCH - @steve-chavez
|
||||
- #1355, Allow PATCH/DELETE without `return=minimal` on tables with no select privileges - @steve-chavez
|
||||
- #1361, Fix embedding a VIEW when its source foreign key is UNIQUE - @bwbroersma
|
||||
|
||||
### Changed
|
||||
|
||||
- #1385, bulk RPC call now should be done by specifying a `Prefer: params=multiple-objects` header - @steve-chavez
|
||||
- #1401, resource embedding now outputs an error when multiple relationships between two tables are found - @steve-chavez
|
||||
- #1423, default Unix Socket file mode from 755 to 660 - @dwagin
|
||||
- #1430, Remove embedding with duck typed column names `GET /projects?select=client(*)`- @steve-chavez
|
||||
+ You can rename the foreign key to `client` to make this request work in the new version: `alter table projects rename constraint projects_client_id_fkey to client`
|
||||
- #1413, Change `server-proxy-uri` config option to `openapi-server-proxy-uri` - @steve-chavez
|
||||
|
||||
## [6.0.2] - 2019-08-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- #1369, Change `raw-media-types` to accept a string of comma separated MIME types - @Dansvidania
|
||||
- #1368, Fix long column descriptions being truncated at 63 characters in PostgreSQL 12 - @amedeedaboville
|
||||
- #1348, Go back to converting plus "+" to space " " in querystrings by default - @steve-chavez
|
||||
|
||||
### Deprecated
|
||||
|
||||
- #1348, Deprecate `.` symbol for disambiguating resource embedding(added in #918). The url-safe '!' should be used instead. We refrained from using `+` as part of our syntax because it conflicts with some http clients and proxies.
|
||||
|
||||
## [6.0.1] - 2019-07-30
|
||||
|
||||
### Added
|
||||
|
||||
- #1349, Add user defined raw output media types via `raw-media-types` config option - @Dansvidania
|
||||
- #1243, Add websearch_to_tsquery support - @herulume
|
||||
|
||||
### Fixed
|
||||
|
||||
- #1336, Error when testing on Chrome/Firefox: text/html requested but a single column was not selected - @Dansvidania
|
||||
- #1334, Unable to compile v6.0.0 on windows - @steve-chavez
|
||||
|
||||
## [6.0.0] - 2019-06-21
|
||||
|
||||
### Added
|
||||
|
||||
- #1186, Add support for user defined unix socket via `server-unix-socket` config option
|
||||
- #1186, Add support for user defined unix socket via `server-unix-socket` config option - @Dansvidania
|
||||
- #690, Add `?columns` query parameter for faster bulk inserts, also ignores unspecified json keys in a payload - @steve-chavez
|
||||
- #1239, Add support for resource embedding on materialized views - @vitorbaptista
|
||||
- #1264, Add support for bulk RPC call - @steve-chavez
|
||||
@@ -22,6 +282,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- #790, Allow override of OpenAPI spec through `root-spec` config option - @steve-chavez
|
||||
- #1308, Accept `text/plain` and `text/html` for raw output - @steve-chavez
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- #1223, Fix incorrect OpenAPI externalDocs url - @steve-chavez
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
Copyright (c) 2014 Joe Nelson
|
||||
Copyright (c) 2019 Steve Chavez
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
.PHONY: check clean lint style test test-watch coverage circleci circleci-prof-test check-dburi prompt-clean prompt-long-process
|
||||
|
||||
check: lint style test
|
||||
|
||||
clean: prompt-clean
|
||||
stack clean --full
|
||||
|
||||
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
|
||||
|
||||
|
||||
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>
|
||||
@@ -50,13 +66,15 @@ Big thanks to our sponsors! You can join them by supporting PostgREST on [Patreo
|
||||
```bash
|
||||
postgrest --help
|
||||
```
|
||||
## [Documentation](http://postgrest.org)
|
||||
|
||||
Latest documentation is at [postgrest.org](http://postgrest.org). You can contribute to the docs in [PostgREST/postgrest-docs](https://github.com/PostgREST/postgrest-docs).
|
||||
|
||||
## 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
|
||||
(or named after precious gems), prepare to be pleasantly surprised by
|
||||
PostgREST performance.
|
||||
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
|
||||
in [Haskell](https://www.haskell.org/) using the
|
||||
@@ -91,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
|
||||
@@ -148,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": "6.0.0"
|
||||
},
|
||||
"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,38 +0,0 @@
|
||||
platform: x64
|
||||
|
||||
cache:
|
||||
- "c:\\sr"
|
||||
- .stack-work
|
||||
|
||||
environment:
|
||||
global:
|
||||
STACK_ROOT: "c:\\sr"
|
||||
GOPATH: c:\gopath
|
||||
|
||||
test: off
|
||||
|
||||
skip_non_tags: true
|
||||
|
||||
skip_branch_with_pr: true
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
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
|
||||
- stack build -j1 --copy-bins --local-bin-path .
|
||||
|
||||
artifacts:
|
||||
- path: postgrest.exe
|
||||
|
||||
deploy_script:
|
||||
- 7z a -tzip postgrest-%APPVEYOR_REPO_TAG_NAME%-windows-x64.zip postgrest.exe
|
||||
- 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"
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
{ system ? builtins.currentSystem }:
|
||||
|
||||
let
|
||||
name =
|
||||
"postgrest";
|
||||
|
||||
compiler =
|
||||
"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
|
||||
# added here will increase the space used in the Nix store and trigger the
|
||||
# build of new Nix derivations when changed.
|
||||
src =
|
||||
pkgs.lib.sourceFilesBySuffices
|
||||
(pkgs.gitignoreSource ./.)
|
||||
[ ".cabal" ".hs" ".lhs" "LICENSE" ];
|
||||
|
||||
# Commit of the Nixpkgs repository that we want to use.
|
||||
nixpkgsVersion =
|
||||
import nix/nixpkgs-version.nix;
|
||||
|
||||
# Nix files that describe the Nixpkgs repository. We evaluate the expression
|
||||
# using `import` below.
|
||||
nixpkgs =
|
||||
builtins.fetchTarball {
|
||||
url = "https://github.com/nixos/nixpkgs/archive/${nixpkgsVersion.rev}.tar.gz";
|
||||
sha256 = nixpkgsVersion.tarballHash;
|
||||
};
|
||||
|
||||
allOverlays =
|
||||
import nix/overlays;
|
||||
|
||||
overlays =
|
||||
[
|
||||
allOverlays.build-toolbox
|
||||
allOverlays.checked-shell-script
|
||||
allOverlays.gitignore
|
||||
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 system; };
|
||||
|
||||
postgresqlVersions =
|
||||
[
|
||||
{ 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 { };
|
||||
|
||||
# Dynamic derivation for PostgREST
|
||||
postgrest =
|
||||
pkgs.haskell.packages."${compiler}".callCabal2nix name src { };
|
||||
|
||||
# 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; };
|
||||
|
||||
# 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;
|
||||
|
||||
# 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.
|
||||
postgrestPackage =
|
||||
lib.dontCheck postgrest;
|
||||
|
||||
# Static executable.
|
||||
postgrestStatic =
|
||||
lib.justStaticExecutables (lib.dontCheck (staticHaskellPackage name src));
|
||||
|
||||
# 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/tools/docker { postgrest = postgrestStatic; };
|
||||
|
||||
# Load testing tools.
|
||||
loadtest =
|
||||
pkgs.callPackage nix/tools/loadtest.nix { inherit withTools; };
|
||||
|
||||
# Script for running memory tests.
|
||||
memory =
|
||||
pkgs.callPackage nix/tools/memory.nix { inherit postgrestProfiled withTools; };
|
||||
|
||||
# Utility for updating the pinned version of Nixpkgs.
|
||||
nixpkgsTools =
|
||||
pkgs.callPackage nix/tools/nixpkgsTools.nix { };
|
||||
|
||||
# Scripts for publishing new releases.
|
||||
release =
|
||||
pkgs.callPackage nix/tools/release { };
|
||||
|
||||
# Linting and styling tools.
|
||||
style =
|
||||
pkgs.callPackage nix/tools/style.nix { inherit hsie; };
|
||||
|
||||
# Scripts for running tests.
|
||||
tests =
|
||||
pkgs.callPackage nix/tools/tests.nix {
|
||||
inherit postgrest devCabalOptions withTools;
|
||||
ghc = pkgs.haskell.compiler."${compiler}";
|
||||
inherit (pkgs.haskell.packages."${compiler}") hpc-codecov;
|
||||
inherit (pkgs.haskell.packages."${compiler}") weeder;
|
||||
};
|
||||
|
||||
withTools =
|
||||
pkgs.callPackage nix/tools/withTools.nix { inherit devCabalOptions postgresqlVersions postgrest; };
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
FROM debian:stretch-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_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"
|
||||
|
||||
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,3 +0,0 @@
|
||||
db-uri = "postgres://app_user:password@postgres:5432/app_db"
|
||||
db-schema = "public"
|
||||
db-anon-role = "app_user"
|
||||
@@ -1,18 +0,0 @@
|
||||
FROM centos:centos6
|
||||
|
||||
RUN yum -y update
|
||||
RUN yum -y install perl make automake gcc gmp-devel libffi zlib zlib-devel xz tar
|
||||
RUN yum -y install https://download.postgresql.org/pub/repos/yum/9.3/redhat/rhel-6-x86_64/pgdg-centos93-9.3-2.noarch.rpm
|
||||
RUN yum -y install postgresql93-devel
|
||||
RUN yum clean all
|
||||
RUN curl -sSL https://get.haskellstack.org/ | sh
|
||||
|
||||
ENV PATH $PATH:/usr/pgsql-9.3/bin
|
||||
|
||||
# To disable warning when building
|
||||
ENV PATH $PATH:/root/.local/bin
|
||||
|
||||
RUN mkdir /source
|
||||
WORKDIR /source
|
||||
|
||||
ENTRYPOINT ["stack"]
|
||||
@@ -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/9.3/redhat/rhel-7-x86_64/pgdg-centos93-9.3-2.noarch.rpm
|
||||
RUN yum -y install postgresql93-devel
|
||||
RUN yum clean all
|
||||
RUN curl -sSL https://get.haskellstack.org/ | sh
|
||||
|
||||
ENV PATH $PATH:/usr/pgsql-9.3/bin
|
||||
|
||||
# To disable warning when building
|
||||
ENV PATH $PATH:/root/.local/bin
|
||||
|
||||
RUN mkdir /source
|
||||
WORKDIR /source
|
||||
|
||||
ENTRYPOINT ["stack"]
|
||||
@@ -1,18 +0,0 @@
|
||||
FROM ubuntu:16.04
|
||||
|
||||
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,18 +0,0 @@
|
||||
FROM 32bit/ubuntu:16.04
|
||||
|
||||
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,17 +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)"
|
||||
|
||||
server-proxy-uri = "$(PGRST_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)"
|
||||
+26
-321
@@ -1,337 +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.IORef (IORef, atomicWriteIORef, newIORef,
|
||||
readIORef)
|
||||
import Data.String (IsString (..))
|
||||
import Data.Text (pack, replace, strip, stripPrefix,
|
||||
unpack)
|
||||
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
|
||||
import Data.Text.IO (hPutStrLn, readFile)
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Network.Socket (Family (AF_UNIX),
|
||||
SockAddr (SockAddrUnix), Socket,
|
||||
SocketType (Stream), bind, close,
|
||||
defaultProtocol, listen,
|
||||
maxListenQueue, socket)
|
||||
import Network.Wai.Handler.Warp (defaultSettings, runSettings,
|
||||
runSettingsSocket, setHost, setPort,
|
||||
setServerName)
|
||||
import System.Directory (removeFile)
|
||||
import System.IO (BufferMode (..), hSetBuffering)
|
||||
import System.IO.Error (isDoesNotExistError)
|
||||
|
||||
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, replace)
|
||||
import qualified PostgREST.App as App
|
||||
import qualified PostgREST.CLI as CLI
|
||||
|
||||
import Protolude
|
||||
|
||||
#ifndef mingw32_HOST_OS
|
||||
import System.Posix.Signals
|
||||
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 -- ^ Schema PostgREST is serving up
|
||||
-> IORef (Maybe DbStructure) -- ^ mutable reference to 'DbStructure'
|
||||
-> IORef Bool -- ^ Used as a binary Semaphore
|
||||
-> IO ()
|
||||
connectionWorker mainTid pool schema 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 schema 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
|
||||
--
|
||||
-- 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 host = configHost conf
|
||||
port = configPort conf
|
||||
proxy = configProxyUri conf
|
||||
maybeSocketAddr = configSocket 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
|
||||
setBuffering
|
||||
opts <- CLI.readCLIShowHelp
|
||||
CLI.main installSignalHandlers runAppInSocket opts
|
||||
|
||||
-- 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
|
||||
when (isLeft 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
|
||||
(configSchema conf)
|
||||
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.
|
||||
installSignalHandlers :: App.SignalHandlerInstaller
|
||||
#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
|
||||
(configSchema conf)
|
||||
refDbStructure
|
||||
refIsWorkerOn
|
||||
) Nothing
|
||||
installSignalHandlers = Unix.installSignalHandlers
|
||||
#else
|
||||
installSignalHandlers _ = pass
|
||||
#endif
|
||||
|
||||
runAppInSocket :: Maybe App.SocketRunner
|
||||
#ifndef mingw32_HOST_OS
|
||||
runAppInSocket = Just Unix.runAppWithSocket
|
||||
#else
|
||||
runAppInSocket = 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
|
||||
(configSchema conf)
|
||||
refDbStructure
|
||||
refIsWorkerOn)
|
||||
in case maybeSocketAddr of
|
||||
Nothing -> do
|
||||
-- run the postgrest application
|
||||
putStrLn $ ("Listening on port " :: Text) <> show (configPort conf)
|
||||
runSettings appSettings postgrestApplication
|
||||
Just socketAddr -> do
|
||||
-- run postgrest application with user defined socket
|
||||
sock <- createAndBindSocket (unpack socketAddr)
|
||||
listen sock maxListenQueue
|
||||
putStrLn $ ("Listening on unix socket " :: Text) <> show socketAddr
|
||||
runSettingsSocket appSettings sock postgrestApplication
|
||||
-- clean socket up when done
|
||||
close sock
|
||||
|
||||
{-|
|
||||
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}
|
||||
|
||||
createAndBindSocket :: FilePath -> IO Socket
|
||||
createAndBindSocket filePath = do
|
||||
deleteSocketFileIfExist filePath
|
||||
sock <- socket AF_UNIX Stream defaultProtocol
|
||||
bind sock $ SockAddrUnix filePath
|
||||
return sock
|
||||
where
|
||||
deleteSocketFileIfExist path = removeFile path `catch` handleDoesNotExist
|
||||
handleDoesNotExist e
|
||||
| isDoesNotExistError e = return ()
|
||||
| otherwise = throwIO e
|
||||
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
|
||||
hSetBuffering stdout LineBuffering
|
||||
hSetBuffering stdin LineBuffering
|
||||
hSetBuffering stderr LineBuffering
|
||||
|
||||
@@ -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
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
# Nix development and build environment
|
||||
|
||||
With Nix it's possible to quickly and reliably recreate the full environments
|
||||
for developing, testing and building PostgREST.
|
||||
|
||||
## Getting started with Nix
|
||||
|
||||
You'll need to [get Nix](https://nixos.org/download.html). The installer will
|
||||
create your Nix store in the `/nix/` directory, where all build artifacts and
|
||||
their dependencies will be stored. It will also link the Nix executables like
|
||||
`nix-env`, `nix-build` and `nix-shell` into your PATH. Nix will manage all
|
||||
other PostgREST dependencies from here on out. To clean up older build
|
||||
artifacts from the `/nix/store`, you can run `nix-collect-garbage`.
|
||||
|
||||
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 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 `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
|
||||
following command will put you into a new shell that has GHC and Cabal on the
|
||||
PATH:
|
||||
|
||||
```bash
|
||||
nix-shell
|
||||
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## 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 (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]$ 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]$
|
||||
|
||||
```
|
||||
|
||||
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
|
||||
# the command to be run in nix-shell:
|
||||
$ nix-shell --run "postgrest-foo --bar"
|
||||
|
||||
```
|
||||
|
||||
A third option is to install utilities that you use very often locally:
|
||||
|
||||
```bash
|
||||
$ nix-env -f default.nix -iA devTools
|
||||
|
||||
# `postgrest-style` can now be run directly:
|
||||
$ postgrest-style
|
||||
|
||||
```
|
||||
|
||||
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 our
|
||||
test suite, including setting up all required dependencies and
|
||||
temporary test databases:
|
||||
|
||||
```bash
|
||||
# Run the tests against the most recent version of PostgreSQL:
|
||||
$ nix-shell --run postgrest-test-spec
|
||||
|
||||
# Run the tests against all supported versions of PostgreSQL:
|
||||
$ 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-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
|
||||
source code:
|
||||
|
||||
```bash
|
||||
# Linting
|
||||
$ nix-shell --run postgrest-lint
|
||||
|
||||
# Styling / auto-formatting code
|
||||
$ 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
|
||||
give you some more background and details on how it works.
|
||||
|
||||
### `default.nix`
|
||||
|
||||
[`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.
|
||||
|
||||
Internally, our `default.nix` uses the `pkgs.callPackage` function to import
|
||||
the modules that we defined in the `nix` directory. It automatically passes the
|
||||
arguments those modules require if they are available in `pkgs` (this means
|
||||
that `pkgs` is defined in terms of itself, better not to think too much about
|
||||
that).
|
||||
|
||||
We also use `default.nix` to load our pinned version of the `nixpkgs`
|
||||
repository. This set of packages will always be the same, independently from
|
||||
where or when you use it. The pinned version can be upgraded with the small
|
||||
`nixpkgs-upgrade` utility. Running `nixpkgs-upgrade > nix/nixpkgs-version.nix`
|
||||
in `nix-shell` will upgrade the pinned version to the latest `nixpkgs-unstable`
|
||||
version.
|
||||
|
||||
### `shell.nix`
|
||||
|
||||
[`shell.nix`](../shell.nix) defines an environment in which PostgREST can be
|
||||
built and developed. It extends the build enviroment from our `postgrest`
|
||||
attribute with useful utilities that will be put on the PATH in `nix-shell`.
|
||||
|
||||
### `nix/overlays`
|
||||
|
||||
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:
|
||||
@@ -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; }
|
||||
@@ -0,0 +1,6 @@
|
||||
# Pinned version of Nixpkgs, generated with postgrest-nixpkgs-upgrade.
|
||||
{
|
||||
date = "2022-08-09";
|
||||
rev = "9f15d6c3a74d2778c6e1af67947c95f100dc6fd2";
|
||||
tarballHash = "14axdmi3kb6rlib39ik42yq907bm66x6vzswm5w1rsnw9vzgm31a";
|
||||
}
|
||||
@@ -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_5
|
||||
, 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_5}/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,9 @@
|
||||
{
|
||||
build-toolbox = import ./build-toolbox;
|
||||
checked-shell-script = import ./checked-shell-script;
|
||||
gitignore = import ./gitignore.nix;
|
||||
haskell-packages = import ./haskell-packages.nix;
|
||||
postgresql-default = import ./postgresql-default.nix;
|
||||
postgresql-legacy = import ./postgresql-legacy.nix;
|
||||
postgresql-future = import ./postgresql-future.nix;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
self: super:
|
||||
# Overlay that adds the `gitignoreSource` function from Hercules-CI.
|
||||
# This function is useful for filtering which files are added to the Nix store.
|
||||
# See: https://github.com/hercules-ci/gitignore.nix
|
||||
|
||||
# To update to a newer revision, the simplest way is to add a new commit hash
|
||||
# from GitHub under `rev` and to then add the hash that Nix suggests on first
|
||||
# use.
|
||||
{
|
||||
gitignoreSource =
|
||||
let
|
||||
gitignoreSrc = super.fetchFromGitHub {
|
||||
owner = "hercules-ci";
|
||||
repo = "gitignore";
|
||||
rev = "a20de23b925fd8264fd7fad6454652e142fd7f73";
|
||||
sha256 = "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=";
|
||||
};
|
||||
in
|
||||
(super.callPackage gitignoreSrc { }).gitignoreSource;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{ 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 = "0iwh4wsjhb7pms88lw1afhdal9f86nrrkkvv65f9wxbd1b159n72";
|
||||
# }
|
||||
# { };
|
||||
#
|
||||
# To get the sha256:
|
||||
# nix-prefetch-url --unpack https://hackage.haskell.org/package/protolude-0.3.0/protolude-0.3.0.tar.gz
|
||||
|
||||
# To temporarily pin unreleased versions from GitHub:
|
||||
# <name> =
|
||||
# prev.callCabal2nixWithOptions "<name>" (super.fetchFromGitHub {
|
||||
# owner = "<owner>";
|
||||
# repo = "<repo>";
|
||||
# rev = "<commit>";
|
||||
# sha256 = "<sha256>";
|
||||
# }) "--subpath=<subpath>" {};
|
||||
#
|
||||
# To get the sha256:
|
||||
# nix-prefetch-url --unpack https://github.com/<owner>/<repo>/archive/<commit>.tar.gz
|
||||
} // extraOverrides final prev;
|
||||
in
|
||||
{
|
||||
haskell =
|
||||
super.haskell // {
|
||||
packages = super.haskell.packages // {
|
||||
"${compiler}" =
|
||||
super.haskell.packages."${compiler}".override { inherit overrides; };
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
self: super:
|
||||
# Overlay that adds legacy versions of PostgreSQL that are supported by
|
||||
# PostgREST.
|
||||
{
|
||||
# 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 = "571cbf3d1db477058303cef8754fb85a14e90eb7";
|
||||
tarballHash = "0q74wn418i1bn5sssacmw8ykpmqvzr0s93sj6pbs3rf6bf134fkz";
|
||||
pinnedPkgs =
|
||||
builtins.fetchTarball {
|
||||
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||
sha256 = tarballHash;
|
||||
};
|
||||
in
|
||||
(import pinnedPkgs { }).pkgs.postgresql_9_6;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{ runCommand }:
|
||||
|
||||
{
|
||||
applyPatches =
|
||||
name: src: patches:
|
||||
runCommand
|
||||
name
|
||||
{ inherit src patches; }
|
||||
''
|
||||
set -eou pipefail
|
||||
|
||||
cp -r $src $out
|
||||
chmod -R u+w $out
|
||||
|
||||
for patch in $patches; do
|
||||
echo "Applying patch $patch"
|
||||
patch -d "$out" -p1 < "$patch"
|
||||
done
|
||||
'';
|
||||
|
||||
static-haskell-nix-ncurses =
|
||||
./static-haskell-nix-ncurses.patch;
|
||||
static-haskell-nix-ghc-bignum =
|
||||
./static-haskell-nix-ghc-bignum.patch;
|
||||
}
|
||||
@@ -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,61 @@
|
||||
# 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
|
||||
];
|
||||
|
||||
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}"
|
||||
@@ -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,95 @@
|
||||
# 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
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
{ buildToolbox
|
||||
, postgrest
|
||||
, dockerTools
|
||||
, checkedShellScript
|
||||
}:
|
||||
let
|
||||
image =
|
||||
dockerTools.buildImage {
|
||||
name = "postgrest";
|
||||
tag = "latest";
|
||||
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 =
|
||||
''
|
||||
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,53 @@
|
||||
{ buildToolbox
|
||||
, checkedShellScript
|
||||
, 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="$(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,127 @@
|
||||
{ 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, 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"
|
||||
|
||||
bump_pre="$major.$minor.$patch.$(date '+%Y%m%d')"
|
||||
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
|
||||
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 DbStructure 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; };
|
||||
}
|
||||
+253
-130
@@ -1,13 +1,13 @@
|
||||
name: postgrest
|
||||
version: 6.0.0
|
||||
version: 10.0.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
|
||||
author: Joe Nelson, Adam Baker
|
||||
maintainer: Steve Chávez <stevechavezast@gmail.com>
|
||||
author: Joe Nelson, Adam Baker, Steve Chavez
|
||||
maintainer: Steve Chavez <stevechavezast@gmail.com>
|
||||
category: Executable, PostgreSQL, Network APIs
|
||||
homepage: https://postgrest.org
|
||||
bug-reports: https://github.com/PostgREST/postgrest/issues
|
||||
@@ -19,164 +19,287 @@ 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.RangeQuery
|
||||
PostgREST.Types
|
||||
other-modules: Paths_postgrest
|
||||
default-language: Haskell2010
|
||||
default-extensions: OverloadedStrings
|
||||
NoImplicitPrelude
|
||||
hs-source-dirs: src
|
||||
build-depends: base >= 4.9 && < 4.13
|
||||
exposed-modules: PostgREST.App
|
||||
PostgREST.Admin
|
||||
PostgREST.AppState
|
||||
PostgREST.Auth
|
||||
PostgREST.CLI
|
||||
PostgREST.Config
|
||||
PostgREST.Config.Database
|
||||
PostgREST.Config.JSPath
|
||||
PostgREST.Config.PgVersion
|
||||
PostgREST.Config.Proxy
|
||||
PostgREST.Cors
|
||||
PostgREST.DbStructure
|
||||
PostgREST.DbStructure.Identifiers
|
||||
PostgREST.DbStructure.Proc
|
||||
PostgREST.DbStructure.Relationship
|
||||
PostgREST.DbStructure.Table
|
||||
PostgREST.Error
|
||||
PostgREST.GucHeader
|
||||
PostgREST.Logger
|
||||
PostgREST.Middleware
|
||||
PostgREST.MediaType
|
||||
PostgREST.OpenAPI
|
||||
PostgREST.Query.QueryBuilder
|
||||
PostgREST.Query.SqlFragment
|
||||
PostgREST.Query.Statements
|
||||
PostgREST.RangeQuery
|
||||
PostgREST.Request.ApiRequest
|
||||
PostgREST.Request.DbRequestBuilder
|
||||
PostgREST.Request.MutateQuery
|
||||
PostgREST.Request.Preferences
|
||||
PostgREST.Request.QueryParams
|
||||
PostgREST.Request.ReadQuery
|
||||
PostgREST.Request.Types
|
||||
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 >= 0.11.3 && < 1.5
|
||||
, ansi-wl-pprint >= 0.6.7 && < 0.7
|
||||
, base64-bytestring >= 1 && < 1.1
|
||||
, bytestring >= 0.10.8 && < 0.11
|
||||
, aeson >= 2.0.3 && < 2.1
|
||||
, 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.1 && < 0.2
|
||||
, 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 >= 1.4 && < 1.6
|
||||
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
||||
, hasql-notifications >= 0.1 && < 0.3
|
||||
, hasql-pool >= 0.5 && < 0.6
|
||||
, hasql-transaction >= 0.7.2 && < 0.8
|
||||
, hasql-transaction >= 1.0.1 && < 1.1
|
||||
, heredoc >= 0.2 && < 0.3
|
||||
, http-types >= 0.12.2 && < 0.13
|
||||
, insert-ordered-containers >= 0.1 && < 0.3
|
||||
, insert-ordered-containers >= 0.2.2 && < 0.3
|
||||
, interpolatedstring-perl6 >= 1 && < 1.1
|
||||
, jose >= 0.7 && < 0.8
|
||||
, lens >= 4.14 && < 4.18
|
||||
, lens-aeson >= 1.0.1 && < 1.1
|
||||
, network-uri >= 2.6.1 && < 2.7
|
||||
, optparse-applicative >= 0.13 && < 0.15
|
||||
, jose >= 0.8.5.1 && < 0.10
|
||||
, lens >= 4.14 && < 5.2
|
||||
, 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.17
|
||||
, parsec >= 3.1.11 && < 3.2
|
||||
, protolude >= 0.2.2 && < 0.3
|
||||
, regex-tdfa >= 1.2.2 && < 1.3
|
||||
, 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.1.4 && < 2.4
|
||||
, swagger2 >= 2.4 && < 2.9
|
||||
, text >= 1.2.2 && < 1.3
|
||||
, time >= 1.6 && < 1.9
|
||||
, time >= 1.6 && < 1.12
|
||||
, unordered-containers >= 0.2.8 && < 0.3
|
||||
, vault >= 0.3.1.5 && < 0.4
|
||||
, vector >= 0.11 && < 0.13
|
||||
, 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
|
||||
, 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
|
||||
|
||||
executable postgrest
|
||||
main-is: Main.hs
|
||||
hs-source-dirs: main
|
||||
build-depends: base >= 4.9 && < 4.13
|
||||
, auto-update >= 0.1.4 && < 0.2
|
||||
, base64-bytestring >= 1 && < 1.1
|
||||
, bytestring >= 0.10.8 && < 0.11
|
||||
, directory >= 1.2.6 && < 1.4
|
||||
, hasql >= 1.4 && < 1.5
|
||||
, hasql-pool >= 0.5 && < 0.6
|
||||
, hasql-transaction >= 0.7.2 && < 0.8
|
||||
, network < 2.9
|
||||
, postgrest
|
||||
, protolude >= 0.2.2 && < 0.3
|
||||
, retry >= 0.7.4 && < 0.9
|
||||
, text >= 1.2.2 && < 1.3
|
||||
, time >= 1.6 && < 1.9
|
||||
, warp >= 3.2.12 && < 3.3
|
||||
default-language: Haskell2010
|
||||
default-extensions: OverloadedStrings
|
||||
QuasiQuotes
|
||||
NoImplicitPrelude
|
||||
ghc-options: -threaded -rtsopts "-with-rtsopts=-N -I2"
|
||||
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
|
||||
build-depends:
|
||||
unix
|
||||
, directory >= 1.2.6 && < 1.4
|
||||
exposed-modules:
|
||||
PostgREST.Unix
|
||||
|
||||
executable postgrest
|
||||
default-language: Haskell2010
|
||||
default-extensions: OverloadedStrings
|
||||
NoImplicitPrelude
|
||||
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 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
|
||||
main-is: Main.hs
|
||||
other-modules: Feature.AndOrParamsSpec
|
||||
Feature.AsymmetricJwtSpec
|
||||
Feature.AudienceJwtSecretSpec
|
||||
Feature.AuthSpec
|
||||
Feature.BinaryJwtSecretSpec
|
||||
Feature.ConcurrentSpec
|
||||
Feature.CorsSpec
|
||||
Feature.DeleteSpec
|
||||
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
|
||||
SpecHelper
|
||||
TestTypes
|
||||
hs-source-dirs: test
|
||||
build-depends: base >= 4.9 && < 4.13
|
||||
, aeson >= 0.11.3 && < 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.1
|
||||
, 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 && < 0.8
|
||||
, heredoc >= 0.2 && < 0.3
|
||||
, hspec >= 2.3 && < 2.8
|
||||
, hspec-wai >= 0.7 && < 0.10
|
||||
, hspec-wai-json >= 0.7 && < 0.10
|
||||
, http-types >= 0.12.3 && < 0.13
|
||||
, lens >= 4.14 && < 4.18
|
||||
, lens-aeson >= 1.0.1 && < 1.1
|
||||
, monad-control >= 1.0.1 && < 1.1
|
||||
, postgrest
|
||||
, process >= 1.4.2 && < 1.7
|
||||
, protolude >= 0.2.2 && < 0.3
|
||||
, regex-tdfa >= 1.2.2 && < 1.3
|
||||
, text >= 1.2.2 && < 1.3
|
||||
, time >= 1.6 && < 1.9
|
||||
, transformers-base >= 0.4.4 && < 0.5
|
||||
, wai >= 3.2.1 && < 3.3
|
||||
, wai-extra >= 3.0.19 && < 3.1
|
||||
default-language: Haskell2010
|
||||
default-extensions: OverloadedStrings
|
||||
QuasiQuotes
|
||||
NoImplicitPrelude
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||
hs-source-dirs: test/spec
|
||||
main-is: Main.hs
|
||||
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.ExtraSearchPathSpec
|
||||
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
|
||||
build-depends: base >= 4.9 && < 4.17
|
||||
, aeson >= 2.0.3 && < 2.1
|
||||
, aeson-qq >= 0.8.1 && < 0.9
|
||||
, async >= 2.1.1 && < 2.3
|
||||
, auto-update >= 0.1.4 && < 0.2
|
||||
, base64-bytestring >= 1 && < 1.3
|
||||
, bytestring >= 0.10.8 && < 0.12
|
||||
, case-insensitive >= 1.2 && < 1.3
|
||||
, containers >= 0.5.7 && < 0.7
|
||||
, hasql-pool >= 0.5 && < 0.6
|
||||
, 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.2
|
||||
, lens-aeson >= 1.0.1 && < 1.2
|
||||
, monad-control >= 1.0.1 && < 1.1
|
||||
, postgrest
|
||||
, process >= 1.4.2 && < 1.7
|
||||
, protolude >= 0.3.1 && < 0.4
|
||||
, regex-tdfa >= 1.2.2 && < 1.4
|
||||
, text >= 1.2.2 && < 1.3
|
||||
, transformers-base >= 0.4.4 && < 0.5
|
||||
, wai >= 3.2.1 && < 3.3
|
||||
, 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
|
||||
hs-source-dirs: test/spec
|
||||
main-is: QueryCost.hs
|
||||
other-modules: SpecHelper
|
||||
build-depends: base >= 4.9 && < 4.17
|
||||
, aeson >= 2.0.3 && < 2.1
|
||||
, 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.4 && < 1.6
|
||||
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
||||
, hasql-pool >= 0.5 && < 0.6
|
||||
, 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.2
|
||||
, 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 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
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# 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.cabal2nix
|
||||
pkgs.git
|
||||
pkgs.postgresql
|
||||
postgrest.hsie.bin
|
||||
]
|
||||
++ toolboxes;
|
||||
|
||||
shellHook =
|
||||
''
|
||||
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)
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,74 @@
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module PostgREST.Admin
|
||||
( postgrestAdmin
|
||||
) where
|
||||
|
||||
import qualified Data.Text as T
|
||||
|
||||
import Network.Socket
|
||||
import Network.Socket.ByteString
|
||||
|
||||
import qualified Network.HTTP.Types.Status as HTTP
|
||||
import qualified Network.Wai as Wai
|
||||
|
||||
import qualified Hasql.Session as SQL
|
||||
|
||||
import qualified PostgREST.AppState as AppState
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
|
||||
import Protolude
|
||||
|
||||
-- | PostgREST admin application
|
||||
postgrestAdmin :: AppState.AppState -> AppConfig -> Wai.Application
|
||||
postgrestAdmin appState appConfig req respond = do
|
||||
isMainAppReachable <- any isRight <$> reachMainApp appConfig
|
||||
isSchemaCacheLoaded <- isJust <$> AppState.getDbStructure appState
|
||||
isConnectionUp <-
|
||||
if configDbChannelEnabled appConfig
|
||||
then AppState.getIsListenerOn appState
|
||||
else isRight <$> AppState.usePool appState (SQL.sql "SELECT 1")
|
||||
|
||||
case Wai.pathInfo req of
|
||||
["ready"] ->
|
||||
respond $ Wai.responseLBS (if isMainAppReachable && isConnectionUp && isSchemaCacheLoaded then HTTP.status200 else HTTP.status503) [] mempty
|
||||
["live"] ->
|
||||
respond $ Wai.responseLBS (if isMainAppReachable then HTTP.status200 else HTTP.status503) [] mempty
|
||||
_ ->
|
||||
respond $ Wai.responseLBS HTTP.status404 [] mempty
|
||||
|
||||
-- Try to connect to the main app socket
|
||||
-- Note that it doesn't even send a valid HTTP request, we just want to check that the main app is accepting connections
|
||||
-- The code for resolving the "*4", "!4", "*6", "!6", "*" special values is taken from
|
||||
-- https://hackage.haskell.org/package/streaming-commons-0.2.2.4/docs/src/Data.Streaming.Network.html#bindPortGenEx
|
||||
reachMainApp :: AppConfig -> IO [Either IOException ()]
|
||||
reachMainApp AppConfig{..} =
|
||||
case configServerUnixSocket of
|
||||
Just path -> do
|
||||
sock <- socket AF_UNIX Stream 0
|
||||
(:[]) <$> try (do
|
||||
connect sock $ SockAddrUnix path
|
||||
withSocketsDo $ bracket (pure sock) close sendEmpty)
|
||||
Nothing -> do
|
||||
let
|
||||
host | configServerHost `elem` ["*4", "!4", "*6", "!6", "*"] = Nothing
|
||||
| otherwise = Just configServerHost
|
||||
filterAddrs xs =
|
||||
case configServerHost of
|
||||
"*4" -> ipv4Addrs xs ++ ipv6Addrs xs
|
||||
"!4" -> ipv4Addrs xs
|
||||
"*6" -> ipv6Addrs xs ++ ipv4Addrs xs
|
||||
"!6" -> ipv6Addrs xs
|
||||
_ -> xs
|
||||
ipv4Addrs = filter ((/=) AF_INET6 . addrFamily)
|
||||
ipv6Addrs = filter ((==) AF_INET6 . addrFamily)
|
||||
|
||||
addrs <- getAddrInfo (Just $ defaultHints { addrSocketType = Stream }) (T.unpack <$> host) (Just . show $ configServerPort)
|
||||
tryAddr `traverse` filterAddrs addrs
|
||||
where
|
||||
sendEmpty sock = void $ send sock mempty
|
||||
tryAddr :: AddrInfo -> IO (Either IOException ())
|
||||
tryAddr addr = do
|
||||
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
|
||||
try $ do
|
||||
connect sock $ addrAddress addr
|
||||
withSocketsDo $ bracket (pure sock) close sendEmpty
|
||||
@@ -1,313 +0,0 @@
|
||||
{-|
|
||||
Module : PostgREST.ApiRequest
|
||||
Description : PostgREST functions to translate HTTP request to a domain type called ApiRequest.
|
||||
-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
|
||||
module PostgREST.ApiRequest (
|
||||
ApiRequest(..)
|
||||
, ContentType(..)
|
||||
, Action(..)
|
||||
, Target(..)
|
||||
, PreferRepresentation (..)
|
||||
, mutuallyAgreeable
|
||||
, 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 Control.Arrow ((***))
|
||||
import Data.Aeson.Types (emptyArray, emptyObject)
|
||||
import Data.List (last, lookup, partition)
|
||||
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 Network.Wai (Request (..))
|
||||
import Network.Wai.Parse (parseHttpAccept)
|
||||
import Web.Cookie (parseCookiesText)
|
||||
|
||||
|
||||
import Data.Ranged.Boundaries
|
||||
|
||||
import PostgREST.Error (ApiRequestError (..))
|
||||
import PostgREST.RangeQuery (NonnegRange, allRange, rangeGeq,
|
||||
rangeLimit, rangeOffset, rangeRequested,
|
||||
restrictRange)
|
||||
import PostgREST.Types
|
||||
import Protolude
|
||||
|
||||
type RequestBody = BL.ByteString
|
||||
|
||||
-- | Types of things a user wants to do to tables/views/procs
|
||||
data Action = ActionCreate | ActionRead
|
||||
| ActionUpdate | ActionDelete
|
||||
| ActionInfo | ActionInvoke{isReadOnly :: Bool}
|
||||
| ActionInspect | ActionSingleUpsert
|
||||
deriving Eq
|
||||
-- | The target db object of a user action
|
||||
data Target = TargetIdent QualifiedIdentifier
|
||||
| TargetProc{tpQi :: QualifiedIdentifier, tpIsRootSpec :: Bool}
|
||||
| TargetDefaultSpec -- The default spec offered at root "/"
|
||||
| TargetUnknown [Text]
|
||||
deriving Eq
|
||||
-- | How to return the inserted data
|
||||
data PreferRepresentation = Full | HeadersOnly | None deriving Eq
|
||||
|
||||
{-|
|
||||
Describes what the user wants to do. This data type is a
|
||||
translation of the raw elements of an HTTP request into domain
|
||||
specific language. There is no guarantee that the intent is
|
||||
sensible, it is up to a later stage of processing to determine
|
||||
if it is an action we are able to perform.
|
||||
-}
|
||||
data ApiRequest = ApiRequest {
|
||||
-- | Similar but not identical to HTTP verb, e.g. Create/Invoke both POST
|
||||
iAction :: Action
|
||||
-- | Requested range of rows within response
|
||||
, iRange :: M.HashMap ByteString NonnegRange
|
||||
-- | The target, be it calling a proc or accessing a table
|
||||
, iTarget :: Target
|
||||
-- | Content types the client will accept, [CTAny] if no Accept header
|
||||
, iAccepts :: [ContentType]
|
||||
-- | Data sent by client and used for mutation actions
|
||||
, iPayload :: Maybe PayloadJSON
|
||||
-- | If client wants created items echoed back
|
||||
, iPreferRepresentation :: PreferRepresentation
|
||||
-- | Pass all parameters as a single json object to a stored procedure
|
||||
, iPreferSingleObjectParameter :: Bool
|
||||
-- | Whether the client wants a result count (slower)
|
||||
, iPreferCount :: Bool
|
||||
-- | Whether the client wants to UPSERT or ignore records on PK conflict
|
||||
, iPreferResolution :: Maybe PreferResolution
|
||||
-- | Filters on the result ("id", "eq.10")
|
||||
, iFilters :: [(Text, Text)]
|
||||
-- | &and and &or parameters used for complex boolean logic
|
||||
, iLogic :: [(Text, Text)]
|
||||
-- | &select parameter used to shape the response
|
||||
, iSelect :: Text
|
||||
-- | &columns parameter used to shape the payload
|
||||
, iColumns :: Maybe Text
|
||||
-- | &order parameters for each level
|
||||
, iOrder :: [(Text, Text)]
|
||||
-- | Alphabetized (canonical) request query string for response URLs
|
||||
, iCanonicalQS :: ByteString
|
||||
-- | JSON Web Token
|
||||
, iJWT :: Text
|
||||
-- | HTTP request headers
|
||||
, iHeaders :: [(Text, Text)]
|
||||
-- | Request Cookies
|
||||
, iCookies :: [(Text, Text)]
|
||||
}
|
||||
|
||||
-- | Examines HTTP request and translates it into user intent.
|
||||
userApiRequest :: Schema -> Maybe QualifiedIdentifier -> Request -> RequestBody -> Either ApiRequestError ApiRequest
|
||||
userApiRequest schema rootSpec req reqBody
|
||||
| isTargetingProc && method `notElem` ["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
|
||||
, iAccepts = maybe [CTAny] (map decodeContentType . parseHttpAccept) $ lookupHeader "accept"
|
||||
, iPayload = relevantPayload
|
||||
, iPreferRepresentation = representation
|
||||
, iPreferSingleObjectParameter = singleObject
|
||||
, iPreferCount = hasPrefer "count=exact"
|
||||
, iPreferResolution = if hasPrefer (show MergeDuplicates) then Just MergeDuplicates
|
||||
else if hasPrefer (show IgnoreDuplicates) then Just IgnoreDuplicates
|
||||
else Nothing
|
||||
, iFilters = filters
|
||||
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
|
||||
, iSelect = toS $ fromMaybe "*" $ join $ lookup "select" 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))
|
||||
$ queryStringWPlus
|
||||
, iJWT = tokenStr
|
||||
, iHeaders = [ (toS $ CI.foldedCase k, toS v) | (k,v) <- hdrs, k /= hAuthorization, k /= hCookie]
|
||||
, iCookies = maybe [] parseCookiesText $ lookupHeader "Cookie"
|
||||
}
|
||||
where
|
||||
-- queryString with '+' not converted to ' '
|
||||
queryStringWPlus = parseQueryReplacePlus False $ 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{isReadOnly=True} -> partition (liftM2 (||) (isEmbedPath . fst) (hasOperator . snd)) flts
|
||||
_ -> (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
|
||||
contentType = decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type"
|
||||
columns | action `elem` [ActionCreate, ActionUpdate, ActionInvoke{isReadOnly=False}] = toS <$> join (lookup "columns" qParams)
|
||||
| otherwise = Nothing
|
||||
payload =
|
||||
case (contentType, action) of
|
||||
(_, ActionInvoke{isReadOnly=True}) ->
|
||||
Right $ ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> rpcQParams) PJObject (S.fromList $ fst <$> rpcQParams)
|
||||
(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) PJObject keys
|
||||
(ct, _) ->
|
||||
Left $ toS $ "Content-Type not acceptable: " <> toMime ct
|
||||
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges
|
||||
action =
|
||||
case method of
|
||||
"GET" | target == TargetDefaultSpec -> ActionInspect
|
||||
| isTargetingProc -> ActionInvoke{isReadOnly=True}
|
||||
| otherwise -> ActionRead
|
||||
|
||||
"POST" -> if isTargetingProc
|
||||
then ActionInvoke{isReadOnly=False}
|
||||
else ActionCreate
|
||||
"PATCH" -> ActionUpdate
|
||||
"PUT" -> ActionSingleUpsert
|
||||
"DELETE" -> ActionDelete
|
||||
"OPTIONS" -> ActionInfo
|
||||
_ -> ActionInspect
|
||||
target = case path of
|
||||
[] -> case rootSpec of
|
||||
Just rsQi -> TargetProc rsQi True
|
||||
Nothing -> TargetDefaultSpec
|
||||
[table] -> TargetIdent $ QualifiedIdentifier schema table
|
||||
["rpc", proc] -> TargetProc (QualifiedIdentifier schema proc) False
|
||||
other -> TargetUnknown other
|
||||
|
||||
shouldParsePayload = action `elem` [ActionCreate, ActionUpdate, ActionSingleUpsert, ActionInvoke{isReadOnly=False}, ActionInvoke{isReadOnly=True}]
|
||||
relevantPayload | shouldParsePayload = rightToMaybe payload
|
||||
| otherwise = Nothing
|
||||
path = pathInfo req
|
||||
method = requestMethod req
|
||||
hdrs = requestHeaders req
|
||||
qParams = [(toS k, v)|(k,v) <- queryStringWPlus]
|
||||
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
|
||||
singleObject = hasPrefer "params=single-object"
|
||||
representation
|
||||
| hasPrefer "return=representation" = Full
|
||||
| hasPrefer "return=minimal" = None
|
||||
| otherwise = HeadersOnly
|
||||
auth = fromMaybe "" $ lookupHeader hAuthorization
|
||||
tokenStr = case T.split (== ' ') (toS auth) of
|
||||
("Bearer" : t : _) -> t
|
||||
_ -> ""
|
||||
endingIn:: [Text] -> Text -> Bool
|
||||
endingIn xx key = lastWord `elem` xx
|
||||
where lastWord = last $ T.split (=='.') key
|
||||
|
||||
headerRange = rangeRequested hdrs
|
||||
replaceLast x s = T.intercalate "." $ L.init (T.split (=='.') s) ++ [x]
|
||||
limitParams :: M.HashMap ByteString NonnegRange
|
||||
limitParams = M.fromList [(toS (replaceLast "limit" k), restrictRange (readMaybe =<< (toS <$> v)) allRange) | (k,v) <- qParams, isJust v, endingIn ["limit"] k]
|
||||
offsetParams :: M.HashMap ByteString NonnegRange
|
||||
offsetParams = M.fromList [(toS (replaceLast "limit" k), maybe allRange rangeGeq (readMaybe =<< (toS <$> v))) | (k,v) <- qParams, isJust v, endingIn ["offset"] k]
|
||||
|
||||
urlRange = M.unionWith f limitParams offsetParams
|
||||
where
|
||||
f rl ro = Range (BoundaryBelow o) (BoundaryAbove $ o + l - 1)
|
||||
where
|
||||
l = fromMaybe 0 $ rangeLimit rl
|
||||
o = rangeOffset ro
|
||||
ranges = M.insert "limit" (rangeIntersection headerRange (fromMaybe allRange (M.lookup "limit" urlRange))) urlRange
|
||||
|
||||
{-|
|
||||
Find the best match from a list of content types accepted by the
|
||||
client in order of decreasing preference and a list of types
|
||||
producible by the server. If there is no match but the client
|
||||
accepts */* then return the top server pick.
|
||||
-}
|
||||
mutuallyAgreeable :: [ContentType] -> [ContentType] -> Maybe ContentType
|
||||
mutuallyAgreeable sProduces cAccepts =
|
||||
let exact = listToMaybe $ L.intersect cAccepts sProduces in
|
||||
if isNothing exact && CTAny `elem` cAccepts
|
||||
then listToMaybe sProduces
|
||||
else exact
|
||||
|
||||
type CsvData = V.Vector (M.HashMap Text BL.ByteString)
|
||||
|
||||
{-|
|
||||
Converts CSV like
|
||||
a,b
|
||||
1,hi
|
||||
2,bye
|
||||
|
||||
into a JSON array like
|
||||
[ {"a": "1", "b": "hi"}, {"a": 2, "b": "bye"} ]
|
||||
|
||||
The reason for its odd signature is so that it can compose
|
||||
directly with CSV.decodeByName
|
||||
-}
|
||||
csvToJson :: (CSV.Header, CsvData) -> JSON.Value
|
||||
csvToJson (_, vals) =
|
||||
JSON.Array $ V.map rowToJsonObj vals
|
||||
where
|
||||
rowToJsonObj = JSON.Object .
|
||||
M.map (\str ->
|
||||
if str == "NULL"
|
||||
then JSON.Null
|
||||
else JSON.String $ toS str
|
||||
)
|
||||
|
||||
payloadAttributes :: RequestBody -> JSON.Value -> Maybe PayloadJSON
|
||||
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
|
||||
areKeysUniform = all (\case
|
||||
JSON.Object x -> S.fromList (M.keys x) == canonicalKeys
|
||||
_ -> False) arr in
|
||||
if areKeysUniform
|
||||
then Just $ ProcessedJSON raw (PJArray $ V.length arr) canonicalKeys
|
||||
else Nothing
|
||||
Just _ -> Nothing
|
||||
Nothing -> Just emptyPJArray
|
||||
|
||||
JSON.Object o -> Just $ ProcessedJSON raw PJObject (S.fromList $ M.keys o)
|
||||
|
||||
-- truncate everything else to an empty array.
|
||||
_ -> Just emptyPJArray
|
||||
where
|
||||
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) (PJArray 0) S.empty
|
||||
+652
-384
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module PostgREST.AppState
|
||||
( AppState
|
||||
, destroy
|
||||
, getConfig
|
||||
, getDbStructure
|
||||
, getIsListenerOn
|
||||
, getJsonDbS
|
||||
, getMainThreadId
|
||||
, getPgVersion
|
||||
, getRetryNextIn
|
||||
, getTime
|
||||
, getWorkerSem
|
||||
, init
|
||||
, initWithPool
|
||||
, logWithZTime
|
||||
, putConfig
|
||||
, putDbStructure
|
||||
, putIsListenerOn
|
||||
, putJsonDbS
|
||||
, putPgVersion
|
||||
, putRetryNextIn
|
||||
, releasePool
|
||||
, 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.DbStructure (DbStructure)
|
||||
|
||||
import Protolude
|
||||
|
||||
|
||||
data AppState = AppState
|
||||
{ statePool :: SQL.Pool -- | Connection pool, either a 'Connection' or a 'ConnectionError'
|
||||
, statePgVersion :: IORef PgVersion
|
||||
-- | No schema cache at the start. Will be filled in by the connectionWorker
|
||||
, stateDbStructure :: IORef (Maybe DbStructure)
|
||||
-- | Cached DbStructure 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
|
||||
newPool <- initPool conf
|
||||
initWithPool newPool conf
|
||||
|
||||
initWithPool :: SQL.Pool -> AppConfig -> IO AppState
|
||||
initWithPool newPool conf =
|
||||
AppState newPool
|
||||
<$> 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 = releasePool
|
||||
|
||||
initPool :: AppConfig -> IO SQL.Pool
|
||||
initPool AppConfig{..} =
|
||||
SQL.acquire (configDbPoolSize, configDbPoolTimeout, toUtf8 configDbUri)
|
||||
|
||||
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
|
||||
usePool AppState{..} = SQL.use statePool
|
||||
|
||||
releasePool :: AppState -> IO ()
|
||||
releasePool AppState{..} = SQL.release statePool
|
||||
|
||||
getPgVersion :: AppState -> IO PgVersion
|
||||
getPgVersion = readIORef . statePgVersion
|
||||
|
||||
putPgVersion :: AppState -> PgVersion -> IO ()
|
||||
putPgVersion = atomicWriteIORef . statePgVersion
|
||||
|
||||
getDbStructure :: AppState -> IO (Maybe DbStructure)
|
||||
getDbStructure = readIORef . stateDbStructure
|
||||
|
||||
putDbStructure :: AppState -> Maybe DbStructure -> IO ()
|
||||
putDbStructure appState = atomicWriteIORef (stateDbStructure 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
|
||||
+95
-87
@@ -1,5 +1,3 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-|
|
||||
Module : PostgREST.Auth
|
||||
Description : PostgREST authorization functions.
|
||||
@@ -12,102 +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
|
||||
|
||||
{-|
|
||||
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.DbStructure (queryDbStructure)
|
||||
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 DbStructure 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 $
|
||||
queryDbStructure
|
||||
(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 dbStructure -> return $ JSON.encode dbStructure
|
||||
|
||||
-- | 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 to live, in seconds, for an idle database pool connection
|
||||
|db-pool-timeout = 3600
|
||||
|
|
||||
|## 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"
|
||||
|]
|
||||
+389
-224
@@ -1,197 +1,364 @@
|
||||
{-|
|
||||
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.Exception (Handler (..))
|
||||
import Control.Lens (preview)
|
||||
import Control.Monad (fail)
|
||||
import Crypto.JWT (StringOrURI, stringOrUri)
|
||||
import Data.List (lookup)
|
||||
import Data.Scientific (floatingOrInteger)
|
||||
import Data.Text (dropEnd, dropWhileEnd,
|
||||
intercalate, lines, splitOn,
|
||||
strip, take, unpack)
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Text.IO (hPutStrLn)
|
||||
import Data.Version (versionBranch)
|
||||
import Development.GitRev (gitHash)
|
||||
import Network.Wai.Middleware.Cors (CorsResourcePolicy (..))
|
||||
import Paths_postgrest (version)
|
||||
import System.IO.Error (IOError)
|
||||
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 Data.Time.Clock (NominalDiffTime)
|
||||
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.DbStructure.Identifiers (QualifiedIdentifier, dumpQi,
|
||||
toQi)
|
||||
import PostgREST.MediaType (MediaType (..), toMime)
|
||||
|
||||
import PostgREST.Error (ApiRequestError (..))
|
||||
import PostgREST.Parsers (pRoleClaimKey)
|
||||
import PostgREST.Types (JSPath, JSPathExp (..),
|
||||
QualifiedIdentifier (..))
|
||||
import Protolude hiding (concat, hPutStrLn, intercalate, null,
|
||||
take, (<>))
|
||||
import Protolude hiding (Proxy, toList)
|
||||
|
||||
|
||||
-- | Config file settings for the server
|
||||
data AppConfig = AppConfig {
|
||||
configDatabase :: Text
|
||||
, configAnonRole :: Text
|
||||
, configProxyUri :: Maybe Text
|
||||
, configSchema :: Text
|
||||
, configHost :: Text
|
||||
, configPort :: Int
|
||||
, configSocket :: Maybe Text
|
||||
|
||||
, 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 QualifiedIdentifier
|
||||
data AppConfig = AppConfig
|
||||
{ configAppSettings :: [(Text, Text)]
|
||||
, configDbAnonRole :: Maybe Text
|
||||
, configDbChannel :: Text
|
||||
, configDbChannelEnabled :: Bool
|
||||
, configDbExtraSearchPath :: [Text]
|
||||
, configDbMaxRows :: Maybe Integer
|
||||
, configDbPlanEnabled :: Bool
|
||||
, configDbPoolSize :: Int
|
||||
, configDbPoolTimeout :: NominalDiffTime
|
||||
, 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-timeout", show . floor . configDbPoolTimeout)
|
||||
,("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\t" <> 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
|
||||
dbSchema = reqString "db-schema"
|
||||
parseConfig =
|
||||
AppConfig
|
||||
<$> reqString "db-uri"
|
||||
<*> reqString "db-anon-role"
|
||||
<*> optString "server-proxy-uri"
|
||||
<*> dbSchema
|
||||
<*> (fromMaybe "!4" <$> optString "server-host")
|
||||
<*> (fromMaybe 3000 <$> optInt "server-port")
|
||||
<*> optString "server-unix-socket"
|
||||
<*> (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"] splitExtraSearchPath <$> optValue "db-extra-search-path")
|
||||
<*> ((\x y -> QualifiedIdentifier x <$> y) <$> dbSchema <*> optString "root-spec")
|
||||
-- Both C.ParseError and IOError are shown here
|
||||
loadConfig :: FilePath -> IO (Either SomeException C.Config)
|
||||
loadConfig = try . C.load
|
||||
|
||||
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")
|
||||
<*> (fromIntegral . fromMaybe 3600 <$> optInt "db-pool-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 =
|
||||
optString k >>= \case
|
||||
Nothing -> pure 432 -- return default 660 mode if no value was provided
|
||||
Just fileModeText ->
|
||||
case readOct $ T.unpack fileModeText of
|
||||
[] ->
|
||||
fail "Invalid server-unix-socket-mode: not an octal"
|
||||
(fileMode, _):_ ->
|
||||
if fileMode < 384 || fileMode > 511
|
||||
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."
|
||||
|
||||
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_timeout", "db_config"]
|
||||
then lookup dbSettingName dbSettings
|
||||
else Nothing
|
||||
|
||||
coerceText :: C.Value -> Text
|
||||
coerceText (C.String s) = s
|
||||
@@ -199,84 +366,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) = T.strip <$> T.splitOn "," s
|
||||
splitOnCommas _ = []
|
||||
|
||||
splitExtraSearchPath :: C.Value -> [Text]
|
||||
splitExtraSearchPath (C.String s) = strip <$> splitOn "," s
|
||||
splitExtraSearchPath _ = []
|
||||
-- | 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)
|
||||
|
||||
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)
|
||||
)
|
||||
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 "." "="
|
||||
|
||||
parserPrefs = prefs showHelpOnError
|
||||
-- | 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 }
|
||||
|
||||
exitErr :: Text -> IO a
|
||||
exitErr err = do
|
||||
hPutStrLn stderr err
|
||||
exitFailure
|
||||
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
|
||||
|
||||
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"
|
||||
|
|
||||
|## base url for swagger output
|
||||
|# server-proxy-uri = ""
|
||||
|
|
||||
|## choose a secret, JSON Web Key (or set) to enable JWT auth
|
||||
|## (use "@filename" to load from separate file)
|
||||
|# jwt-secret = "foo"
|
||||
|# 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"
|
||||
|]
|
||||
-- | 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 }
|
||||
|
||||
pathParser :: Parser FilePath
|
||||
pathParser =
|
||||
strArgument $
|
||||
metavar "FILENAME" <>
|
||||
help "Path to configuration file"
|
||||
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 '"')
|
||||
@@ -0,0 +1,64 @@
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
module PostgREST.Config.PgVersion
|
||||
( PgVersion(..)
|
||||
, minimumPgVersion
|
||||
, pgVersion96
|
||||
, pgVersion100
|
||||
, pgVersion109
|
||||
, pgVersion110
|
||||
, pgVersion112
|
||||
, pgVersion114
|
||||
, pgVersion120
|
||||
, pgVersion121
|
||||
, pgVersion130
|
||||
, pgVersion140
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
|
||||
import Protolude
|
||||
|
||||
|
||||
data PgVersion = PgVersion
|
||||
{ pgvNum :: Int32
|
||||
, pgvName :: Text
|
||||
}
|
||||
deriving (Eq, Generic, JSON.ToJSON)
|
||||
|
||||
instance Ord PgVersion where
|
||||
(PgVersion v1 _) `compare` (PgVersion v2 _) = v1 `compare` v2
|
||||
|
||||
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
|
||||
minimumPgVersion :: PgVersion
|
||||
minimumPgVersion = pgVersion96
|
||||
|
||||
pgVersion96 :: PgVersion
|
||||
pgVersion96 = PgVersion 90600 "9.6"
|
||||
|
||||
pgVersion100 :: PgVersion
|
||||
pgVersion100 = PgVersion 100000 "10"
|
||||
|
||||
pgVersion109 :: PgVersion
|
||||
pgVersion109 = PgVersion 100009 "10.9"
|
||||
|
||||
pgVersion110 :: PgVersion
|
||||
pgVersion110 = PgVersion 110000 "11.0"
|
||||
|
||||
pgVersion112 :: PgVersion
|
||||
pgVersion112 = PgVersion 110002 "11.2"
|
||||
|
||||
pgVersion114 :: PgVersion
|
||||
pgVersion114 = PgVersion 110004 "11.4"
|
||||
|
||||
pgVersion120 :: PgVersion
|
||||
pgVersion120 = PgVersion 120000 "12.0"
|
||||
|
||||
pgVersion121 :: PgVersion
|
||||
pgVersion121 = PgVersion 120001 "12.1"
|
||||
|
||||
pgVersion130 :: PgVersion
|
||||
pgVersion130 = PgVersion 130000 "13.0"
|
||||
|
||||
pgVersion140 :: PgVersion
|
||||
pgVersion140 = PgVersion 140000 "14.0"
|
||||
@@ -0,0 +1,77 @@
|
||||
{-|
|
||||
Module : PostgREST.Private.ProxyUri
|
||||
Description : Proxy Uri validator
|
||||
-}
|
||||
module PostgREST.Config.Proxy
|
||||
( Proxy(..)
|
||||
, isMalformedProxyUri
|
||||
, toURI
|
||||
) where
|
||||
|
||||
import qualified Data.Text as T
|
||||
|
||||
import Data.Maybe (fromJust)
|
||||
import Network.URI (URI (..), URIAuth (..), isAbsoluteURI, parseURI)
|
||||
|
||||
import Protolude hiding (Proxy)
|
||||
|
||||
data Proxy = Proxy
|
||||
{ proxyScheme :: Text
|
||||
, proxyHost :: Text
|
||||
, proxyPort :: Integer
|
||||
, proxyPath :: Text
|
||||
}
|
||||
|
||||
{-|
|
||||
Test whether a proxy uri is malformed or not.
|
||||
A valid proxy uri should be an absolute uri without query and user info,
|
||||
only http(s) schemes are valid, port number range is 1-65535.
|
||||
|
||||
For example
|
||||
http://postgrest.com/openapi.json
|
||||
https://postgrest.com:8080/openapi.json
|
||||
-}
|
||||
isMalformedProxyUri :: Text -> Bool
|
||||
isMalformedProxyUri uri
|
||||
| isAbsoluteURI (toS uri) = not $ isUriValid $ toURI uri
|
||||
| otherwise = True
|
||||
|
||||
toURI :: Text -> URI
|
||||
toURI uri = fromJust $ parseURI (toS uri)
|
||||
|
||||
isUriValid:: URI -> Bool
|
||||
isUriValid = fAnd [isSchemeValid, isQueryValid, isAuthorityValid]
|
||||
|
||||
fAnd :: [a -> Bool] -> a -> Bool
|
||||
fAnd fs x = all ($ x) fs
|
||||
|
||||
isSchemeValid :: URI -> Bool
|
||||
isSchemeValid URI {uriScheme = s}
|
||||
| T.toLower (T.pack s) == "https:" = True
|
||||
| T.toLower (T.pack s) == "http:" = True
|
||||
| otherwise = False
|
||||
|
||||
isQueryValid :: URI -> Bool
|
||||
isQueryValid URI {uriQuery = ""} = True
|
||||
isQueryValid _ = False
|
||||
|
||||
isAuthorityValid :: URI -> Bool
|
||||
isAuthorityValid URI {uriAuthority = a}
|
||||
| isJust a = fAnd [isUserInfoValid, isHostValid, isPortValid] $ fromJust a
|
||||
| otherwise = False
|
||||
|
||||
isUserInfoValid :: URIAuth -> Bool
|
||||
isUserInfoValid URIAuth {uriUserInfo = ""} = True
|
||||
isUserInfoValid _ = False
|
||||
|
||||
isHostValid :: URIAuth -> Bool
|
||||
isHostValid URIAuth {uriRegName = ""} = False
|
||||
isHostValid _ = True
|
||||
|
||||
isPortValid :: URIAuth -> Bool
|
||||
isPortValid URIAuth {uriPort = ""} = True
|
||||
isPortValid URIAuth {uriPort = (':':p)} =
|
||||
case readMaybe p of
|
||||
Just i -> i > (0 :: Integer) && i < 65536
|
||||
Nothing -> False
|
||||
isPortValid _ = False
|
||||
@@ -0,0 +1,42 @@
|
||||
{-|
|
||||
Module : PostgREST.Cors
|
||||
Description : Wai Middleware to set cors policy.
|
||||
-}
|
||||
module PostgREST.Cors (middleware) where
|
||||
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Network.Wai as Wai
|
||||
import qualified Network.Wai.Middleware.Cors as Wai
|
||||
|
||||
import Data.List (lookup)
|
||||
|
||||
import Protolude
|
||||
|
||||
middleware :: Wai.Middleware
|
||||
middleware = Wai.cors corsPolicy
|
||||
|
||||
-- | CORS policy to be used in by Wai Cors middleware
|
||||
corsPolicy :: Wai.Request -> Maybe Wai.CorsResourcePolicy
|
||||
corsPolicy req = case lookup "origin" headers of
|
||||
Just origin ->
|
||||
Just Wai.CorsResourcePolicy
|
||||
{ Wai.corsOrigins = Just ([origin], True)
|
||||
, Wai.corsMethods = ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"]
|
||||
, Wai.corsRequestHeaders = "Authorization" : accHeaders
|
||||
, Wai.corsExposedHeaders = Just
|
||||
[ "Content-Encoding", "Content-Location", "Content-Range", "Content-Type"
|
||||
, "Date", "Location", "Server", "Transfer-Encoding", "Range-Unit"]
|
||||
, Wai.corsMaxAge = Just $ 60*60*24
|
||||
, Wai.corsVaryOrigin = False
|
||||
, Wai.corsRequireOrigin = False
|
||||
, Wai.corsIgnoreFailures = True
|
||||
}
|
||||
Nothing -> Nothing
|
||||
where
|
||||
headers = Wai.requestHeaders req
|
||||
accHeaders = case lookup "access-control-request-headers" headers of
|
||||
Just hdrs -> map (CI.mk . BS.strip) $ BS.split ',' hdrs
|
||||
-- Impossible case, Middleware.Cors will not evaluate this when
|
||||
-- the Access-Control-Request-Headers header is not set.
|
||||
Nothing -> []
|
||||
@@ -1,360 +0,0 @@
|
||||
{-|
|
||||
Module : PostgREST.DbRequestBuilder
|
||||
Description : PostgREST database request builder
|
||||
|
||||
This module is in charge of building an intermediate representation(ReadRequest, MutateRequest) between the HTTP request and the final resulting SQL query.
|
||||
|
||||
A query tree is built in case of resource embedding. By inferring the relationship between tables, join conditions are added for every embedded resource.
|
||||
-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module PostgREST.DbRequestBuilder (
|
||||
readRequest
|
||||
, mutateRequest
|
||||
, fieldNames
|
||||
) where
|
||||
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import qualified Data.Set as S
|
||||
|
||||
import Control.Arrow ((***))
|
||||
import Control.Lens.Getter (view)
|
||||
import Control.Lens.Tuple (_1)
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
import Data.Foldable (foldr1)
|
||||
import Data.List (delete)
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.Text (isInfixOf)
|
||||
import Text.Regex.TDFA ((=~))
|
||||
import Unsafe (unsafeHead)
|
||||
|
||||
import Control.Applicative
|
||||
import Data.Tree
|
||||
import Network.Wai
|
||||
|
||||
import PostgREST.ApiRequest (Action (..), ApiRequest (..),
|
||||
PreferRepresentation (..),
|
||||
PreferRepresentation (..), Target (..))
|
||||
import PostgREST.Error (ApiRequestError (..), errorResponseFor)
|
||||
import PostgREST.Parsers
|
||||
import PostgREST.RangeQuery (NonnegRange, allRange, restrictRange)
|
||||
import PostgREST.Types
|
||||
import Protolude hiding (from)
|
||||
|
||||
readRequest :: Maybe Integer -> [Relation] -> Maybe ProcDescription -> ApiRequest -> Either Response ReadRequest
|
||||
readRequest maxRows allRels proc apiRequest =
|
||||
mapLeft errorResponseFor $
|
||||
treeRestrictRange maxRows =<<
|
||||
augumentRequestWithJoin schema relations =<<
|
||||
addFiltersOrdersRanges apiRequest <*>
|
||||
(buildReadRequest <$> pRequestSelect (iSelect apiRequest))
|
||||
where
|
||||
action = iAction apiRequest
|
||||
(schema, rootTableName) = fromJust $ -- Make it safe
|
||||
let target = iTarget apiRequest in
|
||||
case target of
|
||||
(TargetIdent (QualifiedIdentifier s t) ) -> Just (s, t)
|
||||
(TargetProc (QualifiedIdentifier s pName) _ ) -> Just (s, tName)
|
||||
where
|
||||
tName = case pdReturnType <$> proc of
|
||||
Just (SetOf (Composite qi)) -> qiName qi
|
||||
Just (Single (Composite qi)) -> qiName qi
|
||||
_ -> pName
|
||||
|
||||
_ -> Nothing
|
||||
|
||||
-- Build tree with a Depth attribute so when a self join occurs we can differentiate the parent and child tables by having
|
||||
-- an alias like "table_depth", this is related to issue #987.
|
||||
buildReadRequest :: [Tree SelectItem] -> ReadRequest
|
||||
buildReadRequest fieldTree =
|
||||
let rootDepth = 0
|
||||
rootNodeName = if action == ActionRead then rootTableName else sourceCTEName in
|
||||
foldr (treeEntry rootDepth) (Node (Select [] rootNodeName Nothing [] [] [] [] allRange, (rootNodeName, Nothing, Nothing, Nothing, rootDepth)) []) fieldTree
|
||||
where
|
||||
treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest
|
||||
treeEntry depth (Node fld@((fn, _),_,alias,relationDetail) fldForest) (Node (q, i) rForest) =
|
||||
let nxtDepth = succ depth in
|
||||
case fldForest of
|
||||
[] -> Node (q {select=fld:select q}, i) rForest
|
||||
_ -> Node (q, i) $
|
||||
foldr (treeEntry nxtDepth) (Node (Select [] fn Nothing [] [] [] [] allRange, (fn, Nothing, alias, relationDetail, nxtDepth)) []) fldForest:rForest
|
||||
|
||||
relations :: [Relation]
|
||||
relations = case action of
|
||||
ActionCreate -> fakeSourceRelations ++ allRels
|
||||
ActionUpdate -> fakeSourceRelations ++ allRels
|
||||
ActionDelete -> fakeSourceRelations ++ allRels
|
||||
ActionInvoke _ -> fakeSourceRelations ++ allRels
|
||||
_ -> allRels
|
||||
where fakeSourceRelations = mapMaybe (toSourceRelation rootTableName) allRels
|
||||
|
||||
-- in a relation where one of the tables matches "TableName"
|
||||
-- replace the name to that table with pg_source
|
||||
-- this "fake" relations is needed so that in a mutate query
|
||||
-- we can look at the "returning *" part which is wrapped with a "with"
|
||||
-- as just another table that has relations with other tables
|
||||
toSourceRelation :: TableName -> Relation -> Maybe Relation
|
||||
toSourceRelation mt r@(Relation t _ ft _ _ rt _ _)
|
||||
| mt == tableName t = Just $ r {relTable=t {tableName=sourceCTEName}}
|
||||
| mt == tableName ft = Just $ r {relFTable=t {tableName=sourceCTEName}}
|
||||
| Just mt == (tableName <$> rt) = Just $ r {relLinkTable=(\tbl -> tbl {tableName=sourceCTEName}) <$> rt}
|
||||
| otherwise = Nothing
|
||||
|
||||
treeRestrictRange :: Maybe Integer -> ReadRequest -> Either ApiRequestError ReadRequest
|
||||
treeRestrictRange maxRows_ request = pure $ nodeRestrictRange maxRows_ `fmap` request
|
||||
where
|
||||
nodeRestrictRange :: Maybe Integer -> ReadNode -> ReadNode
|
||||
nodeRestrictRange m (q@Select {range_=r}, i) = (q{range_=restrictRange m r }, i)
|
||||
|
||||
augumentRequestWithJoin :: Schema -> [Relation] -> ReadRequest -> Either ApiRequestError ReadRequest
|
||||
augumentRequestWithJoin schema allRels request =
|
||||
addRelations schema allRels Nothing request
|
||||
>>= addJoinConditions schema Nothing
|
||||
|
||||
addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest
|
||||
addRelations schema allRelations parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, relationDetail, depth)) forest) =
|
||||
case parentNode of
|
||||
Just (Node (Select{from=parentNodeTable}, _) _) ->
|
||||
let newFrom r = if tbl == nodeName then tableName (relTable r) else tbl
|
||||
newReadNode = (\r -> (query{from=newFrom r}, (nodeName, Just r, alias, Nothing, depth))) <$> rel
|
||||
rel :: Either ApiRequestError Relation
|
||||
rel = note (NoRelationBetween parentNodeTable nodeName) $
|
||||
findRelation schema allRelations nodeName parentNodeTable relationDetail in
|
||||
Node <$> newReadNode <*> (updateForest . hush $ Node <$> newReadNode <*> pure forest)
|
||||
_ ->
|
||||
let rn = (query, (nodeName, Just r, alias, Nothing, depth))
|
||||
r = Relation t [] t [] Root Nothing Nothing Nothing
|
||||
t = Table schema nodeName Nothing True in -- !!! TODO find another way to get the table from the query
|
||||
Node rn <$> updateForest (Just $ Node rn forest)
|
||||
where
|
||||
updateForest :: Maybe ReadRequest -> Either ApiRequestError [ReadRequest]
|
||||
updateForest rq = mapM (addRelations schema allRelations rq) forest
|
||||
|
||||
findRelation :: Schema -> [Relation] -> NodeName -> TableName -> Maybe RelationDetail -> Maybe Relation
|
||||
findRelation schema allRelations nodeTableName parentNodeTableName relationDetail =
|
||||
find (\Relation{relTable, relColumns, relFTable, relFColumns, relType, relLinkTable} ->
|
||||
-- Both relation ends need to be on the exposed schema
|
||||
schema == tableSchema relTable && schema == tableSchema relFTable &&
|
||||
case relationDetail of
|
||||
Nothing ->
|
||||
|
||||
-- (request) => projects { ..., clients{...} }
|
||||
-- will match
|
||||
-- (relation type) => parent
|
||||
-- (entity) => clients {id}
|
||||
-- (foriegn entity) => projects {client_id}
|
||||
(
|
||||
nodeTableName == tableName relTable && -- match relation table name
|
||||
parentNodeTableName == tableName relFTable -- match relation foreign table name
|
||||
) ||
|
||||
|
||||
-- (request) => projects { ..., client_id{...} }
|
||||
-- will match
|
||||
-- (relation type) => parent
|
||||
-- (entity) => clients {id}
|
||||
-- (foriegn entity) => projects {client_id}
|
||||
(
|
||||
parentNodeTableName == tableName relFTable &&
|
||||
length relFColumns == 1 &&
|
||||
-- match common foreign key names(table_name_id, table_name_fk) to table_name
|
||||
(toS ("^" <> colName (unsafeHead relFColumns) <> "_?(?:|[iI][dD]|[fF][kK])$") :: BS.ByteString) =~ (toS nodeTableName :: BS.ByteString)
|
||||
)
|
||||
|
||||
-- (request) => project_id { ..., client_id{...} }
|
||||
-- will match
|
||||
-- (relation type) => parent
|
||||
-- (entity) => clients {id}
|
||||
-- (foriegn entity) => projects {client_id}
|
||||
-- this case works becasue before reaching this place
|
||||
-- addRelation will turn project_id to project so the above condition will match
|
||||
|
||||
Just rd ->
|
||||
|
||||
-- (request) => clients { ..., projects.client_id{...} }
|
||||
-- will match
|
||||
-- (relation type) => child
|
||||
-- (entity) => clients {id}
|
||||
-- (foriegn entity) => projects {client_id}
|
||||
(
|
||||
relType == Child &&
|
||||
nodeTableName == tableName relTable && -- match relation table name
|
||||
parentNodeTableName == tableName relFTable && -- match relation foreign table name
|
||||
length relColumns == 1 &&
|
||||
rd == colName (unsafeHead relColumns)
|
||||
) ||
|
||||
|
||||
-- (request) => message { ..., person_detail.sender{...} }
|
||||
-- will match
|
||||
-- (relation type) => parent
|
||||
-- (entity) => message {sender}
|
||||
-- (foriegn entity) => person_detail {id}
|
||||
(
|
||||
relType == Parent &&
|
||||
nodeTableName == tableName relTable && -- match relation table name
|
||||
parentNodeTableName == tableName relFTable && -- match relation foreign table name
|
||||
length relFColumns == 1 &&
|
||||
rd == colName (unsafeHead relFColumns)
|
||||
) ||
|
||||
|
||||
-- (request) => tasks { ..., users.tasks_users{...} }
|
||||
-- will match
|
||||
-- (relation type) => many
|
||||
-- (entity) => users
|
||||
-- (foriegn entity) => tasks
|
||||
(
|
||||
relType == Many &&
|
||||
nodeTableName == tableName relTable && -- match relation table name
|
||||
parentNodeTableName == tableName relFTable && -- match relation foreign table name
|
||||
rd == tableName (fromJust relLinkTable)
|
||||
)
|
||||
) allRelations
|
||||
|
||||
-- previousAlias is only used for the case of self joins
|
||||
addJoinConditions :: Schema -> Maybe Alias -> ReadRequest -> Either ApiRequestError ReadRequest
|
||||
addJoinConditions schema previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_, relation, _, _, depth)) forest) =
|
||||
case relation of
|
||||
Just Relation{relType=Root} -> Node node <$> updatedForest -- this is the root node
|
||||
Just rel@Relation{relType=Parent} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
|
||||
Just rel@Relation{relType=Child} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
|
||||
Just rel@Relation{relType=Many, relLinkTable=(Just linkTable)} ->
|
||||
let rq = augmentQuery rel in
|
||||
Node (rq{implicitJoins=tableName linkTable:implicitJoins rq}, nodeProps) <$> updatedForest
|
||||
_ -> Left UnknownRelation
|
||||
where
|
||||
newAlias = case isSelfJoin <$> relation of
|
||||
Just True
|
||||
| depth /= 0 -> Just (tbl <> "_" <> show depth) -- root node doesn't get aliased
|
||||
| otherwise -> Nothing
|
||||
_ -> Nothing
|
||||
augmentQuery rel =
|
||||
foldr
|
||||
(\jc rq@Select{joinConditions=jcs} -> rq{joinConditions=jc:jcs})
|
||||
query{fromAlias=newAlias}
|
||||
(getJoinConditions previousAlias newAlias rel)
|
||||
updatedForest = mapM (addJoinConditions schema newAlias) forest
|
||||
|
||||
-- previousAlias and newAlias are used in the case of self joins
|
||||
getJoinConditions :: Maybe Alias -> Maybe Alias -> Relation -> [JoinCondition]
|
||||
getJoinConditions previousAlias newAlias (Relation Table{tableSchema=tSchema, tableName=tN} cols Table{tableName=ftN} fCols typ lt lc1 lc2) =
|
||||
case typ of
|
||||
Child ->
|
||||
zipWith (toJoinCondition tN ftN) cols fCols
|
||||
Parent ->
|
||||
zipWith (toJoinCondition tN ftN) cols fCols
|
||||
Many ->
|
||||
let ltN = maybe "" tableName lt in
|
||||
zipWith (toJoinCondition tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toJoinCondition ftN ltN) fCols (fromMaybe [] lc2)
|
||||
Root -> witness
|
||||
where
|
||||
toJoinCondition :: Text -> Text -> Column -> Column -> JoinCondition
|
||||
toJoinCondition tb ftb c fc =
|
||||
let qi1 = QualifiedIdentifier tSchema tb
|
||||
qi2 = QualifiedIdentifier tSchema ftb in
|
||||
JoinCondition (maybe qi1 (QualifiedIdentifier mempty) newAlias, colName c)
|
||||
(maybe qi2 (QualifiedIdentifier mempty) previousAlias, colName fc)
|
||||
|
||||
addFiltersOrdersRanges :: ApiRequest -> Either ApiRequestError (ReadRequest -> ReadRequest)
|
||||
addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [
|
||||
flip (foldr addFilter) <$> filters,
|
||||
flip (foldr addOrder) <$> orders,
|
||||
flip (foldr addRange) <$> ranges,
|
||||
flip (foldr addLogicTree) <$> logicForest
|
||||
]
|
||||
{-
|
||||
The esence of what is going on above is that we are composing tree functions
|
||||
of type (ReadRequest->ReadRequest) that are in (Either ParseError a) context
|
||||
-}
|
||||
where
|
||||
filters :: Either ApiRequestError [(EmbedPath, Filter)]
|
||||
filters = mapM pRequestFilter flts
|
||||
logicForest :: Either ApiRequestError [(EmbedPath, LogicTree)]
|
||||
logicForest = mapM pRequestLogicTree logFrst
|
||||
action = iAction apiRequest
|
||||
-- there can be no filters on the root table when we are doing insert/update/delete
|
||||
(flts, logFrst) =
|
||||
case action of
|
||||
ActionInvoke _ -> (iFilters apiRequest, iLogic apiRequest)
|
||||
ActionRead -> (iFilters apiRequest, iLogic apiRequest)
|
||||
_ -> join (***) (filter (( "." `isInfixOf` ) . fst)) (iFilters apiRequest, iLogic apiRequest)
|
||||
orders :: Either ApiRequestError [(EmbedPath, [OrderTerm])]
|
||||
orders = mapM pRequestOrder $ iOrder apiRequest
|
||||
ranges :: Either ApiRequestError [(EmbedPath, NonnegRange)]
|
||||
ranges = mapM pRequestRange $ M.toList $ iRange apiRequest
|
||||
|
||||
addFilterToNode :: Filter -> ReadRequest -> ReadRequest
|
||||
addFilterToNode flt (Node (q@Select {where_=lf}, i) f) = Node (q{where_=addFilterToLogicForest flt lf}::ReadQuery, i) f
|
||||
|
||||
addFilter :: (EmbedPath, Filter) -> ReadRequest -> ReadRequest
|
||||
addFilter = addProperty addFilterToNode
|
||||
|
||||
addOrderToNode :: [OrderTerm] -> ReadRequest -> ReadRequest
|
||||
addOrderToNode o (Node (q,i) f) = Node (q{order=o}, i) f
|
||||
|
||||
addOrder :: (EmbedPath, [OrderTerm]) -> ReadRequest -> ReadRequest
|
||||
addOrder = addProperty addOrderToNode
|
||||
|
||||
addRangeToNode :: NonnegRange -> ReadRequest -> ReadRequest
|
||||
addRangeToNode r (Node (q,i) f) = Node (q{range_=r}, i) f
|
||||
|
||||
addRange :: (EmbedPath, NonnegRange) -> ReadRequest -> ReadRequest
|
||||
addRange = addProperty addRangeToNode
|
||||
|
||||
addLogicTreeToNode :: LogicTree -> ReadRequest -> ReadRequest
|
||||
addLogicTreeToNode t (Node (q@Select{where_=lf},i) f) = Node (q{where_=t:lf}::ReadQuery, i) f
|
||||
|
||||
addLogicTree :: (EmbedPath, LogicTree) -> ReadRequest -> ReadRequest
|
||||
addLogicTree = addProperty addLogicTreeToNode
|
||||
|
||||
addProperty :: (a -> ReadRequest -> ReadRequest) -> (EmbedPath, a) -> ReadRequest -> ReadRequest
|
||||
addProperty f ([], a) rr = f a rr
|
||||
addProperty f (targetNodeName:remainingPath, a) (Node rn forest) =
|
||||
case pathNode of
|
||||
Nothing -> Node rn forest -- the property is silenty dropped in the Request does not contain the required path
|
||||
Just tn -> Node rn (addProperty f (remainingPath, a) tn:delete tn forest)
|
||||
where
|
||||
pathNode = find (\(Node (_,(nodeName,_,alias,_,_)) _) -> nodeName == targetNodeName || alias == Just targetNodeName) forest
|
||||
|
||||
mutateRequest :: ApiRequest -> TableName -> S.Set FieldName -> [FieldName] -> [FieldName] -> Either Response MutateRequest
|
||||
mutateRequest apiRequest tName cols pkCols fldNames = mapLeft errorResponseFor $
|
||||
case action of
|
||||
ActionCreate -> Right $ Insert tName cols ((,) <$> iPreferResolution apiRequest <*> Just pkCols) [] returnings
|
||||
ActionUpdate -> Update tName cols <$> combinedLogic <*> pure returnings
|
||||
ActionSingleUpsert ->
|
||||
(\flts ->
|
||||
if null (iLogic apiRequest) &&
|
||||
S.fromList (fst <$> iFilters apiRequest) == S.fromList pkCols &&
|
||||
not (null (S.fromList pkCols)) &&
|
||||
all (\case
|
||||
Filter _ (OpExpr False (Op "eq" _)) -> True
|
||||
_ -> False) flts
|
||||
then Insert tName cols (Just (MergeDuplicates, pkCols)) <$> combinedLogic <*> pure returnings
|
||||
else
|
||||
Left InvalidFilters) =<< filters
|
||||
ActionDelete -> Delete tName <$> combinedLogic <*> pure returnings
|
||||
_ -> Left UnsupportedVerb
|
||||
where
|
||||
action = iAction apiRequest
|
||||
returnings = if iPreferRepresentation apiRequest == None then [] else fldNames
|
||||
filters = map snd <$> mapM pRequestFilter mutateFilters
|
||||
logic = map snd <$> mapM pRequestLogicTree logicFilters
|
||||
combinedLogic = foldr addFilterToLogicForest <$> logic <*> filters
|
||||
-- update/delete filters can be only on the root table
|
||||
(mutateFilters, logicFilters) = join (***) onlyRoot (iFilters apiRequest, iLogic apiRequest)
|
||||
onlyRoot = filter (not . ( "." `isInfixOf` ) . fst)
|
||||
|
||||
fieldNames :: ReadRequest -> [FieldName]
|
||||
fieldNames (Node (sel, _) forest) =
|
||||
map (fst . view _1) (select sel) ++ map colName fks
|
||||
where
|
||||
fks = concatMap (fromMaybe [] . f) forest
|
||||
f (Node (_, (_, Just Relation{relFColumns=cols, relType=Parent}, _, _, _)) _) = Just cols
|
||||
f _ = Nothing
|
||||
|
||||
-- Traditional filters(e.g. id=eq.1) are added as root nodes of the LogicTree
|
||||
-- they are later concatenated with AND in the QueryBuilder
|
||||
addFilterToLogicForest :: Filter -> [LogicTree] -> [LogicTree]
|
||||
addFilterToLogicForest flt lf = Stmnt flt : lf
|
||||
+802
-672
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
|
||||
module PostgREST.DbStructure.Identifiers
|
||||
( QualifiedIdentifier(..)
|
||||
, Schema
|
||||
, TableName
|
||||
, FieldName
|
||||
, dumpQi
|
||||
, toQi
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.Text as T
|
||||
|
||||
import Protolude
|
||||
|
||||
|
||||
-- | Represents a pg identifier with a prepended schema name "schema.table".
|
||||
-- When qiSchema is "", the schema is defined by the pg search_path.
|
||||
data QualifiedIdentifier = QualifiedIdentifier
|
||||
{ qiSchema :: Schema
|
||||
, qiName :: TableName
|
||||
}
|
||||
deriving (Eq, Ord, Generic, JSON.ToJSON, JSON.ToJSONKey)
|
||||
|
||||
instance Hashable QualifiedIdentifier
|
||||
|
||||
dumpQi :: QualifiedIdentifier -> Text
|
||||
dumpQi (QualifiedIdentifier s i) =
|
||||
(if T.null s then mempty else s <> ".") <> i
|
||||
|
||||
-- TODO: Handle a case where the QI comes like this: "my.fav.schema"."my.identifier"
|
||||
-- Right now it only handles the schema.identifier case
|
||||
toQi :: Text -> QualifiedIdentifier
|
||||
toQi txt = case T.drop 1 <$> T.breakOn "." txt of
|
||||
(i, "") -> QualifiedIdentifier mempty i
|
||||
(s, i) -> QualifiedIdentifier s i
|
||||
|
||||
type Schema = Text
|
||||
type TableName = Text
|
||||
type FieldName = Text
|
||||
@@ -0,0 +1,91 @@
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
|
||||
module PostgREST.DbStructure.Proc
|
||||
( PgType(..)
|
||||
, ProcDescription(..)
|
||||
, ProcParam(..)
|
||||
, ProcVolatility(..)
|
||||
, ProcsMap
|
||||
, RetType(..)
|
||||
, procReturnsScalar
|
||||
, procReturnsSingle
|
||||
, procReturnsVoid
|
||||
, procTableName
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
|
||||
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..),
|
||||
Schema, TableName)
|
||||
|
||||
import Protolude
|
||||
|
||||
data PgType
|
||||
= Scalar
|
||||
| Composite QualifiedIdentifier
|
||||
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||
|
||||
data RetType
|
||||
= Single PgType
|
||||
| SetOf PgType
|
||||
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||
|
||||
data ProcVolatility
|
||||
= Volatile
|
||||
| Stable
|
||||
| Immutable
|
||||
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||
|
||||
data ProcDescription = ProcDescription
|
||||
{ pdSchema :: Schema
|
||||
, pdName :: Text
|
||||
, pdDescription :: Maybe Text
|
||||
, pdParams :: [ProcParam]
|
||||
, pdReturnType :: Maybe RetType
|
||||
, pdVolatility :: ProcVolatility
|
||||
, pdHasVariadic :: Bool
|
||||
}
|
||||
deriving (Eq, Generic, JSON.ToJSON)
|
||||
|
||||
data ProcParam = ProcParam
|
||||
{ ppName :: Text
|
||||
, ppType :: Text
|
||||
, ppReq :: Bool
|
||||
, ppVar :: Bool
|
||||
}
|
||||
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||
|
||||
-- Order by least number of params in the case of overloaded functions
|
||||
instance Ord ProcDescription where
|
||||
ProcDescription schema1 name1 des1 prms1 rt1 vol1 hasVar1 `compare` ProcDescription schema2 name2 des2 prms2 rt2 vol2 hasVar2
|
||||
| schema1 == schema2 && name1 == name2 && length prms1 < length prms2 = LT
|
||||
| schema2 == schema2 && name1 == name2 && length prms1 > length prms2 = GT
|
||||
| otherwise = (schema1, name1, des1, prms1, rt1, vol1, hasVar1) `compare` (schema2, name2, des2, prms2, rt2, vol2, hasVar2)
|
||||
|
||||
-- | A map of all procs, all of which can be overloaded(one entry will have more than one ProcDescription).
|
||||
-- | It uses a HashMap for a faster lookup.
|
||||
type ProcsMap = HM.HashMap QualifiedIdentifier [ProcDescription]
|
||||
|
||||
procReturnsScalar :: ProcDescription -> Bool
|
||||
procReturnsScalar proc = case proc of
|
||||
ProcDescription{pdReturnType = Just (Single Scalar)} -> True
|
||||
ProcDescription{pdReturnType = Just (SetOf Scalar)} -> True
|
||||
_ -> False
|
||||
|
||||
procReturnsSingle :: ProcDescription -> Bool
|
||||
procReturnsSingle proc = case proc of
|
||||
ProcDescription{pdReturnType = Just (Single _)} -> True
|
||||
_ -> False
|
||||
|
||||
procReturnsVoid :: ProcDescription -> Bool
|
||||
procReturnsVoid proc = case proc of
|
||||
ProcDescription{pdReturnType = Nothing} -> True
|
||||
_ -> False
|
||||
|
||||
procTableName :: ProcDescription -> Maybe TableName
|
||||
procTableName proc = case pdReturnType proc of
|
||||
Just (SetOf (Composite qi)) -> Just $ qiName qi
|
||||
Just (Single (Composite qi)) -> Just $ qiName qi
|
||||
_ -> Nothing
|
||||
@@ -0,0 +1,64 @@
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
|
||||
module PostgREST.DbStructure.Relationship
|
||||
( Cardinality(..)
|
||||
, Relationship(..)
|
||||
, Junction(..)
|
||||
, RelationshipsMap
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
|
||||
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||
QualifiedIdentifier, Schema)
|
||||
|
||||
import Protolude
|
||||
|
||||
|
||||
-- | Relationship between two tables.
|
||||
data Relationship = Relationship
|
||||
{ relTable :: QualifiedIdentifier
|
||||
, relForeignTable :: QualifiedIdentifier
|
||||
, relIsSelf :: Bool -- ^ Whether is a self relationship
|
||||
, relCardinality :: Cardinality
|
||||
, relTableIsView :: Bool
|
||||
, relFTableIsView :: Bool
|
||||
}
|
||||
| ComputedRelationship
|
||||
{ relFunction :: QualifiedIdentifier
|
||||
, relTable :: QualifiedIdentifier
|
||||
, relForeignTable :: QualifiedIdentifier
|
||||
, relToOne :: Bool
|
||||
, relIsSelf :: Bool
|
||||
}
|
||||
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||
|
||||
-- | The relationship cardinality
|
||||
-- | https://en.wikipedia.org/wiki/Cardinality_(data_modeling)
|
||||
data Cardinality
|
||||
= O2M {relCons :: FKConstraint, relColumns :: [(FieldName, FieldName)]}
|
||||
-- ^ one-to-many
|
||||
| M2O {relCons :: FKConstraint, relColumns :: [(FieldName, FieldName)]}
|
||||
-- ^ many-to-one
|
||||
| O2O {relCons :: FKConstraint, relColumns :: [(FieldName, FieldName)]}
|
||||
-- ^ one-to-one, this is a refinement over M2O so operating on it is pretty much the same as M2O
|
||||
| M2M Junction
|
||||
-- ^ many-to-many
|
||||
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||
|
||||
type FKConstraint = Text
|
||||
|
||||
-- | Junction table on an M2M relationship
|
||||
data Junction = Junction
|
||||
{ junTable :: QualifiedIdentifier
|
||||
, junConstraint1 :: FKConstraint
|
||||
, junConstraint2 :: FKConstraint
|
||||
, junColumns1 :: [(FieldName, FieldName)]
|
||||
, junColumns2 :: [(FieldName, FieldName)]
|
||||
}
|
||||
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||
|
||||
-- | Key based on the source table and the foreign table schema
|
||||
type RelationshipsMap = HM.HashMap (QualifiedIdentifier, Schema) [Relationship]
|
||||
@@ -0,0 +1,49 @@
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
|
||||
module PostgREST.DbStructure.Table
|
||||
( Column(..)
|
||||
, Table(..)
|
||||
, TablesMap
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
|
||||
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||
QualifiedIdentifier (..),
|
||||
Schema, TableName)
|
||||
|
||||
import Protolude
|
||||
|
||||
|
||||
data Table = Table
|
||||
{ tableSchema :: Schema
|
||||
, tableName :: TableName
|
||||
, tableDescription :: Maybe Text
|
||||
-- TODO Find a better way to separate tables and views
|
||||
, tableIsView :: Bool
|
||||
-- The following fields identify what can be done on the table/view, they're not related to the privileges granted to it
|
||||
, tableInsertable :: Bool
|
||||
, tableUpdatable :: Bool
|
||||
, tableDeletable :: Bool
|
||||
, tablePKCols :: [FieldName]
|
||||
, tableColumns :: [Column]
|
||||
}
|
||||
deriving (Show, Ord, Generic, JSON.ToJSON)
|
||||
|
||||
instance Eq Table where
|
||||
Table{tableSchema=s1,tableName=n1} == Table{tableSchema=s2,tableName=n2} = s1 == s2 && n1 == n2
|
||||
|
||||
data Column = Column
|
||||
{ colName :: FieldName
|
||||
, colDescription :: Maybe Text
|
||||
, colNullable :: Bool
|
||||
, colType :: Text
|
||||
, colMaxLen :: Maybe Int32
|
||||
, colDefault :: Maybe Text
|
||||
, colEnum :: [Text]
|
||||
}
|
||||
deriving (Eq, Show, Ord, Generic, JSON.ToJSON)
|
||||
|
||||
type TablesMap = HM.HashMap QualifiedIdentifier Table
|
||||
+434
-186
@@ -3,36 +3,48 @@ Module : PostgREST.Error
|
||||
Description : PostgREST error HTTP responses
|
||||
-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module PostgREST.Error (
|
||||
errorResponseFor
|
||||
, ApiRequestError(..)
|
||||
, PgError(..)
|
||||
, SimpleError(..)
|
||||
, errorPayload
|
||||
, checkIsFatal
|
||||
, singularityError
|
||||
) where
|
||||
module PostgREST.Error
|
||||
( errorResponseFor
|
||||
, ApiRequestError(..)
|
||||
, PgError(..)
|
||||
, Error(..)
|
||||
, errorPayload
|
||||
, checkIsFatal
|
||||
, singularityError
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Hasql.Pool as P
|
||||
import qualified Hasql.Session as H
|
||||
import qualified Network.HTTP.Types.Status as HT
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Data.Text.Encoding.Error as T
|
||||
import qualified Hasql.Pool as SQL
|
||||
import qualified Hasql.Session as SQL
|
||||
import qualified Network.HTTP.Types.Status as HTTP
|
||||
|
||||
import Data.Aeson ((.=))
|
||||
import Data.Text (unwords)
|
||||
import Network.Wai (Response, responseLBS)
|
||||
import Text.Read (readMaybe)
|
||||
|
||||
import Network.HTTP.Types.Header
|
||||
import Network.HTTP.Types.Header (Header)
|
||||
|
||||
import PostgREST.Types
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import qualified PostgREST.MediaType as MediaType
|
||||
import PostgREST.Request.Types (ApiRequestError (..),
|
||||
QPError (..))
|
||||
|
||||
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..))
|
||||
import PostgREST.DbStructure.Proc (ProcDescription (..),
|
||||
ProcParam (..))
|
||||
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
||||
Junction (..),
|
||||
Relationship (..))
|
||||
import Protolude
|
||||
|
||||
|
||||
class (JSON.ToJSON a) => PgrstError a where
|
||||
status :: a -> HT.Status
|
||||
status :: a -> HTTP.Status
|
||||
headers :: a -> [Header]
|
||||
|
||||
errorPayload :: a -> LByteString
|
||||
@@ -41,216 +53,452 @@ class (JSON.ToJSON a) => PgrstError a where
|
||||
errorResponseFor :: a -> Response
|
||||
errorResponseFor err = responseLBS (status err) (headers err) $ errorPayload err
|
||||
|
||||
|
||||
|
||||
data ApiRequestError
|
||||
= ActionInappropriate
|
||||
| InvalidRange
|
||||
| InvalidBody ByteString
|
||||
| ParseRequestError Text Text
|
||||
| NoRelationBetween Text Text
|
||||
| InvalidFilters
|
||||
| UnknownRelation -- Unreachable?
|
||||
| UnsupportedVerb -- Unreachable?
|
||||
deriving (Show, Eq)
|
||||
|
||||
instance PgrstError ApiRequestError where
|
||||
status InvalidRange = HT.status416
|
||||
status InvalidFilters = HT.status405
|
||||
status (InvalidBody _) = HT.status400
|
||||
status UnsupportedVerb = HT.status405
|
||||
status UnknownRelation = HT.status404
|
||||
status ActionInappropriate = HT.status405
|
||||
status (ParseRequestError _ _) = HT.status400
|
||||
status (NoRelationBetween _ _) = HT.status400
|
||||
status AmbiguousRelBetween{} = HTTP.status300
|
||||
status AmbiguousRpc{} = HTTP.status300
|
||||
status MediaTypeError{} = HTTP.status415
|
||||
status InvalidBody{} = HTTP.status400
|
||||
status InvalidFilters = HTTP.status405
|
||||
status InvalidRpcMethod{} = HTTP.status405
|
||||
status InvalidRange = HTTP.status416
|
||||
status NotFound = HTTP.status404
|
||||
status NoRelBetween{} = HTTP.status400
|
||||
status NoRpc{} = HTTP.status404
|
||||
status NotEmbedded{} = HTTP.status400
|
||||
status ParseRequestError{} = HTTP.status400
|
||||
status PutRangeNotAllowedError = HTTP.status400
|
||||
status QueryParamError{} = HTTP.status400
|
||||
status UnacceptableSchema{} = HTTP.status406
|
||||
status UnsupportedMethod{} = HTTP.status405
|
||||
status LimitNoOrderError = HTTP.status400
|
||||
|
||||
headers _ = [toHeader CTApplicationJSON]
|
||||
headers _ = [MediaType.toContentType MTApplicationJSON]
|
||||
|
||||
instance JSON.ToJSON ApiRequestError where
|
||||
toJSON (ParseRequestError message details) = JSON.object [
|
||||
"message" .= message, "details" .= details]
|
||||
toJSON ActionInappropriate = JSON.object [
|
||||
"message" .= ("Bad Request" :: Text)]
|
||||
toJSON (QueryParamError (QPError message details)) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode00,
|
||||
"message" .= message,
|
||||
"details" .= details,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (InvalidRpcMethod method) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode01,
|
||||
"message" .= ("Cannot use the " <> T.decodeUtf8 method <> " method on RPC"),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (InvalidBody errorMessage) = JSON.object [
|
||||
"message" .= (toS errorMessage :: Text)]
|
||||
"code" .= ApiRequestErrorCode02,
|
||||
"message" .= T.decodeUtf8 errorMessage,
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON InvalidRange = JSON.object [
|
||||
"message" .= ("HTTP Range error" :: Text)]
|
||||
toJSON UnknownRelation = JSON.object [
|
||||
"message" .= ("Unknown relation" :: Text)]
|
||||
toJSON (NoRelationBetween parent child) = JSON.object [
|
||||
"message" .= ("Could not find foreign keys between these entities, No relation found between " <> parent <> " and " <> child :: Text)]
|
||||
toJSON UnsupportedVerb = JSON.object [
|
||||
"message" .= ("Unsupported HTTP verb" :: Text)]
|
||||
"code" .= ApiRequestErrorCode03,
|
||||
"message" .= ("HTTP Range error" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (ParseRequestError message details) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode04,
|
||||
"message" .= message,
|
||||
"details" .= details,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON InvalidFilters = JSON.object [
|
||||
"message" .= ("Filters must include all and only primary key columns with 'eq' operators" :: Text)]
|
||||
"code" .= ApiRequestErrorCode05,
|
||||
"message" .= ("Filters must include all and only primary key columns with 'eq' operators" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (UnacceptableSchema schemas) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode06,
|
||||
"message" .= ("The schema must be one of the following: " <> T.intercalate ", " schemas),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (MediaTypeError cts) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode07,
|
||||
"message" .= ("None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON NotFound = JSON.object []
|
||||
toJSON (NotEmbedded resource) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode08,
|
||||
"message" .= ("Cannot apply filter because '" <> resource <> "' is not an embedded resource in this request" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= ("Verify that '" <> resource <> "' is included in the 'select' query parameter." :: Text)]
|
||||
|
||||
toJSON LimitNoOrderError = JSON.object [
|
||||
"code" .= ApiRequestErrorCode09,
|
||||
"message" .= ("A 'limit' was applied without an explicit 'order'":: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= ("Apply an 'order' using unique column(s)" :: Text)]
|
||||
|
||||
data PgError = PgError Authenticated P.UsageError
|
||||
toJSON PutRangeNotAllowedError = JSON.object [
|
||||
"code" .= ApiRequestErrorCode14,
|
||||
"message" .= ("Range header and limit/offset querystring parameters are not allowed for PUT" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
toJSON (UnsupportedMethod method) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode17,
|
||||
"message" .= ("Unsupported HTTP method: " <> T.decodeUtf8 method),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
toJSON (NoRelBetween parent child schema) = JSON.object [
|
||||
"code" .= SchemaCacheErrorCode00,
|
||||
"message" .= ("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= ("Verify that '" <> parent <> "' and '" <> child <> "' exist in the schema '" <> schema <> "' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache." :: Text)]
|
||||
toJSON (AmbiguousRelBetween parent child rels) = JSON.object [
|
||||
"code" .= SchemaCacheErrorCode01,
|
||||
"message" .= ("Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'" :: Text),
|
||||
"details" .= (compressedRel <$> rels),
|
||||
"hint" .= ("Try changing '" <> child <> "' to one of the following: " <> relHint rels <> ". Find the desired relationship in the 'details' key." :: Text)]
|
||||
toJSON (NoRpc schema procName argumentKeys hasPreferSingleObject contentType isInvPost) =
|
||||
let prms = "(" <> T.intercalate ", " argumentKeys <> ")" in JSON.object [
|
||||
"code" .= SchemaCacheErrorCode02,
|
||||
"message" .= ("Could not find the " <> schema <> "." <> procName <>
|
||||
(case (hasPreferSingleObject, isInvPost, contentType) of
|
||||
(True, _, _) -> " function with a single json or jsonb parameter"
|
||||
(_, True, MTTextPlain) -> " function with a single unnamed text parameter"
|
||||
(_, True, MTTextXML) -> " function with a single unnamed xml parameter"
|
||||
(_, True, MTOctetStream) -> " function with a single unnamed bytea parameter"
|
||||
(_, True, MTApplicationJSON) -> prms <> " function or the " <> schema <> "." <> procName <>" function with a single unnamed json or jsonb parameter"
|
||||
_ -> prms <> " function") <>
|
||||
" in the schema cache"),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= ("If a new function was created in the database with this name and parameters, try reloading the schema cache." :: Text)]
|
||||
toJSON (AmbiguousRpc procs) = JSON.object [
|
||||
"code" .= SchemaCacheErrorCode03,
|
||||
"message" .= ("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs]),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= ("Try renaming the parameters or the function itself in the database so function overloading can be resolved" :: Text)]
|
||||
|
||||
compressedRel :: Relationship -> JSON.Value
|
||||
-- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed
|
||||
compressedRel ComputedRelationship{} = JSON.object mempty
|
||||
compressedRel Relationship{..} =
|
||||
let
|
||||
fmtEls els = "(" <> T.intercalate ", " els <> ")"
|
||||
in
|
||||
JSON.object $
|
||||
("embedding" .= (qiName relTable <> " with " <> qiName relForeignTable :: Text))
|
||||
: case relCardinality of
|
||||
M2M Junction{..} -> [
|
||||
"cardinality" .= ("many-to-many" :: Text)
|
||||
, "relationship" .= (qiName junTable <> " using " <> junConstraint1 <> fmtEls (snd <$> junColumns1) <> " and " <> junConstraint2 <> fmtEls (snd <$> junColumns2))
|
||||
]
|
||||
M2O cons relColumns -> [
|
||||
"cardinality" .= ("many-to-one" :: Text)
|
||||
, "relationship" .= (cons <> " using " <> qiName relTable <> fmtEls (fst <$> relColumns) <> " and " <> qiName relForeignTable <> fmtEls (snd <$> relColumns))
|
||||
]
|
||||
O2O cons relColumns -> [
|
||||
"cardinality" .= ("one-to-one" :: Text)
|
||||
, "relationship" .= (cons <> " using " <> qiName relTable <> fmtEls (fst <$> relColumns) <> " and " <> qiName relForeignTable <> fmtEls (snd <$> relColumns))
|
||||
]
|
||||
O2M cons relColumns -> [
|
||||
"cardinality" .= ("one-to-many" :: Text)
|
||||
, "relationship" .= (cons <> " using " <> qiName relTable <> fmtEls (fst <$> relColumns) <> " and " <> qiName relForeignTable <> fmtEls (snd <$> relColumns))
|
||||
]
|
||||
|
||||
relHint :: [Relationship] -> Text
|
||||
relHint rels = T.intercalate ", " (hintList <$> rels)
|
||||
where
|
||||
hintList Relationship{..} =
|
||||
let buildHint rel = "'" <> qiName relForeignTable <> "!" <> rel <> "'" in
|
||||
case relCardinality of
|
||||
M2M Junction{..} -> buildHint (qiName junTable)
|
||||
M2O cons _ -> buildHint cons
|
||||
O2O cons _ -> buildHint cons
|
||||
O2M cons _ -> buildHint cons
|
||||
-- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed
|
||||
hintList ComputedRelationship{} = mempty
|
||||
|
||||
data PgError = PgError Authenticated SQL.UsageError
|
||||
type Authenticated = Bool
|
||||
|
||||
instance PgrstError PgError where
|
||||
status (PgError authed usageError) = pgErrorStatus authed usageError
|
||||
|
||||
headers err =
|
||||
if status err == HT.status401
|
||||
then [toHeader CTApplicationJSON, ("WWW-Authenticate", "Bearer") :: Header]
|
||||
else [toHeader CTApplicationJSON]
|
||||
if status err == HTTP.status401
|
||||
then [MediaType.toContentType MTApplicationJSON, ("WWW-Authenticate", "Bearer") :: Header]
|
||||
else [MediaType.toContentType MTApplicationJSON]
|
||||
|
||||
instance JSON.ToJSON PgError where
|
||||
toJSON (PgError _ usageError) = JSON.toJSON usageError
|
||||
|
||||
instance JSON.ToJSON P.UsageError where
|
||||
toJSON (P.ConnectionError e) = JSON.object [
|
||||
"code" .= ("" :: Text),
|
||||
"message" .= ("Database connection error" :: Text),
|
||||
"details" .= (toS $ fromMaybe "" e :: Text)]
|
||||
toJSON (P.SessionError e) = JSON.toJSON e -- H.Error
|
||||
instance JSON.ToJSON SQL.UsageError where
|
||||
toJSON (SQL.ConnectionError e) = JSON.object [
|
||||
"code" .= ConnectionErrorCode00,
|
||||
"message" .= ("Database connection error. Retrying the connection." :: Text),
|
||||
"details" .= (T.decodeUtf8With T.lenientDecode $ fromMaybe "" e :: Text),
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (SQL.SessionError e) = JSON.toJSON e -- SQL.Error
|
||||
|
||||
instance JSON.ToJSON H.QueryError where
|
||||
toJSON (H.QueryError _ _ e) = JSON.toJSON e
|
||||
instance JSON.ToJSON SQL.QueryError where
|
||||
toJSON (SQL.QueryError _ _ e) = JSON.toJSON e
|
||||
|
||||
instance JSON.ToJSON H.CommandError where
|
||||
toJSON (H.ResultError (H.ServerError c m d h)) = case toS c of
|
||||
'P':'T':_ -> JSON.object [
|
||||
"details" .= (fmap toS d :: Maybe Text),
|
||||
"hint" .= (fmap toS h :: Maybe Text)]
|
||||
instance JSON.ToJSON SQL.CommandError where
|
||||
toJSON (SQL.ResultError (SQL.ServerError c m d h)) = JSON.object [
|
||||
"code" .= (T.decodeUtf8 c :: Text),
|
||||
"message" .= (T.decodeUtf8 m :: Text),
|
||||
"details" .= (fmap T.decodeUtf8 d :: Maybe Text),
|
||||
"hint" .= (fmap T.decodeUtf8 h :: Maybe Text)]
|
||||
|
||||
_ -> JSON.object [
|
||||
"code" .= (toS c :: Text),
|
||||
"message" .= (toS m :: Text),
|
||||
"details" .= (fmap toS d :: Maybe Text),
|
||||
"hint" .= (fmap toS h :: Maybe Text)]
|
||||
toJSON (SQL.ResultError resultError) = JSON.object [
|
||||
"code" .= InternalErrorCode00,
|
||||
"message" .= (show resultError :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
toJSON (H.ResultError (H.UnexpectedResult m)) = JSON.object [
|
||||
"message" .= (m :: Text)]
|
||||
toJSON (H.ResultError (H.RowError i H.EndOfInput)) = JSON.object [
|
||||
"message" .= ("Row error: end of input" :: Text),
|
||||
"details" .= ("Attempt to parse more columns than there are in the result" :: Text),
|
||||
"hint" .= (("Row number " <> show i) :: Text)]
|
||||
toJSON (H.ResultError (H.RowError i H.UnexpectedNull)) = JSON.object [
|
||||
"message" .= ("Row error: unexpected null" :: Text),
|
||||
"details" .= ("Attempt to parse a NULL as some value." :: Text),
|
||||
"hint" .= (("Row number " <> show i) :: Text)]
|
||||
toJSON (H.ResultError (H.RowError i (H.ValueError d))) = JSON.object [
|
||||
"message" .= ("Row error: Wrong value parser used" :: Text),
|
||||
"details" .= d,
|
||||
"hint" .= (("Row number " <> show i) :: Text)]
|
||||
toJSON (H.ResultError (H.UnexpectedAmountOfRows i)) = JSON.object [
|
||||
"message" .= ("Unexpected amount of rows" :: Text),
|
||||
"details" .= i]
|
||||
toJSON (H.ClientError d) = JSON.object [
|
||||
"message" .= ("Database client error" :: Text),
|
||||
"details" .= (fmap toS d :: Maybe Text)]
|
||||
toJSON (SQL.ClientError d) = JSON.object [
|
||||
"code" .= ConnectionErrorCode01,
|
||||
"message" .= ("Database client error. Retrying the connection." :: Text),
|
||||
"details" .= (fmap T.decodeUtf8 d :: Maybe Text),
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
pgErrorStatus :: Bool -> P.UsageError -> HT.Status
|
||||
pgErrorStatus _ (P.ConnectionError _) = HT.status503
|
||||
pgErrorStatus _ (P.SessionError (H.QueryError _ _ (H.ClientError _))) = HT.status503
|
||||
pgErrorStatus authed (P.SessionError (H.QueryError _ _ (H.ResultError rError))) =
|
||||
pgErrorStatus :: Bool -> SQL.UsageError -> HTTP.Status
|
||||
pgErrorStatus _ (SQL.ConnectionError _) = HTTP.status503
|
||||
pgErrorStatus _ (SQL.SessionError (SQL.QueryError _ _ (SQL.ClientError _))) = HTTP.status503
|
||||
pgErrorStatus authed (SQL.SessionError (SQL.QueryError _ _ (SQL.ResultError rError))) =
|
||||
case rError of
|
||||
(H.ServerError c m _ _) ->
|
||||
case toS c of
|
||||
'0':'8':_ -> HT.status503 -- pg connection err
|
||||
'0':'9':_ -> HT.status500 -- triggered action exception
|
||||
'0':'L':_ -> HT.status403 -- invalid grantor
|
||||
'0':'P':_ -> HT.status403 -- invalid role specification
|
||||
"23503" -> HT.status409 -- foreign_key_violation
|
||||
"23505" -> HT.status409 -- unique_violation
|
||||
'2':'5':_ -> HT.status500 -- invalid tx state
|
||||
'2':'8':_ -> HT.status403 -- invalid auth specification
|
||||
'2':'D':_ -> HT.status500 -- invalid tx termination
|
||||
'3':'8':_ -> HT.status500 -- external routine exception
|
||||
'3':'9':_ -> HT.status500 -- external routine invocation
|
||||
'3':'B':_ -> HT.status500 -- savepoint exception
|
||||
'4':'0':_ -> HT.status500 -- tx rollback
|
||||
'5':'3':_ -> HT.status503 -- insufficient resources
|
||||
'5':'4':_ -> HT.status413 -- too complex
|
||||
'5':'5':_ -> HT.status500 -- obj not on prereq state
|
||||
'5':'7':_ -> HT.status500 -- operator intervention
|
||||
'5':'8':_ -> HT.status500 -- system error
|
||||
'F':'0':_ -> HT.status500 -- conf file error
|
||||
'H':'V':_ -> HT.status500 -- foreign data wrapper error
|
||||
"P0001" -> HT.status400 -- default code for "raise"
|
||||
'P':'0':_ -> HT.status500 -- PL/pgSQL Error
|
||||
'X':'X':_ -> HT.status500 -- internal Error
|
||||
"42883" -> HT.status404 -- undefined function
|
||||
"42P01" -> HT.status404 -- undefined table
|
||||
"42501" -> if authed then HT.status403 else HT.status401 -- insufficient privilege
|
||||
'P':'T':n -> fromMaybe HT.status500 (HT.mkStatus <$> readMaybe n <*> pure m)
|
||||
_ -> HT.status400
|
||||
(SQL.ServerError c m _ _) ->
|
||||
case BS.unpack c of
|
||||
'0':'8':_ -> HTTP.status503 -- pg connection err
|
||||
'0':'9':_ -> HTTP.status500 -- triggered action exception
|
||||
'0':'L':_ -> HTTP.status403 -- invalid grantor
|
||||
'0':'P':_ -> HTTP.status403 -- invalid role specification
|
||||
"23503" -> HTTP.status409 -- foreign_key_violation
|
||||
"23505" -> HTTP.status409 -- unique_violation
|
||||
"25006" -> HTTP.status405 -- read_only_sql_transaction
|
||||
'2':'5':_ -> HTTP.status500 -- invalid tx state
|
||||
'2':'8':_ -> HTTP.status403 -- invalid auth specification
|
||||
'2':'D':_ -> HTTP.status500 -- invalid tx termination
|
||||
'3':'8':_ -> HTTP.status500 -- external routine exception
|
||||
'3':'9':_ -> HTTP.status500 -- external routine invocation
|
||||
'3':'B':_ -> HTTP.status500 -- savepoint exception
|
||||
'4':'0':_ -> HTTP.status500 -- tx rollback
|
||||
'5':'3':_ -> HTTP.status503 -- insufficient resources
|
||||
'5':'4':_ -> HTTP.status413 -- too complex
|
||||
'5':'5':_ -> HTTP.status500 -- obj not on prereq state
|
||||
'5':'7':_ -> HTTP.status500 -- operator intervention
|
||||
'5':'8':_ -> HTTP.status500 -- system error
|
||||
'F':'0':_ -> HTTP.status500 -- conf file error
|
||||
'H':'V':_ -> HTTP.status500 -- foreign data wrapper error
|
||||
"P0001" -> HTTP.status400 -- default code for "raise"
|
||||
'P':'0':_ -> HTTP.status500 -- PL/pgSQL Error
|
||||
'X':'X':_ -> HTTP.status500 -- internal Error
|
||||
"42883"-> if BS.isPrefixOf "function xmlagg(" m
|
||||
then HTTP.status406
|
||||
else HTTP.status404 -- undefined function
|
||||
"42P01" -> HTTP.status404 -- undefined table
|
||||
"42501" -> if authed then HTTP.status403 else HTTP.status401 -- insufficient privilege
|
||||
'P':'T':n -> fromMaybe HTTP.status500 (HTTP.mkStatus <$> readMaybe n <*> pure m)
|
||||
_ -> HTTP.status400
|
||||
|
||||
_ -> HT.status500
|
||||
_ -> HTTP.status500
|
||||
|
||||
checkIsFatal :: PgError -> Maybe Text
|
||||
checkIsFatal (PgError _ (P.ConnectionError e))
|
||||
checkIsFatal (PgError _ (SQL.ConnectionError e))
|
||||
| isAuthFailureMessage = Just $ toS failureMessage
|
||||
| otherwise = Nothing
|
||||
where isAuthFailureMessage = "FATAL: password authentication failed" `isPrefixOf` toS failureMessage
|
||||
failureMessage = fromMaybe "" e
|
||||
where isAuthFailureMessage = "FATAL: password authentication failed" `isPrefixOf` failureMessage
|
||||
failureMessage = BS.unpack $ fromMaybe mempty e
|
||||
checkIsFatal (PgError _ (SQL.SessionError (SQL.QueryError _ _ (SQL.ResultError serverError))))
|
||||
= case serverError of
|
||||
-- Check for a syntax error (42601 is the pg code). This would mean the error is on our part somehow, so we treat it as fatal.
|
||||
SQL.ServerError "42601" _ _ _
|
||||
-> Just "Hint: This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues"
|
||||
-- Check for a "prepared statement <name> already exists" error (Code 42P05: duplicate_prepared_statement).
|
||||
-- This would mean that a connection pooler in transaction mode is being used
|
||||
-- while prepared statements are enabled in the PostgREST configuration,
|
||||
-- both of which are incompatible with each other.
|
||||
SQL.ServerError "42P05" _ _ _
|
||||
-> Just "Hint: If you are using connection poolers in transaction mode, try setting db-prepared-statements to false."
|
||||
-- Check for a "transaction blocks not allowed in statement pooling mode" error (Code 08P01: protocol_violation).
|
||||
-- This would mean that a connection pooler in statement mode is being used which is not supported in PostgREST.
|
||||
SQL.ServerError "08P01" "transaction blocks not allowed in statement pooling mode" _ _
|
||||
-> Just "Hint: Connection poolers in statement mode are not supported."
|
||||
_ -> Nothing
|
||||
checkIsFatal _ = Nothing
|
||||
|
||||
|
||||
data SimpleError
|
||||
= GucHeadersError
|
||||
| BinaryFieldError ContentType
|
||||
| ConnectionLostError
|
||||
| PutSingletonError
|
||||
| PutMatchingPkError
|
||||
| PutRangeNotAllowedError
|
||||
| PutPayloadIncompleteError
|
||||
| JwtTokenMissing
|
||||
data Error
|
||||
= ApiRequestError ApiRequestError
|
||||
| BinaryFieldError MediaType
|
||||
| GucHeadersError
|
||||
| GucStatusError
|
||||
| JwtTokenInvalid Text
|
||||
| JwtTokenMissing
|
||||
| JwtTokenRequired
|
||||
| NoSchemaCacheError
|
||||
| OffLimitsChangesError Int64 Integer
|
||||
| PgErr PgError
|
||||
| PutMatchingPkError
|
||||
| SingularityError Integer
|
||||
| ContentTypeError [ByteString]
|
||||
deriving (Show, Eq)
|
||||
|
||||
instance PgrstError SimpleError where
|
||||
status GucHeadersError = HT.status500
|
||||
status (BinaryFieldError _) = HT.status406
|
||||
status ConnectionLostError = HT.status503
|
||||
status PutSingletonError = HT.status400
|
||||
status PutMatchingPkError = HT.status400
|
||||
status PutRangeNotAllowedError = HT.status400
|
||||
status PutPayloadIncompleteError = HT.status400
|
||||
status JwtTokenMissing = HT.status500
|
||||
status (JwtTokenInvalid _) = HT.unauthorized401
|
||||
status (SingularityError _) = HT.status406
|
||||
status (ContentTypeError _) = HT.status415
|
||||
instance PgrstError Error where
|
||||
status (ApiRequestError err) = status err
|
||||
status BinaryFieldError{} = HTTP.status406
|
||||
status GucHeadersError = HTTP.status500
|
||||
status GucStatusError = HTTP.status500
|
||||
status JwtTokenInvalid{} = HTTP.unauthorized401
|
||||
status JwtTokenMissing = HTTP.status500
|
||||
status JwtTokenRequired = HTTP.unauthorized401
|
||||
status NoSchemaCacheError = HTTP.status503
|
||||
status OffLimitsChangesError{} = HTTP.status400
|
||||
status (PgErr err) = status err
|
||||
status PutMatchingPkError = HTTP.status400
|
||||
status SingularityError{} = HTTP.status406
|
||||
|
||||
headers (SingularityError _) = [toHeader CTSingularJSON]
|
||||
headers (JwtTokenInvalid m) = [toHeader CTApplicationJSON, invalidTokenHeader m]
|
||||
headers _ = [toHeader CTApplicationJSON]
|
||||
headers (ApiRequestError err) = headers err
|
||||
headers (JwtTokenInvalid m) = [MediaType.toContentType MTApplicationJSON, invalidTokenHeader m]
|
||||
headers JwtTokenRequired = [MediaType.toContentType MTApplicationJSON, requiredTokenHeader]
|
||||
headers (PgErr err) = headers err
|
||||
headers SingularityError{} = [MediaType.toContentType MTSingularJSON]
|
||||
headers _ = [MediaType.toContentType MTApplicationJSON]
|
||||
|
||||
instance JSON.ToJSON SimpleError where
|
||||
toJSON GucHeadersError = JSON.object [
|
||||
"message" .= ("response.headers guc must be a JSON array composed of objects with a single key and a string value" :: Text)]
|
||||
toJSON (BinaryFieldError ct) = JSON.object [
|
||||
"message" .= ((toS (toMime ct) <> " requested but a single column was not selected") :: Text)]
|
||||
toJSON ConnectionLostError = JSON.object [
|
||||
"message" .= ("Database connection lost, retrying the connection." :: Text)]
|
||||
instance JSON.ToJSON Error where
|
||||
toJSON NoSchemaCacheError = JSON.object [
|
||||
"code" .= ConnectionErrorCode02,
|
||||
"message" .= ("Could not query the database for the schema cache. Retrying." :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
toJSON PutSingletonError = JSON.object [
|
||||
"message" .= ("PUT payload must contain a single row" :: Text)]
|
||||
toJSON PutRangeNotAllowedError = JSON.object [
|
||||
"message" .= ("Range header and limit/offset querystring parameters are not allowed for PUT" :: Text)]
|
||||
toJSON PutPayloadIncompleteError = JSON.object [
|
||||
"message" .= ("You must specify all columns in the payload when using PUT" :: Text)]
|
||||
toJSON PutMatchingPkError = JSON.object [
|
||||
"message" .= ("Payload values do not match URL in primary key column(s)" :: Text)]
|
||||
|
||||
toJSON (ContentTypeError cts) = JSON.object [
|
||||
"message" .= ("None of these Content-Types are available: " <> (toS . intercalate ", " . map toS) cts :: Text)]
|
||||
toJSON (SingularityError n) = JSON.object [
|
||||
"message" .= ("JSON object requested, multiple (or no) rows returned" :: Text),
|
||||
"details" .= unwords ["Results contain", show n, "rows,", toS (toMime CTSingularJSON), "requires 1 row"]]
|
||||
|
||||
toJSON JwtTokenMissing = JSON.object [
|
||||
"message" .= ("Server lacks JWT secret" :: Text)]
|
||||
toJSON JwtTokenMissing = JSON.object [
|
||||
"code" .= JWTErrorCode00,
|
||||
"message" .= ("Server lacks JWT secret" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (JwtTokenInvalid message) = JSON.object [
|
||||
"message" .= (message :: Text)]
|
||||
"code" .= JWTErrorCode01,
|
||||
"message" .= (message :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON JwtTokenRequired = JSON.object [
|
||||
"code" .= JWTErrorCode02,
|
||||
"message" .= ("Anonymous access is disabled" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
toJSON (OffLimitsChangesError n maxs) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode10,
|
||||
"message" .= ("The maximum number of rows allowed to change was surpassed" :: Text),
|
||||
"details" .= T.unwords ["Results contain", show n, "rows changed but the maximum number allowed is", show maxs],
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
toJSON GucHeadersError = JSON.object [
|
||||
"code" .= ApiRequestErrorCode11,
|
||||
"message" .= ("response.headers guc must be a JSON array composed of objects with a single key and a string value" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON GucStatusError = JSON.object [
|
||||
"code" .= ApiRequestErrorCode12,
|
||||
"message" .= ("response.status guc must be a valid status code" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (BinaryFieldError ct) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode13,
|
||||
"message" .= ((T.decodeUtf8 (MediaType.toMime ct) <> " requested but more than one column was selected") :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
toJSON PutMatchingPkError = JSON.object [
|
||||
"code" .= ApiRequestErrorCode15,
|
||||
"message" .= ("Payload values do not match URL in primary key column(s)" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
toJSON (SingularityError n) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode16,
|
||||
"message" .= ("JSON object requested, multiple (or no) rows returned" :: Text),
|
||||
"details" .= T.unwords ["Results contain", show n, "rows,", T.decodeUtf8 (MediaType.toMime MTSingularJSON), "requires 1 row"],
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
toJSON (PgErr err) = JSON.toJSON err
|
||||
toJSON (ApiRequestError err) = JSON.toJSON err
|
||||
|
||||
invalidTokenHeader :: Text -> Header
|
||||
invalidTokenHeader m =
|
||||
("WWW-Authenticate", "Bearer error=\"invalid_token\", " <> "error_description=" <> show m)
|
||||
("WWW-Authenticate", "Bearer error=\"invalid_token\", " <> "error_description=" <> encodeUtf8 (show m))
|
||||
|
||||
singularityError :: (Integral a) => a -> SimpleError
|
||||
requiredTokenHeader :: Header
|
||||
requiredTokenHeader = ("WWW-Authenticate", "Bearer")
|
||||
|
||||
singularityError :: (Integral a) => a -> Error
|
||||
singularityError = SingularityError . toInteger
|
||||
|
||||
-- Error codes are grouped by common modules or characteristics
|
||||
data ErrorCode
|
||||
-- PostgreSQL connection errors
|
||||
= ConnectionErrorCode00
|
||||
| ConnectionErrorCode01
|
||||
| ConnectionErrorCode02
|
||||
-- API Request errors
|
||||
| ApiRequestErrorCode00
|
||||
| ApiRequestErrorCode01
|
||||
| ApiRequestErrorCode02
|
||||
| ApiRequestErrorCode03
|
||||
| ApiRequestErrorCode04
|
||||
| ApiRequestErrorCode05
|
||||
| ApiRequestErrorCode06
|
||||
| ApiRequestErrorCode07
|
||||
| ApiRequestErrorCode08
|
||||
| ApiRequestErrorCode09
|
||||
| ApiRequestErrorCode10
|
||||
| ApiRequestErrorCode11
|
||||
| ApiRequestErrorCode12
|
||||
| ApiRequestErrorCode13
|
||||
| ApiRequestErrorCode14
|
||||
| ApiRequestErrorCode15
|
||||
| ApiRequestErrorCode16
|
||||
| ApiRequestErrorCode17
|
||||
-- Schema Cache errors
|
||||
| SchemaCacheErrorCode00
|
||||
| SchemaCacheErrorCode01
|
||||
| SchemaCacheErrorCode02
|
||||
| SchemaCacheErrorCode03
|
||||
-- JWT authentication errors
|
||||
| JWTErrorCode00
|
||||
| JWTErrorCode01
|
||||
| JWTErrorCode02
|
||||
-- Internal errors related to the Hasql library
|
||||
| InternalErrorCode00
|
||||
|
||||
instance JSON.ToJSON ErrorCode where
|
||||
toJSON e = JSON.toJSON (buildErrorCode e)
|
||||
|
||||
-- New group of errors will be added at the end of all the groups and will have the next prefix in the sequence
|
||||
-- New errors are added at the end of the group they belong to and will have the next code in the sequence
|
||||
buildErrorCode :: ErrorCode -> Text
|
||||
buildErrorCode code = "PGRST" <> case code of
|
||||
ConnectionErrorCode00 -> "000"
|
||||
ConnectionErrorCode01 -> "001"
|
||||
ConnectionErrorCode02 -> "002"
|
||||
|
||||
ApiRequestErrorCode00 -> "100"
|
||||
ApiRequestErrorCode01 -> "101"
|
||||
ApiRequestErrorCode02 -> "102"
|
||||
ApiRequestErrorCode03 -> "103"
|
||||
ApiRequestErrorCode04 -> "104"
|
||||
ApiRequestErrorCode05 -> "105"
|
||||
ApiRequestErrorCode06 -> "106"
|
||||
ApiRequestErrorCode07 -> "107"
|
||||
ApiRequestErrorCode08 -> "108"
|
||||
ApiRequestErrorCode09 -> "109"
|
||||
ApiRequestErrorCode10 -> "110"
|
||||
ApiRequestErrorCode11 -> "111"
|
||||
ApiRequestErrorCode12 -> "112"
|
||||
ApiRequestErrorCode13 -> "113"
|
||||
ApiRequestErrorCode14 -> "114"
|
||||
ApiRequestErrorCode15 -> "115"
|
||||
ApiRequestErrorCode16 -> "116"
|
||||
ApiRequestErrorCode17 -> "117"
|
||||
|
||||
SchemaCacheErrorCode00 -> "200"
|
||||
SchemaCacheErrorCode01 -> "201"
|
||||
SchemaCacheErrorCode02 -> "202"
|
||||
SchemaCacheErrorCode03 -> "203"
|
||||
|
||||
JWTErrorCode00 -> "300"
|
||||
JWTErrorCode01 -> "301"
|
||||
JWTErrorCode02 -> "302"
|
||||
|
||||
InternalErrorCode00 -> "X00"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
module PostgREST.GucHeader
|
||||
( GucHeader
|
||||
, unwrapGucHeader
|
||||
, addHeadersIfNotIncluded
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.Aeson.Key as K
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
|
||||
import Network.HTTP.Types.Header (Header)
|
||||
|
||||
import Protolude
|
||||
|
||||
|
||||
{-|
|
||||
Custom guc header, it's obtained by parsing the json in a:
|
||||
`SET LOCAL "response.headers" = '[{"Set-Cookie": ".."}]'
|
||||
-}
|
||||
newtype GucHeader = GucHeader (CI.CI ByteString, ByteString)
|
||||
|
||||
instance JSON.FromJSON GucHeader where
|
||||
parseJSON (JSON.Object o) =
|
||||
case KM.toList o of
|
||||
[(k, JSON.String s)] -> pure $ GucHeader (CI.mk $ toUtf8 $ K.toText k, toUtf8 s)
|
||||
_ -> mzero
|
||||
parseJSON _ = mzero
|
||||
|
||||
unwrapGucHeader :: GucHeader -> Header
|
||||
unwrapGucHeader (GucHeader (k, v)) = (k, v)
|
||||
|
||||
-- | Add headers not already included to allow the user to override them instead of duplicating them
|
||||
addHeadersIfNotIncluded :: [Header] -> [Header] -> [Header]
|
||||
addHeadersIfNotIncluded newHeaders initialHeaders =
|
||||
filter (\(nk, _) -> isNothing $ find (\(ik, _) -> ik == nk) initialHeaders) newHeaders ++
|
||||
initialHeaders
|
||||
@@ -0,0 +1,30 @@
|
||||
{-|
|
||||
Module : PostgREST.Logger
|
||||
Description : Wai Middleware to log requests to stdout.
|
||||
-}
|
||||
module PostgREST.Logger (middleware) where
|
||||
|
||||
import qualified Network.Wai as Wai
|
||||
import qualified Network.Wai.Middleware.RequestLogger as Wai
|
||||
|
||||
import Network.HTTP.Types.Status (status400, status500)
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
|
||||
import qualified PostgREST.Auth as Auth
|
||||
import PostgREST.Config (LogLevel (..))
|
||||
|
||||
import Protolude
|
||||
|
||||
middleware :: LogLevel -> Wai.Middleware
|
||||
middleware logLevel = case logLevel of
|
||||
LogInfo -> requestLogger (const True)
|
||||
LogWarn -> requestLogger (>= status400)
|
||||
LogError -> requestLogger (>= status500)
|
||||
LogCrit -> requestLogger (const False)
|
||||
where
|
||||
requestLogger filterStatus = unsafePerformIO $ Wai.mkRequestLogger Wai.defaultRequestLoggerSettings
|
||||
{ Wai.outputFormat = Wai.ApacheWithSettings $
|
||||
Wai.defaultApacheSettings
|
||||
& Wai.setApacheRequestFilter (\_ res -> filterStatus $ Wai.responseStatus res)
|
||||
& Wai.setApacheUserGetter (fmap encodeUtf8 . Auth.getRole)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
|
||||
module PostgREST.MediaType
|
||||
( MediaType(..)
|
||||
, MTPlanOption (..)
|
||||
, MTPlanFormat (..)
|
||||
, MTPlanAttrs(..)
|
||||
, toContentType
|
||||
, toMime
|
||||
, decodeMediaType
|
||||
, getMediaType
|
||||
) where
|
||||
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Internal as BS (c2w)
|
||||
import Data.Maybe (fromJust)
|
||||
|
||||
import Network.HTTP.Types.Header (Header, hContentType)
|
||||
|
||||
import Protolude
|
||||
|
||||
-- | Enumeration of currently supported media types
|
||||
data MediaType
|
||||
= MTApplicationJSON
|
||||
| MTSingularJSON
|
||||
| MTGeoJSON
|
||||
| MTTextCSV
|
||||
| MTTextPlain
|
||||
| MTTextXML
|
||||
| MTOpenAPI
|
||||
| MTUrlEncoded
|
||||
| MTOctetStream
|
||||
| MTAny
|
||||
| MTOther ByteString
|
||||
| MTPlan MTPlanAttrs
|
||||
deriving Eq
|
||||
|
||||
data MTPlanAttrs = MTPlanAttrs (Maybe MediaType) MTPlanFormat [MTPlanOption]
|
||||
instance Eq MTPlanAttrs where
|
||||
MTPlanAttrs {} == MTPlanAttrs {} = True -- we don't care about the attributes when comparing two MTPlan media types
|
||||
|
||||
data MTPlanOption
|
||||
= PlanAnalyze | PlanVerbose | PlanSettings | PlanBuffers | PlanWAL
|
||||
|
||||
data MTPlanFormat
|
||||
= PlanJSON | PlanText
|
||||
|
||||
-- | Convert MediaType to a Content-Type HTTP Header
|
||||
toContentType :: MediaType -> Header
|
||||
toContentType ct = (hContentType, toMime ct <> charset)
|
||||
where
|
||||
charset = case ct of
|
||||
MTOctetStream -> mempty
|
||||
MTOther _ -> mempty
|
||||
_ -> "; charset=utf-8"
|
||||
|
||||
-- | Convert from MediaType to a ByteString representing the mime type
|
||||
toMime :: MediaType -> ByteString
|
||||
toMime MTApplicationJSON = "application/json"
|
||||
toMime MTGeoJSON = "application/geo+json"
|
||||
toMime MTTextCSV = "text/csv"
|
||||
toMime MTTextPlain = "text/plain"
|
||||
toMime MTTextXML = "text/xml"
|
||||
toMime MTOpenAPI = "application/openapi+json"
|
||||
toMime MTSingularJSON = "application/vnd.pgrst.object+json"
|
||||
toMime MTUrlEncoded = "application/x-www-form-urlencoded"
|
||||
toMime MTOctetStream = "application/octet-stream"
|
||||
toMime MTAny = "*/*"
|
||||
toMime (MTOther ct) = ct
|
||||
toMime (MTPlan (MTPlanAttrs mt fmt opts)) =
|
||||
"application/vnd.pgrst.plan+" <> toMimePlanFormat fmt <>
|
||||
(if isNothing mt then mempty else "; for=\"" <> toMime (fromJust mt) <> "\"") <>
|
||||
(if null opts then mempty else "; options=" <> BS.intercalate "|" (toMimePlanOption <$> opts))
|
||||
|
||||
toMimePlanOption :: MTPlanOption -> ByteString
|
||||
toMimePlanOption PlanAnalyze = "analyze"
|
||||
toMimePlanOption PlanVerbose = "verbose"
|
||||
toMimePlanOption PlanSettings = "settings"
|
||||
toMimePlanOption PlanBuffers = "buffers"
|
||||
toMimePlanOption PlanWAL = "wal"
|
||||
|
||||
toMimePlanFormat :: MTPlanFormat -> ByteString
|
||||
toMimePlanFormat PlanJSON = "json"
|
||||
toMimePlanFormat PlanText = "text"
|
||||
|
||||
-- | Convert from ByteString to MediaType. Warning: discards MIME parameters
|
||||
decodeMediaType :: BS.ByteString -> MediaType
|
||||
decodeMediaType mt =
|
||||
case BS.split (BS.c2w ';') mt of
|
||||
"application/json":_ -> MTApplicationJSON
|
||||
"application/geo+json":_ -> MTGeoJSON
|
||||
"text/csv":_ -> MTTextCSV
|
||||
"text/plain":_ -> MTTextPlain
|
||||
"text/xml":_ -> MTTextXML
|
||||
"application/openapi+json":_ -> MTOpenAPI
|
||||
"application/vnd.pgrst.object+json":_ -> MTSingularJSON
|
||||
"application/vnd.pgrst.object":_ -> MTSingularJSON
|
||||
"application/x-www-form-urlencoded":_ -> MTUrlEncoded
|
||||
"application/octet-stream":_ -> MTOctetStream
|
||||
"application/vnd.pgrst.plan":rest -> getPlan PlanText rest
|
||||
"application/vnd.pgrst.plan+text":rest -> getPlan PlanText rest
|
||||
"application/vnd.pgrst.plan+json":rest -> getPlan PlanJSON rest
|
||||
"*/*":_ -> MTAny
|
||||
other:_ -> MTOther other
|
||||
_ -> MTAny
|
||||
where
|
||||
getPlan fmt rest =
|
||||
let
|
||||
opts = BS.split (BS.c2w '|') $ fromMaybe mempty (BS.stripPrefix "options=" =<< find (BS.isPrefixOf "options=") rest)
|
||||
inOpts str = str `elem` opts
|
||||
mtFor = decodeMediaType . dropAround (== BS.c2w '"') <$> (BS.stripPrefix "for=" =<< find (BS.isPrefixOf "for=") rest)
|
||||
dropAround p = BS.dropWhile p . BS.dropWhileEnd p in
|
||||
MTPlan $ MTPlanAttrs mtFor fmt $
|
||||
[PlanAnalyze | inOpts "analyze" ] ++
|
||||
[PlanVerbose | inOpts "verbose" ] ++
|
||||
[PlanSettings | inOpts "settings"] ++
|
||||
[PlanBuffers | inOpts "buffers" ] ++
|
||||
[PlanWAL | inOpts "wal" ]
|
||||
|
||||
getMediaType :: MediaType -> MediaType
|
||||
getMediaType mt = case mt of
|
||||
MTPlan (MTPlanAttrs (Just mType) _ _) -> mType
|
||||
MTPlan (MTPlanAttrs Nothing _ _) -> MTApplicationJSON
|
||||
other -> other
|
||||
+111
-49
@@ -1,60 +1,122 @@
|
||||
{-|
|
||||
Module : PostgREST.Middleware
|
||||
Description : Sets the PostgreSQL GUCs, role, search_path and pre-request function. Validates JWT.
|
||||
Description : Sets CORS policy. Also the PostgreSQL GUCs, role, search_path and pre-request function.
|
||||
-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module PostgREST.Middleware
|
||||
( runPgLocals
|
||||
, optionalRollback
|
||||
) where
|
||||
|
||||
module PostgREST.Middleware where
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.Aeson.Key as K
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Hasql.Decoders as HD
|
||||
import qualified Hasql.DynamicStatements.Snippet as SQL hiding (sql)
|
||||
import qualified Hasql.DynamicStatements.Statement as SQL
|
||||
import qualified Hasql.Transaction as SQL
|
||||
import qualified Network.Wai as Wai
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import qualified Hasql.Transaction as H
|
||||
|
||||
import Network.Wai (Application, Response)
|
||||
import Network.Wai.Middleware.Cors (cors)
|
||||
import Network.Wai.Middleware.Gzip (def, gzip)
|
||||
import Network.Wai.Middleware.Static (only, staticPolicy)
|
||||
import Control.Arrow ((***))
|
||||
|
||||
import Crypto.JWT
|
||||
import Data.Scientific (FPFormat (..), formatScientific, isInteger)
|
||||
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.Config.PgVersion (PgVersion (..), pgVersion140)
|
||||
import PostgREST.Error (Error, errorResponseFor)
|
||||
import PostgREST.GucHeader (addHeadersIfNotIncluded)
|
||||
import PostgREST.Query.SqlFragment (fromQi, intercalateSnippet,
|
||||
pgFmtIdentList, unknownEncoder)
|
||||
import PostgREST.Request.ApiRequest (ApiRequest (..), Target (..))
|
||||
|
||||
import PostgREST.Request.Preferences
|
||||
|
||||
import PostgREST.ApiRequest (ApiRequest (..))
|
||||
import PostgREST.Auth (JWTAttempt (..))
|
||||
import PostgREST.Config (AppConfig (..), corsPolicy)
|
||||
import PostgREST.Error (SimpleError (JwtTokenInvalid, JwtTokenMissing),
|
||||
errorResponseFor)
|
||||
import PostgREST.QueryBuilder (pgFmtSetLocal, pgFmtSetLocalSearchPath,
|
||||
unquoted)
|
||||
import Protolude
|
||||
|
||||
runWithClaims :: AppConfig -> JWTAttempt ->
|
||||
(ApiRequest -> H.Transaction Response) ->
|
||||
ApiRequest -> H.Transaction Response
|
||||
runWithClaims conf eClaims app req =
|
||||
case eClaims of
|
||||
JWTMissingSecret -> return . errorResponseFor $ JwtTokenMissing
|
||||
JWTInvalid JWTExpired -> return . errorResponseFor . JwtTokenInvalid $ "JWT expired"
|
||||
JWTInvalid e -> return . errorResponseFor . JwtTokenInvalid . show $ e
|
||||
JWTClaims claims -> do
|
||||
H.sql $ toS . mconcat $ setSearchPathSql : setRoleSql ++ claimsSql ++ headersSql ++ cookiesSql ++ appSettingsSql
|
||||
mapM_ H.sql customReqCheck
|
||||
app req
|
||||
where
|
||||
headersSql = pgFmtSetLocal "request.header." <$> iHeaders req
|
||||
cookiesSql = pgFmtSetLocal "request.cookie." <$> iCookies req
|
||||
claimsSql = pgFmtSetLocal "request.jwt.claim." <$> [(c,unquoted v) | (c,v) <- M.toList claimsWithRole]
|
||||
appSettingsSql = pgFmtSetLocal mempty <$> configSettings conf
|
||||
setRoleSql = maybeToList $ (\x ->
|
||||
pgFmtSetLocal mempty ("role", unquoted x)) <$> M.lookup "role" claimsWithRole
|
||||
setSearchPathSql = pgFmtSetLocalSearchPath $ configSchema conf : configExtraSearchPath conf
|
||||
-- role claim defaults to anon if not specified in jwt
|
||||
claimsWithRole = M.union claims (M.singleton "role" anon)
|
||||
anon = JSON.String . toS $ configAnonRole conf
|
||||
customReqCheck = (\f -> "select " <> toS f <> "();") <$> configReqCheck conf
|
||||
-- | Runs local(transaction scoped) GUCs for every request, plus the pre-request function
|
||||
runPgLocals :: AppConfig -> KM.KeyMap JSON.Value -> Text ->
|
||||
(ApiRequest -> ExceptT Error SQL.Transaction Wai.Response) ->
|
||||
ApiRequest -> ByteString -> PgVersion -> ExceptT Error SQL.Transaction Wai.Response
|
||||
runPgLocals conf claims role app req jsonDbS actualPgVersion = do
|
||||
lift $ SQL.statement mempty $ SQL.dynamicallyParameterized
|
||||
("select " <> intercalateSnippet ", " (searchPathSql : roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql ++ specSql))
|
||||
HD.noResult (configDbPreparedStatements conf)
|
||||
lift $ traverse_ SQL.sql preReqSql
|
||||
app req
|
||||
where
|
||||
methodSql = setConfigLocal mempty ("request.method", iMethod req)
|
||||
pathSql = setConfigLocal mempty ("request.path", iPath req)
|
||||
headersSql = if usesLegacyGucs
|
||||
then setConfigLocal "request.header." <$> iHeaders req
|
||||
else setConfigLocalJson "request.headers" (iHeaders req)
|
||||
cookiesSql = if usesLegacyGucs
|
||||
then setConfigLocal "request.cookie." <$> iCookies req
|
||||
else setConfigLocalJson "request.cookies" (iCookies req)
|
||||
claimsSql = if usesLegacyGucs
|
||||
then setConfigLocal "request.jwt.claim." <$> [(toUtf8 $ K.toText c, toUtf8 $ unquoted v) | (c,v) <- KM.toList claims]
|
||||
else [setConfigLocal mempty ("request.jwt.claims", LBS.toStrict $ JSON.encode claims)]
|
||||
roleSql = [setConfigLocal mempty ("role", toUtf8 role)]
|
||||
appSettingsSql = setConfigLocal mempty <$> (join bimap toUtf8 <$> configAppSettings conf)
|
||||
searchPathSql =
|
||||
let schemas = pgFmtIdentList (iSchema req : configDbExtraSearchPath conf) in
|
||||
setConfigLocal mempty ("search_path", schemas)
|
||||
preReqSql = (\f -> "select " <> fromQi f <> "();") <$> configDbPreRequest conf
|
||||
specSql = case iTarget req of
|
||||
TargetProc{tpIsRootSpec=True} -> [setConfigLocal mempty ("request.spec", jsonDbS)]
|
||||
_ -> mempty
|
||||
usesLegacyGucs = configDbUseLegacyGucs conf && actualPgVersion < pgVersion140
|
||||
|
||||
defaultMiddle :: Application -> Application
|
||||
defaultMiddle =
|
||||
gzip def
|
||||
. cors corsPolicy
|
||||
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
|
||||
unquoted :: JSON.Value -> Text
|
||||
unquoted (JSON.String t) = t
|
||||
unquoted (JSON.Number n) =
|
||||
toS $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n
|
||||
unquoted (JSON.Bool b) = show b
|
||||
unquoted v = T.decodeUtf8 . LBS.toStrict $ JSON.encode v
|
||||
|
||||
-- | Set a transaction to eventually roll back if requested and set respective
|
||||
-- headers on the response.
|
||||
optionalRollback
|
||||
:: AppConfig
|
||||
-> ApiRequest
|
||||
-> ExceptT Error SQL.Transaction Wai.Response
|
||||
-> ExceptT Error SQL.Transaction Wai.Response
|
||||
optionalRollback AppConfig{..} ApiRequest{..} transaction = do
|
||||
resp <- catchError transaction $ return . errorResponseFor
|
||||
when (shouldRollback || (configDbTxRollbackAll && not shouldCommit)) $ lift do
|
||||
SQL.sql "SET CONSTRAINTS ALL IMMEDIATE"
|
||||
SQL.condemn
|
||||
return $ Wai.mapResponseHeaders preferenceApplied resp
|
||||
where
|
||||
shouldCommit =
|
||||
configDbTxAllowOverride && iPreferTransaction == Just Commit
|
||||
shouldRollback =
|
||||
configDbTxAllowOverride && iPreferTransaction == Just Rollback
|
||||
preferenceApplied
|
||||
| shouldCommit =
|
||||
addHeadersIfNotIncluded
|
||||
[toAppliedHeader Commit]
|
||||
| shouldRollback =
|
||||
addHeadersIfNotIncluded
|
||||
[toAppliedHeader Rollback]
|
||||
| otherwise =
|
||||
identity
|
||||
|
||||
-- | Do a pg set_config(setting, value, true) call. This is equivalent to a SET LOCAL.
|
||||
setConfigLocal :: ByteString -> (ByteString, ByteString) -> SQL.Snippet
|
||||
setConfigLocal prefix (k, v) =
|
||||
"set_config(" <> unknownEncoder (prefix <> k) <> ", " <> unknownEncoder v <> ", true)"
|
||||
|
||||
-- | Starting from PostgreSQL v14, some characters are not allowed for config names (mostly affecting headers with "-").
|
||||
-- | A JSON format string is used to avoid this problem. See https://github.com/PostgREST/postgrest/issues/1857
|
||||
setConfigLocalJson :: ByteString -> [(ByteString, ByteString)] -> [SQL.Snippet]
|
||||
setConfigLocalJson prefix keyVals = [setConfigLocal mempty (prefix, gucJsonVal keyVals)]
|
||||
where
|
||||
gucJsonVal :: [(ByteString, ByteString)] -> ByteString
|
||||
gucJsonVal = LBS.toStrict . JSON.encode . HM.fromList . arrayByteStringToText
|
||||
arrayByteStringToText :: [(ByteString, ByteString)] -> [(Text,Text)]
|
||||
arrayByteStringToText keyVal = (T.decodeUtf8 *** T.decodeUtf8) <$> keyVal
|
||||
|
||||
+172
-162
@@ -2,100 +2,142 @@
|
||||
Module : PostgREST.OpenAPI
|
||||
Description : Generates the OpenAPI output
|
||||
-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module PostgREST.OpenAPI (encode) where
|
||||
|
||||
module PostgREST.OpenAPI (
|
||||
encodeOpenAPI
|
||||
, isMalformedProxyUri
|
||||
, pickProxy
|
||||
) where
|
||||
|
||||
import qualified Data.Set as Set
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.HashSet.InsOrd as Set
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as T
|
||||
|
||||
import Control.Arrow ((&&&))
|
||||
import Data.Aeson (decode, encode)
|
||||
import Data.HashMap.Strict.InsOrd (InsOrdHashMap, fromList)
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.String (IsString (..))
|
||||
import Data.Text (append, breakOn, dropWhile, init,
|
||||
intercalate, pack, tail, toLower,
|
||||
unpack)
|
||||
import Network.URI (URI (..), URIAuth (..),
|
||||
isAbsoluteURI, parseURI)
|
||||
import Network.URI (URI (..), URIAuth (..))
|
||||
|
||||
import Control.Lens (at, (.~), (?~))
|
||||
|
||||
import Control.Lens
|
||||
import Data.Swagger
|
||||
|
||||
import PostgREST.ApiRequest (ContentType (..))
|
||||
import PostgREST.Config (docsVersion, prettyVersion)
|
||||
import PostgREST.Types (Column (..), ForeignKey (..), PgArg (..),
|
||||
PrimaryKey (..), ProcDescription (..),
|
||||
Proxy (..), Table (..), toMime)
|
||||
import Protolude hiding (Proxy, dropWhile, get,
|
||||
intercalate, (&))
|
||||
import PostgREST.Config (AppConfig (..), Proxy (..),
|
||||
isMalformedProxyUri, toURI)
|
||||
import PostgREST.DbStructure (DbStructure (..))
|
||||
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..))
|
||||
import PostgREST.DbStructure.Proc (ProcDescription (..),
|
||||
ProcParam (..))
|
||||
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
||||
Relationship (..),
|
||||
RelationshipsMap)
|
||||
import PostgREST.DbStructure.Table (Column (..), Table (..),
|
||||
TablesMap)
|
||||
import PostgREST.Version (docsVersion, prettyVersion)
|
||||
|
||||
makeMimeList :: [ContentType] -> MimeList
|
||||
makeMimeList cs = MimeList $ map (fromString . toS . toMime) cs
|
||||
import PostgREST.MediaType
|
||||
|
||||
toSwaggerType :: Text -> SwaggerType t
|
||||
toSwaggerType "character varying" = SwaggerString
|
||||
toSwaggerType "character" = SwaggerString
|
||||
toSwaggerType "text" = SwaggerString
|
||||
toSwaggerType "boolean" = SwaggerBoolean
|
||||
toSwaggerType "smallint" = SwaggerInteger
|
||||
toSwaggerType "integer" = SwaggerInteger
|
||||
toSwaggerType "bigint" = SwaggerInteger
|
||||
toSwaggerType "numeric" = SwaggerNumber
|
||||
toSwaggerType "real" = SwaggerNumber
|
||||
toSwaggerType "double precision" = SwaggerNumber
|
||||
toSwaggerType _ = SwaggerString
|
||||
import Protolude hiding (Proxy, get)
|
||||
|
||||
makeTableDef :: [PrimaryKey] -> (Table, [Column], [Text]) -> (Text, Schema)
|
||||
makeTableDef pks (t, cs, _) =
|
||||
encode :: AppConfig -> DbStructure -> TablesMap -> HM.HashMap k [ProcDescription] -> Maybe Text -> LBS.ByteString
|
||||
encode conf dbStructure tables procs schemaDescription =
|
||||
JSON.encode $
|
||||
postgrestSpec
|
||||
(dbRelationships dbStructure)
|
||||
(concat $ HM.elems procs)
|
||||
(snd <$> HM.toList tables)
|
||||
(proxyUri conf)
|
||||
schemaDescription
|
||||
(configOpenApiSecurityActive conf)
|
||||
|
||||
makeMimeList :: [MediaType] -> MimeList
|
||||
makeMimeList cs = MimeList $ fmap (fromString . BS.unpack . toMime) cs
|
||||
|
||||
toSwaggerType :: Text -> Maybe (SwaggerType t)
|
||||
toSwaggerType "character varying" = Just SwaggerString
|
||||
toSwaggerType "character" = Just SwaggerString
|
||||
toSwaggerType "text" = Just SwaggerString
|
||||
toSwaggerType "boolean" = Just SwaggerBoolean
|
||||
toSwaggerType "smallint" = Just SwaggerInteger
|
||||
toSwaggerType "integer" = Just SwaggerInteger
|
||||
toSwaggerType "bigint" = Just SwaggerInteger
|
||||
toSwaggerType "numeric" = Just SwaggerNumber
|
||||
toSwaggerType "real" = Just SwaggerNumber
|
||||
toSwaggerType "double precision" = Just SwaggerNumber
|
||||
toSwaggerType "ARRAY" = Just SwaggerArray
|
||||
toSwaggerType "json" = Nothing
|
||||
toSwaggerType "jsonb" = Nothing
|
||||
toSwaggerType _ = Just SwaggerString
|
||||
|
||||
parseDefault :: Text -> Text -> Text
|
||||
parseDefault colType colDefault =
|
||||
case toSwaggerType colType of
|
||||
Just SwaggerString -> wrapInQuotations $ case T.stripSuffix ("::" <> colType) colDefault of
|
||||
Just def -> T.dropAround (=='\'') def
|
||||
Nothing -> colDefault
|
||||
_ -> colDefault
|
||||
where
|
||||
wrapInQuotations text = "\"" <> text <> "\""
|
||||
|
||||
makeTableDef :: RelationshipsMap -> Table -> (Text, Schema)
|
||||
makeTableDef rels t =
|
||||
let tn = tableName t in
|
||||
(tn, (mempty :: Schema)
|
||||
& description .~ tableDescription t
|
||||
& type_ .~ SwaggerObject
|
||||
& properties .~ fromList (map (makeProperty pks) cs)
|
||||
& required .~ map colName (filter (not . colNullable) cs))
|
||||
& type_ ?~ SwaggerObject
|
||||
& properties .~ fromList (makeProperty t rels <$> tableColumns t)
|
||||
& required .~ fmap colName (filter (not . colNullable) $ tableColumns t))
|
||||
|
||||
makeProperty :: [PrimaryKey] -> Column -> (Text, Referenced Schema)
|
||||
makeProperty pks c = (colName c, Inline s)
|
||||
makeProperty :: Table -> RelationshipsMap -> Column -> (Text, Referenced Schema)
|
||||
makeProperty tbl rels col = (colName col, Inline s)
|
||||
where
|
||||
e = if null $ colEnum c then Nothing else decode $ encode $ colEnum c
|
||||
fk ForeignKey{fkCol=Column{colTable=Table{tableName=a}, colName=b}} =
|
||||
intercalate "" ["This is a Foreign Key to `", a, ".", b, "`.<fk table='", a, "' column='", b, "'/>"]
|
||||
e = if null $ colEnum col then Nothing else JSON.decode $ JSON.encode $ colEnum col
|
||||
fk :: Maybe Text
|
||||
fk =
|
||||
let
|
||||
-- Finds the relationship that has a single column foreign key
|
||||
rel = find (\case
|
||||
Relationship{relCardinality=(M2O _ relColumns)} -> [colName col] == (fst <$> relColumns)
|
||||
_ -> False
|
||||
) $ fromMaybe mempty $ HM.lookup (QualifiedIdentifier (tableSchema tbl) (tableName tbl), tableSchema tbl) rels
|
||||
fCol = (headMay . (\r -> snd <$> relColumns (relCardinality r)) =<< rel)
|
||||
fTbl = qiName . relForeignTable <$> rel
|
||||
fTblCol = (,) <$> fTbl <*> fCol
|
||||
in
|
||||
(\(a, b) -> T.intercalate "" ["This is a Foreign Key to `", a, ".", b, "`.<fk table='", a, "' column='", b, "'/>"]) <$> fTblCol
|
||||
pk :: Bool
|
||||
pk = any (\p -> pkTable p == colTable c && pkName p == colName c) pks
|
||||
pk = colName col `elem` tablePKCols tbl
|
||||
n = catMaybes
|
||||
[ Just "Note:"
|
||||
, if pk then Just "This is a Primary Key.<pk/>" else Nothing
|
||||
, fk <$> colFK c
|
||||
, fk
|
||||
]
|
||||
d =
|
||||
if length n > 1 then
|
||||
Just $ append (maybe "" (`append` "\n\n") $ colDescription c) (intercalate "\n" n)
|
||||
Just $ T.append (maybe "" (`T.append` "\n\n") $ colDescription col) (T.intercalate "\n" n)
|
||||
else
|
||||
colDescription c
|
||||
colDescription col
|
||||
s =
|
||||
(mempty :: Schema)
|
||||
& default_ .~ (decode . toS =<< colDefault c)
|
||||
& default_ .~ (JSON.decode . toUtf8Lazy . parseDefault (colType col) =<< colDefault col)
|
||||
& description .~ d
|
||||
& enum_ .~ e
|
||||
& format ?~ colType c
|
||||
& maxLength .~ (fromIntegral <$> colMaxLen c)
|
||||
& type_ .~ toSwaggerType (colType c)
|
||||
& format ?~ colType col
|
||||
& maxLength .~ (fromIntegral <$> colMaxLen col)
|
||||
& type_ .~ toSwaggerType (colType col)
|
||||
|
||||
makeProcSchema :: ProcDescription -> Schema
|
||||
makeProcSchema pd =
|
||||
(mempty :: Schema)
|
||||
& description .~ pdDescription pd
|
||||
& type_ .~ SwaggerObject
|
||||
& properties .~ fromList (map makeProcProperty (pdArgs pd))
|
||||
& required .~ map pgaName (filter pgaReq (pdArgs pd))
|
||||
& type_ ?~ SwaggerObject
|
||||
& properties .~ fromList (fmap makeProcProperty (pdParams pd))
|
||||
& required .~ fmap ppName (filter ppReq (pdParams pd))
|
||||
|
||||
makeProcProperty :: PgArg -> (Text, Referenced Schema)
|
||||
makeProcProperty (PgArg n t _) = (n, Inline s)
|
||||
makeProcProperty :: ProcParam -> (Text, Referenced Schema)
|
||||
makeProcProperty (ProcParam n t _ _) = (n, Inline s)
|
||||
where
|
||||
s = (mempty :: Schema)
|
||||
& type_ .~ toSwaggerType t
|
||||
@@ -109,19 +151,19 @@ makePreferParam ts =
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamHeader
|
||||
& type_ .~ SwaggerString
|
||||
& enum_ .~ decode (encode ts))
|
||||
& type_ ?~ SwaggerString
|
||||
& enum_ .~ JSON.decode (JSON.encode ts))
|
||||
|
||||
makeProcParam :: ProcDescription -> [Referenced Param]
|
||||
makeProcParam pd =
|
||||
[ Inline $ (mempty :: Param)
|
||||
& name .~ "args"
|
||||
& required ?~ True
|
||||
& schema .~ (ParamBody $ Inline $ makeProcSchema pd)
|
||||
& schema .~ ParamBody (Inline $ makeProcSchema pd)
|
||||
, Ref $ Reference "preferParams"
|
||||
]
|
||||
|
||||
makeParamDefs :: [(Table, [Column], [Text])] -> [(Text, Param)]
|
||||
makeParamDefs :: [Table] -> [(Text, Param)]
|
||||
makeParamDefs ti =
|
||||
[ ("preferParams", makePreferParam ["params=single-object"])
|
||||
, ("preferReturn", makePreferParam ["return=representation", "return=minimal", "return=none"])
|
||||
@@ -132,46 +174,53 @@ makeParamDefs ti =
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamQuery
|
||||
& type_ .~ SwaggerString))
|
||||
& type_ ?~ SwaggerString))
|
||||
, ("on_conflict", (mempty :: Param)
|
||||
& name .~ "on_conflict"
|
||||
& description ?~ "On Conflict"
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamQuery
|
||||
& type_ ?~ SwaggerString))
|
||||
, ("order", (mempty :: Param)
|
||||
& name .~ "order"
|
||||
& description ?~ "Ordering"
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamQuery
|
||||
& type_ .~ SwaggerString))
|
||||
& type_ ?~ SwaggerString))
|
||||
, ("range", (mempty :: Param)
|
||||
& name .~ "Range"
|
||||
& description ?~ "Limiting and Pagination"
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamHeader
|
||||
& type_ .~ SwaggerString))
|
||||
& type_ ?~ SwaggerString))
|
||||
, ("rangeUnit", (mempty :: Param)
|
||||
& name .~ "Range-Unit"
|
||||
& description ?~ "Limiting and Pagination"
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamHeader
|
||||
& type_ .~ SwaggerString
|
||||
& default_ .~ decode "\"items\""))
|
||||
& type_ ?~ SwaggerString
|
||||
& default_ .~ JSON.decode "\"items\""))
|
||||
, ("offset", (mempty :: Param)
|
||||
& name .~ "offset"
|
||||
& description ?~ "Limiting and Pagination"
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamQuery
|
||||
& type_ .~ SwaggerString))
|
||||
& type_ ?~ SwaggerString))
|
||||
, ("limit", (mempty :: Param)
|
||||
& name .~ "limit"
|
||||
& description ?~ "Limiting and Pagination"
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamQuery
|
||||
& type_ .~ SwaggerString))
|
||||
& type_ ?~ SwaggerString))
|
||||
]
|
||||
<> concat [ makeObjectBody (tableName t) : makeRowFilters (tableName t) cs
|
||||
| (t, cs, _) <- ti
|
||||
<> concat [ makeObjectBody (tableName t) : makeRowFilters (tableName t) (tableColumns t)
|
||||
| t <- ti
|
||||
]
|
||||
|
||||
makeObjectBody :: Text -> (Text, Param)
|
||||
@@ -184,54 +233,54 @@ makeObjectBody tn =
|
||||
|
||||
makeRowFilter :: Text -> Column -> (Text, Param)
|
||||
makeRowFilter tn c =
|
||||
(intercalate "." ["rowFilter", tn, colName c], (mempty :: Param)
|
||||
(T.intercalate "." ["rowFilter", tn, colName c], (mempty :: Param)
|
||||
& name .~ colName c
|
||||
& description .~ colDescription c
|
||||
& required ?~ False
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamQuery
|
||||
& type_ .~ SwaggerString
|
||||
& type_ ?~ SwaggerString
|
||||
& format ?~ colType c))
|
||||
|
||||
makeRowFilters :: Text -> [Column] -> [(Text, Param)]
|
||||
makeRowFilters tn = map (makeRowFilter tn)
|
||||
makeRowFilters tn = fmap (makeRowFilter tn)
|
||||
|
||||
makePathItem :: (Table, [Column], [Text]) -> (FilePath, PathItem)
|
||||
makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t)
|
||||
makePathItem :: Table -> (FilePath, PathItem)
|
||||
makePathItem t = ("/" ++ T.unpack tn, p $ tableInsertable t || tableUpdatable t || tableDeletable t)
|
||||
where
|
||||
-- Use first line of table description as summary; rest as description (if present)
|
||||
-- We strip leading newlines from description so that users can include a blank line between summary and description
|
||||
(tSum, tDesc) = fmap fst &&& fmap (dropWhile (=='\n') . snd) $
|
||||
breakOn "\n" <$> tableDescription t
|
||||
(tSum, tDesc) = fmap fst &&& fmap (T.dropWhile (=='\n') . snd) $
|
||||
T.breakOn "\n" <$> tableDescription t
|
||||
tOp = (mempty :: Operation)
|
||||
& tags .~ Set.fromList [tn]
|
||||
& summary .~ tSum
|
||||
& description .~ mfilter (/="") tDesc
|
||||
getOp = tOp
|
||||
& parameters .~ map ref (rs <> ["select", "order", "range", "rangeUnit", "offset", "limit", "preferCount"])
|
||||
& parameters .~ fmap ref (rs <> ["select", "order", "range", "rangeUnit", "offset", "limit", "preferCount"])
|
||||
& at 206 ?~ "Partial Content"
|
||||
& at 200 ?~ Inline ((mempty :: Response)
|
||||
& description .~ "OK"
|
||||
& schema ?~ Inline (mempty
|
||||
& type_ .~ SwaggerArray
|
||||
& items ?~ (SwaggerItemsObject $ Ref $ Reference $ tableName t)
|
||||
& type_ ?~ SwaggerArray
|
||||
& items ?~ SwaggerItemsObject (Ref $ Reference $ tableName t)
|
||||
)
|
||||
)
|
||||
postOp = tOp
|
||||
& parameters .~ map ref ["body." <> tn, "preferReturn"]
|
||||
& parameters .~ fmap ref ["body." <> tn, "select", "preferReturn"]
|
||||
& at 201 ?~ "Created"
|
||||
patchOp = tOp
|
||||
& parameters .~ map ref (rs <> ["body." <> tn, "preferReturn"])
|
||||
& parameters .~ fmap ref (rs <> ["body." <> tn, "preferReturn"])
|
||||
& at 204 ?~ "No Content"
|
||||
deletOp = tOp
|
||||
& parameters .~ map ref (rs <> ["preferReturn"])
|
||||
& parameters .~ fmap ref (rs <> ["preferReturn"])
|
||||
& at 204 ?~ "No Content"
|
||||
pr = (mempty :: PathItem) & get ?~ getOp
|
||||
pw = pr & post ?~ postOp & patch ?~ patchOp & delete ?~ deletOp
|
||||
p False = pr
|
||||
p True = pw
|
||||
tn = tableName t
|
||||
rs = [ intercalate "." ["rowFilter", tn, colName c ] | c <- cs ]
|
||||
rs = [ T.intercalate "." ["rowFilter", tn, colName c ] | c <- tableColumns t ]
|
||||
ref = Ref . Reference
|
||||
|
||||
makeProcPathItem :: ProcDescription -> (FilePath, PathItem)
|
||||
@@ -239,14 +288,14 @@ makeProcPathItem pd = ("/rpc/" ++ toS (pdName pd), pe)
|
||||
where
|
||||
-- Use first line of proc description as summary; rest as description (if present)
|
||||
-- We strip leading newlines from description so that users can include a blank line between summary and description
|
||||
(pSum, pDesc) = fmap fst &&& fmap (dropWhile (=='\n') . snd) $
|
||||
breakOn "\n" <$> pdDescription pd
|
||||
(pSum, pDesc) = fmap fst &&& fmap (T.dropWhile (=='\n') . snd) $
|
||||
T.breakOn "\n" <$> pdDescription pd
|
||||
postOp = (mempty :: Operation)
|
||||
& summary .~ pSum
|
||||
& description .~ mfilter (/="") pDesc
|
||||
& parameters .~ makeProcParam pd
|
||||
& tags .~ Set.fromList ["(rpc) " <> pdName pd]
|
||||
& produces ?~ makeMimeList [CTApplicationJSON, CTSingularJSON]
|
||||
& produces ?~ makeMimeList [MTApplicationJSON, MTSingularJSON]
|
||||
& at 200 ?~ "OK"
|
||||
pe = (mempty :: PathItem) & post ?~ postOp
|
||||
|
||||
@@ -256,14 +305,22 @@ makeRootPathItem = ("/", p)
|
||||
getOp = (mempty :: Operation)
|
||||
& tags .~ Set.fromList ["Introspection"]
|
||||
& summary ?~ "OpenAPI description (this document)"
|
||||
& produces ?~ makeMimeList [CTOpenAPI, CTApplicationJSON]
|
||||
& produces ?~ makeMimeList [MTOpenAPI, MTApplicationJSON]
|
||||
& at 200 ?~ "OK"
|
||||
pr = (mempty :: PathItem) & get ?~ getOp
|
||||
p = pr
|
||||
|
||||
makePathItems :: [ProcDescription] -> [(Table, [Column], [Text])] -> InsOrdHashMap FilePath PathItem
|
||||
makePathItems :: [ProcDescription] -> [Table] -> InsOrdHashMap FilePath PathItem
|
||||
makePathItems pds ti = fromList $ makeRootPathItem :
|
||||
map makePathItem ti ++ map makeProcPathItem pds
|
||||
fmap makePathItem ti ++ fmap makeProcPathItem pds
|
||||
|
||||
makeSecurityDefinitions :: Text -> Bool -> SecurityDefinitions
|
||||
makeSecurityDefinitions secName allow
|
||||
| allow = SecurityDefinitions (fromList [(secName, SecurityScheme secSchType secSchDescription)])
|
||||
| otherwise = mempty
|
||||
where
|
||||
secSchType = SecuritySchemeApiKey (ApiKeyParams "Authorization" ApiKeyHeader)
|
||||
secSchDescription = Just "Add the token prepending \"Bearer \" (without quotes) to it"
|
||||
|
||||
escapeHostName :: Text -> Text
|
||||
escapeHostName "*" = "0.0.0.0"
|
||||
@@ -273,48 +330,30 @@ escapeHostName "*6" = "0.0.0.0"
|
||||
escapeHostName "!6" = "0.0.0.0"
|
||||
escapeHostName h = h
|
||||
|
||||
postgrestSpec :: [ProcDescription] -> [(Table, [Column], [Text])] -> (Text, Text, Integer, Text) -> Maybe Text -> [PrimaryKey] -> Swagger
|
||||
postgrestSpec pds ti (s, h, p, b) sd pks = (mempty :: Swagger)
|
||||
& basePath ?~ unpack b
|
||||
postgrestSpec :: RelationshipsMap -> [ProcDescription] -> [Table] -> (Text, Text, Integer, Text) -> Maybe Text -> Bool -> Swagger
|
||||
postgrestSpec rels pds ti (s, h, p, b) sd allowSecurityDef = (mempty :: Swagger)
|
||||
& basePath ?~ T.unpack b
|
||||
& schemes ?~ [s']
|
||||
& info .~ ((mempty :: Info)
|
||||
& version .~ prettyVersion
|
||||
& version .~ T.decodeUtf8 prettyVersion
|
||||
& title .~ "PostgREST API"
|
||||
& description ?~ d)
|
||||
& externalDocs ?~ ((mempty :: ExternalDocs)
|
||||
& description ?~ "PostgREST Documentation"
|
||||
& url .~ URL ("https://postgrest.org/en/" <> docsVersion <> "/api.html"))
|
||||
& host .~ h'
|
||||
& definitions .~ fromList (map (makeTableDef pks) ti)
|
||||
& definitions .~ fromList (makeTableDef rels <$> ti)
|
||||
& parameters .~ fromList (makeParamDefs ti)
|
||||
& paths .~ makePathItems pds ti
|
||||
& produces .~ makeMimeList [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||
& consumes .~ makeMimeList [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||
& produces .~ makeMimeList [MTApplicationJSON, MTSingularJSON, MTTextCSV]
|
||||
& consumes .~ makeMimeList [MTApplicationJSON, MTSingularJSON, MTTextCSV]
|
||||
& securityDefinitions .~ makeSecurityDefinitions securityDefName allowSecurityDef
|
||||
& security .~ [SecurityRequirement (fromList [(securityDefName, [])]) | allowSecurityDef]
|
||||
where
|
||||
s' = if s == "http" then Http else Https
|
||||
h' = Just $ Host (unpack $ escapeHostName h) (Just (fromInteger p))
|
||||
h' = Just $ Host (T.unpack $ escapeHostName h) (Just (fromInteger p))
|
||||
d = fromMaybe "This is a dynamic API generated by PostgREST" sd
|
||||
|
||||
encodeOpenAPI :: [ProcDescription] -> [(Table, [Column], [Text])] -> (Text, Text, Integer, Text) -> Maybe Text -> [PrimaryKey] -> LByteString
|
||||
encodeOpenAPI pds ti uri sd pks = encode $ postgrestSpec pds ti uri sd pks
|
||||
|
||||
{-|
|
||||
Test whether a proxy uri is malformed or not.
|
||||
A valid proxy uri should be an absolute uri without query and user info,
|
||||
only http(s) schemes are valid, port number range is 1-65535.
|
||||
|
||||
For example
|
||||
http://postgrest.com/openapi.json
|
||||
https://postgrest.com:8080/openapi.json
|
||||
-}
|
||||
isMalformedProxyUri :: Maybe Text -> Bool
|
||||
isMalformedProxyUri Nothing = False
|
||||
isMalformedProxyUri (Just uri)
|
||||
| isAbsoluteURI (toS uri) = not $ isUriValid $ toURI uri
|
||||
| otherwise = True
|
||||
|
||||
toURI :: Text -> URI
|
||||
toURI uri = fromJust $ parseURI (toS uri)
|
||||
securityDefName = "JWT"
|
||||
|
||||
pickProxy :: Maybe Text -> Maybe Proxy
|
||||
pickProxy proxy
|
||||
@@ -322,7 +361,7 @@ pickProxy proxy
|
||||
-- should never happen
|
||||
-- since the request would have been rejected by the middleware if proxy uri
|
||||
-- is malformed
|
||||
| isMalformedProxyUri proxy = Nothing
|
||||
| isMalformedProxyUri $ fromMaybe mempty proxy = Nothing
|
||||
| otherwise = Just Proxy {
|
||||
proxyScheme = scheme
|
||||
, proxyHost = host'
|
||||
@@ -331,53 +370,24 @@ pickProxy proxy
|
||||
}
|
||||
where
|
||||
uri = toURI $ fromJust proxy
|
||||
scheme = init $ toLower $ pack $ uriScheme uri
|
||||
scheme = T.init $ T.toLower $ T.pack $ uriScheme uri
|
||||
path URI {uriPath = ""} = "/"
|
||||
path URI {uriPath = p} = p
|
||||
path' = pack $ path uri
|
||||
path' = T.pack $ path uri
|
||||
authority = fromJust $ uriAuthority uri
|
||||
host' = pack $ uriRegName authority
|
||||
host' = T.pack $ uriRegName authority
|
||||
port' = uriPort authority
|
||||
readPort = fromMaybe 80 . readMaybe
|
||||
port'' :: Integer
|
||||
port'' = case (port', scheme) of
|
||||
("", "http") -> 80
|
||||
("", "https") -> 443
|
||||
_ -> readPort $ unpack $ tail $ pack port'
|
||||
_ -> readPort $ T.unpack $ T.tail $ T.pack port'
|
||||
|
||||
isUriValid:: URI -> Bool
|
||||
isUriValid = fAnd [isSchemeValid, isQueryValid, isAuthorityValid]
|
||||
|
||||
fAnd :: [a -> Bool] -> a -> Bool
|
||||
fAnd fs x = all ($ x) fs
|
||||
|
||||
isSchemeValid :: URI -> Bool
|
||||
isSchemeValid URI {uriScheme = s}
|
||||
| toLower (pack s) == "https:" = True
|
||||
| toLower (pack s) == "http:" = True
|
||||
| otherwise = False
|
||||
|
||||
isQueryValid :: URI -> Bool
|
||||
isQueryValid URI {uriQuery = ""} = True
|
||||
isQueryValid _ = False
|
||||
|
||||
isAuthorityValid :: URI -> Bool
|
||||
isAuthorityValid URI {uriAuthority = a}
|
||||
| isJust a = fAnd [isUserInfoValid, isHostValid, isPortValid] $ fromJust a
|
||||
| otherwise = False
|
||||
|
||||
isUserInfoValid :: URIAuth -> Bool
|
||||
isUserInfoValid URIAuth {uriUserInfo = ""} = True
|
||||
isUserInfoValid _ = False
|
||||
|
||||
isHostValid :: URIAuth -> Bool
|
||||
isHostValid URIAuth {uriRegName = ""} = False
|
||||
isHostValid _ = True
|
||||
|
||||
isPortValid :: URIAuth -> Bool
|
||||
isPortValid URIAuth {uriPort = ""} = True
|
||||
isPortValid URIAuth {uriPort = (':':p)} =
|
||||
case readMaybe p of
|
||||
Just i -> i > (0 :: Integer) && i < 65536
|
||||
Nothing -> False
|
||||
isPortValid _ = False
|
||||
proxyUri :: AppConfig -> (Text, Text, Integer, Text)
|
||||
proxyUri AppConfig{..} =
|
||||
case pickProxy $ toS <$> configOpenApiServerProxyUri of
|
||||
Just Proxy{..} ->
|
||||
(proxyScheme, proxyHost, proxyPort, proxyPath)
|
||||
Nothing ->
|
||||
("http", configServerHost, toInteger configServerPort, "/")
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
{-|
|
||||
Module : PostgREST.Parsers
|
||||
Description : PostgREST parser combinators
|
||||
|
||||
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`.
|
||||
-}
|
||||
module PostgREST.Parsers where
|
||||
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import qualified Data.Set as S
|
||||
|
||||
import Control.Monad ((>>))
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
import Data.Foldable (foldl1)
|
||||
import Data.Functor (($>))
|
||||
import Data.List (init, last)
|
||||
import Data.Text (intercalate, replace, strip)
|
||||
import Text.Read (read)
|
||||
|
||||
import Data.Tree
|
||||
import Text.Parsec.Error
|
||||
import Text.ParserCombinators.Parsec hiding (many, (<|>))
|
||||
|
||||
import PostgREST.Error (ApiRequestError (ParseRequestError))
|
||||
import PostgREST.RangeQuery (NonnegRange)
|
||||
import PostgREST.Types
|
||||
import Protolude hiding (intercalate, option, replace, try)
|
||||
|
||||
pRequestSelect :: Text -> Either ApiRequestError [Tree SelectItem]
|
||||
pRequestSelect selStr =
|
||||
mapError $ parse pFieldForest ("failed to parse select parameter (" <> toS selStr <> ")") (toS selStr)
|
||||
|
||||
pRequestFilter :: (Text, Text) -> Either ApiRequestError (EmbedPath, Filter)
|
||||
pRequestFilter (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> oper)
|
||||
where
|
||||
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
|
||||
oper = parse (pOpExpr pSingleVal) ("failed to parse filter (" ++ toS v ++ ")") $ toS v
|
||||
path = fst <$> treePath
|
||||
fld = snd <$> treePath
|
||||
|
||||
pRequestOrder :: (Text, Text) -> Either ApiRequestError (EmbedPath, [OrderTerm])
|
||||
pRequestOrder (k, v) = mapError $ (,) <$> path <*> ord'
|
||||
where
|
||||
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
|
||||
path = fst <$> treePath
|
||||
ord' = parse pOrder ("failed to parse order (" ++ toS v ++ ")") $ toS v
|
||||
|
||||
pRequestRange :: (ByteString, NonnegRange) -> Either ApiRequestError (EmbedPath, NonnegRange)
|
||||
pRequestRange (k, v) = mapError $ (,) <$> path <*> pure v
|
||||
where
|
||||
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
|
||||
path = fst <$> treePath
|
||||
|
||||
pRequestLogicTree :: (Text, Text) -> Either ApiRequestError (EmbedPath, LogicTree)
|
||||
pRequestLogicTree (k, v) = mapError $ (,) <$> embedPath <*> logicTree
|
||||
where
|
||||
path = parse pLogicPath ("failed to parser 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=(.. , ..)"
|
||||
parse pLogicTree ("failed to parse logic tree (" ++ toS v ++ ")") $ toS (op <> v)
|
||||
|
||||
pRequestColumns :: Maybe Text -> Either ApiRequestError (Maybe (S.Set FieldName))
|
||||
pRequestColumns colStr =
|
||||
case colStr of
|
||||
Just str ->
|
||||
mapError $ Just . S.fromList <$> 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 <- option [] pJsonPath
|
||||
return (init p, (last p, jp))
|
||||
|
||||
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 = toS <$> (string "*" $> ("*"::ByteString))
|
||||
|
||||
pFieldName :: Parser Text
|
||||
pFieldName =
|
||||
pQuotedValue <|>
|
||||
intercalate "-" . map toS <$> (many1 (letter <|> digit <|> oneOf "_ ") `sepBy1` dash) <?>
|
||||
"field name (* or [a..z0..9_])"
|
||||
where
|
||||
isDash :: GenParser Char st ()
|
||||
isDash = try ( char '-' >> notFollowedBy (char '>') )
|
||||
dash :: Parser Char
|
||||
dash = isDash $> '-'
|
||||
|
||||
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 <$> ((:) <$> option '+' (char '-') <*> many1 digit) <* pEnd
|
||||
pEnd = try (void $ lookAhead (string "->")) <|>
|
||||
try (void $ lookAhead (string "::")) <|>
|
||||
try eof in
|
||||
try pJIdx <|> try pJKey
|
||||
|
||||
pField :: Parser Field
|
||||
pField = lexeme $ (,) <$> pFieldName <*> option [] pJsonPath
|
||||
|
||||
aliasSeparator :: Parser ()
|
||||
aliasSeparator = char ':' >> notFollowedBy (char ':')
|
||||
|
||||
pRelationSelect :: Parser SelectItem
|
||||
pRelationSelect = lexeme $ try ( do
|
||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||
fld <- pField
|
||||
relationDetail <- optionMaybe (
|
||||
try ( char '+' *> pFieldName ) <|>
|
||||
try ( char '.' *> pFieldName ) -- TODO deprecated, remove in next major version
|
||||
)
|
||||
|
||||
return (fld, Nothing, alias, relationDetail)
|
||||
)
|
||||
|
||||
pFieldSelect :: Parser SelectItem
|
||||
pFieldSelect = lexeme $
|
||||
try (
|
||||
do
|
||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||
fld <- pField
|
||||
cast' <- optionMaybe (string "::" *> many letter)
|
||||
return (fld, toS <$> cast', alias, Nothing)
|
||||
)
|
||||
<|> do
|
||||
s <- pStar
|
||||
return ((s, []), Nothing, Nothing, Nothing)
|
||||
|
||||
pOpExpr :: Parser SingleVal -> Parser OpExpr
|
||||
pOpExpr pSVal = try ( string "not" *> pDelimiter *> (OpExpr True <$> pOperation)) <|> OpExpr False <$> pOperation
|
||||
where
|
||||
pOperation :: Parser Operation
|
||||
pOperation =
|
||||
Op . toS <$> foldl1 (<|>) (try . ((<* pDelimiter) . string) . toS <$> M.keys ops) <*> pSVal
|
||||
<|> In <$> (try (string "in" *> pDelimiter) *> pListVal)
|
||||
<|> pFts
|
||||
<?> "operator (eq, gt, ...)"
|
||||
|
||||
pFts = do
|
||||
op <- foldl1 (<|>) (try . string . toS <$> ftsOps)
|
||||
lang <- optionMaybe $ try (between (char '(') (char ')') (many (letter <|> digit <|> oneOf "_")))
|
||||
pDelimiter >> Fts (toS op) (toS <$> lang) <$> pSVal
|
||||
|
||||
ops = M.filterWithKey (const . flip notElem ("in":ftsOps)) operators
|
||||
ftsOps = M.keys ftsOperators
|
||||
|
||||
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 (noneOf "\"") <* char '"')
|
||||
|
||||
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 "}"
|
||||
toS <$> pure (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 ',')
|
||||
|
||||
mapError :: Either ParseError a -> Either ApiRequestError a
|
||||
mapError = mapLeft translateError
|
||||
where
|
||||
translateError e =
|
||||
ParseRequestError message details
|
||||
where
|
||||
message = show $ errorPos e
|
||||
details = strip $ replace "\n" " " $ toS
|
||||
$ showErrorMessages "or" "unknown parse error" "expecting" "unexpected" "end of input" (errorMessages e)
|
||||
|
||||
-- Used for the config value "role-claim-key"
|
||||
pRoleClaimKey :: Text -> Either ApiRequestError JSPath
|
||||
pRoleClaimKey selStr =
|
||||
mapError $ parse pJSPath ("failed to parse role-claim-key value (" <> toS selStr <> ")") (toS selStr)
|
||||
|
||||
pJSPath :: Parser JSPath
|
||||
pJSPath = toJSPath <$> (period *> pPath `sepBy` period <* eof)
|
||||
where
|
||||
toJSPath :: [(Text, Maybe Int)] -> JSPath
|
||||
toJSPath = concatMap (\(key, idx) -> JSPKey key : maybeToList (JSPIdx <$> idx))
|
||||
period = char '.' <?> "period (.)"
|
||||
pPath :: Parser (Text, Maybe Int)
|
||||
pPath = (,) <$> pJSPKey <*> optionMaybe pJSPIdx
|
||||
|
||||
pJSPKey :: Parser Text
|
||||
pJSPKey = toS <$> many1 (alphaNum <|> oneOf "_$@") <|> pQuotedValue <?> "attribute name [a..z0..9_$@])"
|
||||
|
||||
pJSPIdx :: Parser Int
|
||||
pJSPIdx = char '[' *> (read <$> many1 digit) <* char ']' <?> "array index [0..n]"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user