Compare commits
552
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a46ac79ea8 | ||
|
|
a2faa667f3 | ||
|
|
366502729d | ||
|
|
7c8f79d86f | ||
|
|
827121b72f | ||
|
|
d4949c633e | ||
|
|
5f6f7dca44 | ||
|
|
ea08a4d767 | ||
|
|
822f8ef802 | ||
|
|
c8439e437c | ||
|
|
dc179665dc | ||
|
|
3ec9b94c79 | ||
|
|
9b9521b75a | ||
|
|
41b86fffa5 | ||
|
|
bf758698b3 | ||
|
|
a5cc457875 | ||
|
|
f80122e12b | ||
|
|
2e5baaf296 | ||
|
|
0e0bc6a2c1 | ||
|
|
38829191a7 | ||
|
|
c9253ed056 | ||
|
|
a1ab388ab6 | ||
|
|
de4ad62562 | ||
|
|
e2a677e21d | ||
|
|
baebacf3db | ||
|
|
3b1373ec02 | ||
|
|
695678ad40 | ||
|
|
92ba6d5caa | ||
|
|
6b4022b0c1 | ||
|
|
0cb354be7a | ||
|
|
1c869bf3a6 | ||
|
|
fee820bac2 | ||
|
|
1676314fc3 | ||
|
|
49a16fe505 | ||
|
|
43447ed711 | ||
|
|
edbd2a3dca | ||
|
|
03f7234360 | ||
|
|
d79f28fd75 | ||
|
|
051b4799de | ||
|
|
1b74b93466 | ||
|
|
4ccb125e85 | ||
|
|
b6c6bb27a8 | ||
|
|
1f70e42d3b | ||
|
|
be7a42ff30 | ||
|
|
9ed7f52685 | ||
|
|
dfb3602318 | ||
|
|
f1cd9a2136 | ||
|
|
759917772c | ||
|
|
bcc8998e5e | ||
|
|
6af77360d3 | ||
|
|
f939a81ad6 | ||
|
|
bf8a52694a | ||
|
|
4fbde347f8 | ||
|
|
aca58c8571 | ||
|
|
4ca34c0695 | ||
|
|
26ce39bb46 | ||
|
|
4e8c4f218f | ||
|
|
070eb2fc3a | ||
|
|
431f8586f5 | ||
|
|
c9adaed79d | ||
|
|
381e12efa2 | ||
|
|
f4fc29855e | ||
|
|
2fb2e86473 | ||
|
|
c31aeecdcf | ||
|
|
5d4f82d70f | ||
|
|
ad907219be | ||
|
|
463b701e9f | ||
|
|
3d98f8d65b | ||
|
|
a4c1d945ee | ||
|
|
ff068aa1b7 | ||
|
|
9e48a338c3 | ||
|
|
b610b1ea51 | ||
|
|
69c6a0aa36 | ||
|
|
a5e66a4448 | ||
|
|
328598eaed | ||
|
|
d6c338d588 | ||
|
|
bd5de884fa | ||
|
|
8d5b1018d3 | ||
|
|
ae7d4d2553 | ||
|
|
88afa05cf8 | ||
|
|
748ba7f02b | ||
|
|
0b665ecd10 | ||
|
|
1b402d16ad | ||
|
|
135b77d12b | ||
|
|
effcc080e5 | ||
|
|
5e504a77fa | ||
|
|
c9c6ebb53a | ||
|
|
941c2f2d17 | ||
|
|
bf7dc485b3 | ||
|
|
622c6d3f19 | ||
|
|
12ef63370b | ||
|
|
796339172c | ||
|
|
238e9078bf | ||
|
|
1934179c64 | ||
|
|
88c365fb52 | ||
|
|
0ff8a1a37a | ||
|
|
c448712e88 | ||
|
|
d4ba41bd43 | ||
|
|
40a1fea916 | ||
|
|
1f54e2accc | ||
|
|
d5df12c9c2 | ||
|
|
af4c4157f5 | ||
|
|
5ff271592c | ||
|
|
bb4358c8bb | ||
|
|
2ab2706c8e | ||
|
|
b83fbb76a9 | ||
|
|
fb54db143d | ||
|
|
8ee8cdd7f8 | ||
|
|
bee2ea6fa6 | ||
|
|
a4aa58b4de | ||
|
|
e741c1bca7 | ||
|
|
d5694672a4 | ||
|
|
ca858f7544 | ||
|
|
cab60b9ee8 | ||
|
|
0b7f0197fc | ||
|
|
93db1a5a47 | ||
|
|
f657ac4e0b | ||
|
|
f5b9ed28db | ||
|
|
0eb2bdd8e1 | ||
|
|
58368513ab | ||
|
|
5abacba0d8 | ||
|
|
2861b35f41 | ||
|
|
4e4ae3c7a2 | ||
|
|
1c6d0816a5 | ||
|
|
341e40196d | ||
|
|
8e1f0a0471 | ||
|
|
2408cd332d | ||
|
|
58a973e664 | ||
|
|
a36963026e | ||
|
|
78f231c87a | ||
|
|
5992430e9f | ||
|
|
83dc082acf | ||
|
|
2edc44c352 | ||
|
|
02feaf087e | ||
|
|
c52fd3fd2b | ||
|
|
248b777117 | ||
|
|
66fda76644 | ||
|
|
23692ab8e7 | ||
|
|
b977ffbb90 | ||
|
|
85a313a8cc | ||
|
|
bfb4f900e7 | ||
|
|
055efe7abd | ||
|
|
ca96328142 | ||
|
|
d6816d8d2a | ||
|
|
c5d495006a | ||
|
|
abe414ad88 | ||
|
|
e95e815483 | ||
|
|
c3500bd3af | ||
|
|
99984d328e | ||
|
|
aaaf7e96bd | ||
|
|
37fc0a211b | ||
|
|
c35eb7a6c2 | ||
|
|
2bd4b07418 | ||
|
|
55cd5a23ff | ||
|
|
72a9d70f09 | ||
|
|
d10c779fc6 | ||
|
|
d031bb2df5 | ||
|
|
73a465501e | ||
|
|
6f1fe69433 | ||
|
|
246239f42a | ||
|
|
1682677297 | ||
|
|
21b8c3458a | ||
|
|
5356f4e973 | ||
|
|
00c7cb1a22 | ||
|
|
d75b1a7e96 | ||
|
|
fe0386e9c4 | ||
|
|
5a4c11c72a | ||
|
|
c15e908266 | ||
|
|
c18727ff43 | ||
|
|
886df84e87 | ||
|
|
8f5fe3f46b | ||
|
|
ffa393850a | ||
|
|
f5c7cde9b7 | ||
|
|
0018c38f79 | ||
|
|
9042457326 | ||
|
|
e988fe49ed | ||
|
|
e592d568c6 | ||
|
|
9ec5b030ce | ||
|
|
76e0e1fbff | ||
|
|
b8ca1bb04c | ||
|
|
57c5ff5db3 | ||
|
|
2b83dcc794 | ||
|
|
7c3bccf4ba | ||
|
|
d2aa6c1b8a | ||
|
|
a329bca16b | ||
|
|
388930336d | ||
|
|
d06377a37b | ||
|
|
eed018fe1d | ||
|
|
013f078bc4 | ||
|
|
2bcd336400 | ||
|
|
8f34afd66e | ||
|
|
4cdc4c4861 | ||
|
|
050554e2bb | ||
|
|
b3b7bd9327 | ||
|
|
dac8a5fb86 | ||
|
|
fd6a3bdccf | ||
|
|
60c70c5b37 | ||
|
|
c9f9b93ad7 | ||
|
|
e39b91c27b | ||
|
|
d694ad90e0 | ||
|
|
99b8246d6e | ||
|
|
90bf1762ae | ||
|
|
c9c975ce49 | ||
|
|
29c2aa015f | ||
|
|
abad47ff1b | ||
|
|
c232e97f80 | ||
|
|
ab9b00ce88 | ||
|
|
9200a98087 | ||
|
|
3b27b2220d | ||
|
|
6573953ebd | ||
|
|
0fa2f569a7 | ||
|
|
24a27d17d0 | ||
|
|
1724e4551d | ||
|
|
457f20f962 | ||
|
|
70240b0d08 | ||
|
|
50eec773bf | ||
|
|
35de13eebd | ||
|
|
3baa6b0063 | ||
|
|
6d462f762e | ||
|
|
802cce9a28 | ||
|
|
ee334cbcf8 | ||
|
|
c561a3749c | ||
|
|
f8b56736d7 | ||
|
|
7bcd46b145 | ||
|
|
d905592115 | ||
|
|
4b8dec8901 | ||
|
|
379eaec8e0 | ||
|
|
ae2d650516 | ||
|
|
4a83e79923 | ||
|
|
eb00c1991c | ||
|
|
462798dc2d | ||
|
|
1efd5781f8 | ||
|
|
08c6deba12 | ||
|
|
531848366d | ||
|
|
91abcd49e1 | ||
|
|
770c404ebe | ||
|
|
99217433b1 | ||
|
|
bccf7ac97a | ||
|
|
ed8be34b20 | ||
|
|
b6bb8382e7 | ||
|
|
893e0061a3 | ||
|
|
58efc2680e | ||
|
|
8180905890 | ||
|
|
db2be093b5 | ||
|
|
a688878236 | ||
|
|
75d4131aa6 | ||
|
|
66f84c5903 | ||
|
|
5d9b169380 | ||
|
|
eb908c696f | ||
|
|
21edcc86ce | ||
|
|
6f46233ad7 | ||
|
|
018803473c | ||
|
|
a4880f0372 | ||
|
|
daad7c317d | ||
|
|
20f2e894c9 | ||
|
|
5a98f0c26d | ||
|
|
4680e62d7b | ||
|
|
cd83ebec00 | ||
|
|
e40b7351ec | ||
|
|
3e249724fd | ||
|
|
1b453b6064 | ||
|
|
3611929f01 | ||
|
|
f8cf1375d5 | ||
|
|
ee4d94feab | ||
|
|
4e11d80eb7 | ||
|
|
84d228a3b5 | ||
|
|
f162c21d1b | ||
|
|
e613ed4dd2 | ||
|
|
36ce510547 | ||
|
|
22e2cecaee | ||
|
|
0a92513108 | ||
|
|
9309ca3107 | ||
|
|
8ff8b7ae49 | ||
|
|
68f668951f | ||
|
|
7e081500f3 | ||
|
|
2666f1e830 | ||
|
|
03f73d8cde | ||
|
|
af6cabe185 | ||
|
|
e7a8e99f3f | ||
|
|
0fda50b372 | ||
|
|
76ad00a018 | ||
|
|
e9da6f635e | ||
|
|
e02ac85388 | ||
|
|
3dd5152aba | ||
|
|
0ea2c5146a | ||
|
|
612726f87e | ||
|
|
4d1f8dc86b | ||
|
|
1277dfd941 | ||
|
|
a645953330 | ||
|
|
fa0f1dc0fa | ||
|
|
5e1b1ea982 | ||
|
|
f39de46d9f | ||
|
|
95ccde5a2d | ||
|
|
b8fa8a0729 | ||
|
|
ae9dd77749 | ||
|
|
66d6d3506f | ||
|
|
357c0fdbed | ||
|
|
a93c7e6343 | ||
|
|
65c61c1d63 | ||
|
|
2770a143d0 | ||
|
|
c649da1944 | ||
|
|
9d450cd9ff | ||
|
|
7491e92f2a | ||
|
|
3add75ac4d | ||
|
|
278b6c59e6 | ||
|
|
ff8ed02b81 | ||
|
|
cfa026a068 | ||
|
|
a69d4ec1dd | ||
|
|
b45c3e6e73 | ||
|
|
b1f7eb5899 | ||
|
|
8bf457d6b7 | ||
|
|
8c3c133342 | ||
|
|
ced5fd366e | ||
|
|
8c88bc7d64 | ||
|
|
5024f3fef9 | ||
|
|
1b1f82b749 | ||
|
|
f75c99bda8 | ||
|
|
7c0e45c844 | ||
|
|
ace10b648c | ||
|
|
8a8eb8c728 | ||
|
|
3bd6f07f9b | ||
|
|
d6a710e40e | ||
|
|
675837c232 | ||
|
|
243a309bc1 | ||
|
|
60943c0596 | ||
|
|
a8d1363581 | ||
|
|
57912017fe | ||
|
|
f542a361a4 | ||
|
|
70fca4ab9c | ||
|
|
81dcc985fc | ||
|
|
9a61a70a19 | ||
|
|
8c9d0f4666 | ||
|
|
fd7fc5fb8e | ||
|
|
65a0518008 | ||
|
|
7f29497262 | ||
|
|
6d2a32f0dd | ||
|
|
74f185e22f | ||
|
|
8db43f6bff | ||
|
|
4694fee383 | ||
|
|
2d8d1f3357 | ||
|
|
d462550f07 | ||
|
|
c9a2d176d1 | ||
|
|
8eb1633dd8 | ||
|
|
2c44add128 | ||
|
|
62393669dc | ||
|
|
abf2fbc369 | ||
|
|
72085dec92 | ||
|
|
923194c5b2 | ||
|
|
5964f545cf | ||
|
|
8387808d6e | ||
|
|
2cd38fcb03 | ||
|
|
9533e739ff | ||
|
|
6c9b60700e | ||
|
|
7908bad846 | ||
|
|
bfc7306ed7 | ||
|
|
63d561de0c | ||
|
|
3175e6c1eb | ||
|
|
eccb07f726 | ||
|
|
459d82012b | ||
|
|
5717b9a42c | ||
|
|
d7bc982142 | ||
|
|
d30d6a39ab | ||
|
|
46220a1a4a | ||
|
|
c2570f77e6 | ||
|
|
b4d33c3c96 | ||
|
|
8c676e28de | ||
|
|
a2c66fc236 | ||
|
|
456a063e35 | ||
|
|
269c81b1d2 | ||
|
|
307ecd7d6a | ||
|
|
41710698ee | ||
|
|
8b9197ff01 | ||
|
|
89d4a49c7e | ||
|
|
ca5087201b | ||
|
|
8ad2522cda | ||
|
|
5866877d3f | ||
|
|
92905843a4 | ||
|
|
a324e26d54 | ||
|
|
79bbc2de6e | ||
|
|
721374d42c | ||
|
|
0ab61665ee | ||
|
|
6ac3e01da0 | ||
|
|
fd3d667cde | ||
|
|
f9b5a5cbf4 | ||
|
|
12cd9fae46 | ||
|
|
531a07ccfc | ||
|
|
e72d06929e | ||
|
|
39d56792ce | ||
|
|
3994639b2b | ||
|
|
03f5746e96 | ||
|
|
070ac54067 | ||
|
|
e452604224 | ||
|
|
e836d3f690 | ||
|
|
51fdf38c71 | ||
|
|
d0e81532af | ||
|
|
4a2ea2735b | ||
|
|
6d3940b4e5 | ||
|
|
254dafcb12 | ||
|
|
33e412b59b | ||
|
|
5ce97cd0e9 | ||
|
|
1140df0f91 | ||
|
|
ed6fa9c8ea | ||
|
|
1f2ed3e4cb | ||
|
|
cc0565112f | ||
|
|
0fb7730b0c | ||
|
|
d6d1004070 | ||
|
|
c7eeb5349e | ||
|
|
257a38f68f | ||
|
|
66958def93 | ||
|
|
c692eb03fb | ||
|
|
a33669ca6d | ||
|
|
6cb17bb94b | ||
|
|
00c2fce11a | ||
|
|
8995aa71f3 | ||
|
|
b32c80d0bb | ||
|
|
c28fa6b00c | ||
|
|
e041d6de8a | ||
|
|
7cdf6f1cc8 | ||
|
|
fb6a942954 | ||
|
|
ca74e2901d | ||
|
|
60ca33fecb | ||
|
|
fb6f00b90b | ||
|
|
dac4888f61 | ||
|
|
50faedad80 | ||
|
|
d5db73dbbf | ||
|
|
6da0d42e8d | ||
|
|
7060f55fdf | ||
|
|
2b729aaadc | ||
|
|
fa482cfd46 | ||
|
|
3972d847e6 | ||
|
|
82e0b04e4d | ||
|
|
7560af0955 | ||
|
|
5b6bc27cbe | ||
|
|
aeacdd9630 | ||
|
|
250cfe2b7f | ||
|
|
c43a282322 | ||
|
|
d090411207 | ||
|
|
077198f1a5 | ||
|
|
bccd31e327 | ||
|
|
cf2fb7cd3a | ||
|
|
f95489b8f2 | ||
|
|
961f71b971 | ||
|
|
547aca5af4 | ||
|
|
860f74b3c2 | ||
|
|
a86046b654 | ||
|
|
c061df8f06 | ||
|
|
a2e97dc936 | ||
|
|
e3e2857424 | ||
|
|
4f96a523da | ||
|
|
9e4f374811 | ||
|
|
7a0e274b20 | ||
|
|
2010cec78e | ||
|
|
0346b366ee | ||
|
|
ee06770742 | ||
|
|
a678ca3b60 | ||
|
|
36b56ad3a2 | ||
|
|
e14e1e3613 | ||
|
|
d17fc9aab3 | ||
|
|
5bf3b7f812 | ||
|
|
5de8f47138 | ||
|
|
4437583e3f | ||
|
|
76f3f9f17c | ||
|
|
1cb0630529 | ||
|
|
7aa8b6646e | ||
|
|
245dd5190e | ||
|
|
2f619f3ebd | ||
|
|
b658b4c8ef | ||
|
|
b621258ce7 | ||
|
|
827c0ef87b | ||
|
|
6b6f8967ac | ||
|
|
21be7f2908 | ||
|
|
f0fd11791c | ||
|
|
f6586ba8b4 | ||
|
|
63d5ed625d | ||
|
|
c2ca1677c8 | ||
|
|
b8479ebcb5 | ||
|
|
89fdc587e0 | ||
|
|
cf1fa981ef | ||
|
|
cef28ed3a0 | ||
|
|
205643ea14 | ||
|
|
fa58c5452a | ||
|
|
22c0e743f9 | ||
|
|
f4178f0472 | ||
|
|
e2b886bb64 | ||
|
|
a7bc40452f | ||
|
|
0aa40b8b21 | ||
|
|
dce9f0c9d5 | ||
|
|
b6805513f6 | ||
|
|
e0cace8a4d | ||
|
|
8d794da86c | ||
|
|
78b16ebcfa | ||
|
|
173b86de45 | ||
|
|
30d0bbca93 | ||
|
|
a9c8d300a5 | ||
|
|
6edc320b69 | ||
|
|
6ae19be4b7 | ||
|
|
6df421ad72 | ||
|
|
1e690a9e78 | ||
|
|
f05079bf98 | ||
|
|
d39e014533 | ||
|
|
75f5f4d88f | ||
|
|
a70f6e2d84 | ||
|
|
561ee5077e | ||
|
|
a9cc28f3b1 | ||
|
|
0b3090973e | ||
|
|
fba329438f | ||
|
|
c0b161602d | ||
|
|
2c83d2dbf6 | ||
|
|
5c8186f618 | ||
|
|
aecd72fa5c | ||
|
|
8756b6507b | ||
|
|
4b3caa5c18 | ||
|
|
da7da2fe75 | ||
|
|
e442b31203 | ||
|
|
ed6d680c70 | ||
|
|
ea709c2945 | ||
|
|
e7debe1a2c | ||
|
|
8bf3029d01 | ||
|
|
76bc617db9 | ||
|
|
cbaa117b8a | ||
|
|
ef52c8d507 | ||
|
|
f6fca1059d | ||
|
|
26d679e257 | ||
|
|
a14ad9bdd6 | ||
|
|
5388b8cd4b | ||
|
|
99427e741c | ||
|
|
33d029c1b0 | ||
|
|
949d62cc12 | ||
|
|
ef5b47b4f7 | ||
|
|
26c148510e | ||
|
|
1a6ce9436f | ||
|
|
1f0a5a9576 | ||
|
|
806939605a | ||
|
|
bd5f8f1b23 | ||
|
|
2c47ec7262 | ||
|
|
cff538114d | ||
|
|
1c8fc29c4a | ||
|
|
22fda7cedc | ||
|
|
b3674708d4 | ||
|
|
7a62c5d489 | ||
|
|
959cd272f7 | ||
|
|
8cd2b4ffd3 | ||
|
|
d5bdaf0c88 | ||
|
|
8f6e97633c | ||
|
|
4bd0ed3e83 | ||
|
|
6ede03c8ce | ||
|
|
dd6a97adfa | ||
|
|
ccf89378f8 | ||
|
|
8417d2a031 | ||
|
|
28dad7fca1 | ||
|
|
e94c364d65 | ||
|
|
29b854cf98 |
@@ -0,0 +1,2 @@
|
||||
# Ignore blame for commit that moved protolude files under src/protolude
|
||||
d4949c633e8172d0e4dd8f5c991eaaae6b48fbb0
|
||||
@@ -1,5 +0,0 @@
|
||||
# TODO: Remove this once a new actionlint release has been cut
|
||||
# and made its way to us through nixpkgs.
|
||||
self-hosted-runner:
|
||||
labels:
|
||||
- ubuntu-24.04-arm
|
||||
@@ -112,7 +112,7 @@ runs:
|
||||
echo "artifacts=${artifacts}" >> "$GITHUB_OUTPUT"
|
||||
- name: Save artifact to GitHub Actions
|
||||
if: steps.find-task.outputs.task_found
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ inputs.upload }}
|
||||
path: ${{ steps.download.outputs.artifacts }}
|
||||
|
||||
@@ -8,7 +8,6 @@ inputs:
|
||||
required: true
|
||||
save-prs:
|
||||
description: Whether to additionally store the cache in a pull request, too. Should only be used for very small caches.
|
||||
type: boolean
|
||||
prefix:
|
||||
description: Cache key prefix to be used in both primary key and restore-keys.
|
||||
required: true
|
||||
@@ -19,17 +18,17 @@ inputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ inputs.prefix }}-
|
||||
- uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}-
|
||||
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ inputs.prefix }}-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}-
|
||||
|
||||
@@ -16,7 +16,7 @@ runs:
|
||||
nix_conf: |-
|
||||
always-allow-substitutes = true
|
||||
max-jobs = auto
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
|
||||
with:
|
||||
name: postgrest
|
||||
authToken: ${{ inputs.authToken }}
|
||||
|
||||
@@ -4,6 +4,9 @@ codecov:
|
||||
|
||||
comment: false
|
||||
|
||||
github_checks:
|
||||
annotations: true
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"matchBaseBranches": [
|
||||
"/^v[0-9]+/"
|
||||
],
|
||||
"matchManagers": [
|
||||
"haskell-cabal"
|
||||
],
|
||||
@@ -26,44 +23,6 @@
|
||||
"/^v[0-9]+/"
|
||||
],
|
||||
"groupName": "all dependencies"
|
||||
},
|
||||
{
|
||||
"matchManagers": [
|
||||
"haskell-cabal"
|
||||
],
|
||||
"matchPackageNames": [
|
||||
"base",
|
||||
"bytestring",
|
||||
"containers",
|
||||
"directory",
|
||||
"mtl",
|
||||
"parsec",
|
||||
"process",
|
||||
"text"
|
||||
],
|
||||
"groupName": "GHC dependencies"
|
||||
},
|
||||
{
|
||||
"matchManagers": [
|
||||
"haskell-cabal"
|
||||
],
|
||||
"matchPackageNames": [
|
||||
"hasql",
|
||||
"hasql-dynamic-statements",
|
||||
"hasql-notifications",
|
||||
"hasql-transaction",
|
||||
"hasql-pool"
|
||||
],
|
||||
"groupName": "hasql"
|
||||
},
|
||||
{
|
||||
"matchManagers": [
|
||||
"haskell-cabal"
|
||||
],
|
||||
"matchPackageNames": [
|
||||
"fuzzyset"
|
||||
],
|
||||
"allowedVersions": "<0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
# This actions creates the github token using the postgrest app secrets
|
||||
- name: Create Github App Token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ vars.POSTGREST_CI_APP_ID }}
|
||||
private-key: ${{ secrets.POSTGREST_CI_PRIVATE_KEY }}
|
||||
@@ -38,14 +38,14 @@ jobs:
|
||||
|
||||
# This is required for backport action to cherry-pick the PR
|
||||
- name: Fetch PR ref
|
||||
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
# Backport action that creates the PR with given settings
|
||||
- name: Create backport PR
|
||||
uses: korthout/backport-action@d07416681cab29bf2661702f925f020aaa962997 # v3.4.1
|
||||
uses: korthout/backport-action@7c3f6cd5843cac11bc59a04a1b7699af93261670 # v4.5
|
||||
with:
|
||||
github_token: ${{ steps.app-token.outputs.token }}
|
||||
pull_description: 'Backport for #${pull_number}.'
|
||||
|
||||
@@ -16,6 +16,7 @@ on:
|
||||
- .github/*
|
||||
- '*.nix'
|
||||
- nix/**
|
||||
- flake.lock
|
||||
- .cirrus.yml
|
||||
- cabal.project*
|
||||
- postgrest.cabal
|
||||
@@ -30,10 +31,20 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
static:
|
||||
name: Nix - Linux x86-64 static
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Linux aarch64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
artifact: aarch64
|
||||
- name: Linux x86-64
|
||||
runs-on: ubuntu-24.04
|
||||
artifact: x86-64
|
||||
name: Nix - ${{ matrix.name }} static
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -42,19 +53,19 @@ jobs:
|
||||
- name: Build static executable
|
||||
run: nix-build -A postgrestStatic -A postgrestStatic.tests
|
||||
- name: Save built executable as artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: postgrest-linux-static-x86-64
|
||||
name: postgrest-linux-static-${{ matrix.artifact }}
|
||||
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
|
||||
run: nix-build -A docker.image --out-link postgrest-docker-${{ matrix.artifact }}.tar.gz
|
||||
- name: Save built Docker image as artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: postgrest-docker-x86-64
|
||||
path: postgrest-docker.tar.gz
|
||||
name: postgrest-docker-${{ matrix.artifact }}
|
||||
path: postgrest-docker-${{ matrix.artifact }}.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
@@ -62,24 +73,19 @@ jobs:
|
||||
name: Nix - MacOS
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
- name: Install gnu sed
|
||||
run: brew install gnu-sed
|
||||
- name: Install nix-build-uncached
|
||||
run: nix-env -f default.nix -iA nix-build-uncached
|
||||
|
||||
- name: Build everything
|
||||
run: |
|
||||
# The --dry-run will give us a list of derivations to download from cachix and
|
||||
# derivations to build. We only take those that would have to be built and then build
|
||||
# those explicitly. This has the advantage that pure verification will not include
|
||||
# a download anymore, making it much faster. If something needs to be built, only
|
||||
# the dependencies required to do so will be downloaded, but not everything.
|
||||
nix-build --dry-run 2>&1 \
|
||||
| gsed -e '1,/derivations will be built:$/d' -e '/paths will be fetched/Q' \
|
||||
| xargs nix-build
|
||||
- name: Build everything (default.nix)
|
||||
run: nix-build-uncached
|
||||
|
||||
- name: Build everything (shell.nix)
|
||||
run: nix-build-uncached shell.nix
|
||||
|
||||
|
||||
stack:
|
||||
@@ -87,15 +93,6 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Linux aarch64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
cache: |
|
||||
~/.stack/pantry
|
||||
~/.stack/snapshots
|
||||
~/.stack/stack.sqlite3
|
||||
artifact: postgrest-ubuntu-aarch64
|
||||
deps: sudo apt-get update && sudo apt-get install libpq-dev
|
||||
|
||||
- name: MacOS aarch64
|
||||
runs-on: macos-14
|
||||
cache: |
|
||||
@@ -106,7 +103,7 @@ jobs:
|
||||
deps: brew link --force libpq
|
||||
|
||||
- name: MacOS x86-64
|
||||
runs-on: macos-13
|
||||
runs-on: macos-15-intel
|
||||
cache: |
|
||||
~/.stack/pantry
|
||||
~/.stack/snapshots
|
||||
@@ -126,11 +123,11 @@ jobs:
|
||||
name: Stack - ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: haskell-actions/setup@82e8b5066385702e477d9dc98f287070493e8abd # v2.8.2
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
|
||||
with:
|
||||
# This must match the version in stack.yaml's resolver
|
||||
ghc-version: 9.6.7
|
||||
ghc-version: 9.10.3
|
||||
enable-stack: true
|
||||
stack-no-global: true
|
||||
stack-setup-ghc: true
|
||||
@@ -155,7 +152,7 @@ jobs:
|
||||
- name: Strip Executable
|
||||
run: strip result/postgrest*
|
||||
- name: Save built executable as artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ matrix.artifact }}
|
||||
path: |
|
||||
@@ -168,7 +165,7 @@ jobs:
|
||||
name: Stack - FreeBSD from CirrusCI
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: ./.github/actions/artifact-from-cirrus
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
@@ -180,13 +177,13 @@ jobs:
|
||||
cabal:
|
||||
strategy:
|
||||
matrix:
|
||||
ghc: ['9.6.7', '9.8.4']
|
||||
ghc: ['9.10.3', '9.12.3']
|
||||
fail-fast: false
|
||||
name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: haskell-actions/setup@82e8b5066385702e477d9dc98f287070493e8abd # v2.8.2
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
|
||||
with:
|
||||
ghc-version: ${{ matrix.ghc }}
|
||||
- name: Cache .cabal
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
name: Lint & Style
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
name: Commit
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 100 # fetch history (last 100 commits) instead of default shallow clone history, this is deemed enough for a PR history
|
||||
- name: Setup Nix Environment
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
- test
|
||||
- build
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}
|
||||
- name: Tag latest commit
|
||||
|
||||
@@ -14,6 +14,7 @@ on:
|
||||
- .github/actions/setup-nix/**
|
||||
- default.nix
|
||||
- nix/**
|
||||
- flake.lock
|
||||
- docs/**
|
||||
- '!**.md'
|
||||
|
||||
@@ -27,7 +28,7 @@ jobs:
|
||||
name: Build
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -41,7 +42,7 @@ jobs:
|
||||
name: Spellcheck
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
linkcheck:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Check the version to be released
|
||||
run: |
|
||||
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
echo "Relevant extract from CHANGELOG.md:"
|
||||
cat CHANGES.md
|
||||
- name: Save CHANGES.md as artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: release-changes
|
||||
path: CHANGES.md
|
||||
@@ -64,9 +64,9 @@ jobs:
|
||||
needs:
|
||||
- prepare
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: artifacts
|
||||
- name: Create release bundle with archives for all builds
|
||||
@@ -75,6 +75,9 @@ jobs:
|
||||
|
||||
mkdir -p release-bundle
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-linux-static-aarch64.tar.xz" \
|
||||
-C artifacts/postgrest-linux-static-aarch64 postgrest
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-linux-static-x86-64.tar.xz" \
|
||||
-C artifacts/postgrest-linux-static-x86-64 postgrest
|
||||
|
||||
@@ -87,14 +90,11 @@ jobs:
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x86-64.tar.xz" \
|
||||
-C artifacts/postgrest-freebsd-x86-64 postgrest
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-ubuntu-aarch64.tar.xz" \
|
||||
-C artifacts/postgrest-ubuntu-aarch64 postgrest
|
||||
|
||||
zip --junk-paths "release-bundle/postgrest-${GITHUB_REF_NAME}-windows-x86-64.zip" \
|
||||
artifacts/postgrest-windows-x86-64/postgrest.exe
|
||||
|
||||
- name: Save release bundle
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: release-bundle
|
||||
path: release-bundle
|
||||
@@ -138,50 +138,42 @@ jobs:
|
||||
env:
|
||||
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Download aarch64 Docker image
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: postgrest-docker-aarch64
|
||||
- name: Download x86-64 Docker image
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: postgrest-docker-x86-64
|
||||
- name: Download aarch64 binary
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: postgrest-ubuntu-aarch64
|
||||
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ vars.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASS }}
|
||||
- name: Build aarch64 Docker image
|
||||
run: |
|
||||
# This only pushes the image via digest, not a tag. This will not appear
|
||||
# in the image list on Docker Hub, yet. It will be later added to the main
|
||||
# tag's manifest.
|
||||
docker buildx build \
|
||||
-t "$DOCKER_REPO/postgrest" \
|
||||
--platform linux/arm64 \
|
||||
--output push-by-digest=true,type=image,push=true \
|
||||
--metadata-file metadata.json \
|
||||
.
|
||||
echo "SHA256_ARM=$(jq -r '."containerimage.digest"' metadata.json)" >> "$GITHUB_ENV"
|
||||
- name: Publish images on Docker Hub
|
||||
run: |
|
||||
docker load -i postgrest-docker.tar.gz
|
||||
docker load -i postgrest-docker-aarch64.tar.gz
|
||||
docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-arm64"
|
||||
docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-arm64"
|
||||
|
||||
docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
|
||||
docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
|
||||
docker buildx imagetools create --append \
|
||||
-t "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}" \
|
||||
"$DOCKER_REPO/postgrest@$SHA256_ARM"
|
||||
docker load -i postgrest-docker-x86-64.tar.gz
|
||||
docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-amd64"
|
||||
docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-amd64"
|
||||
|
||||
docker manifest create "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}" \
|
||||
"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-arm64" \
|
||||
"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-amd64"
|
||||
docker manifest push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
|
||||
|
||||
# Only tag 'latest' for full releases
|
||||
if [ "${GITHUB_REF_NAME}" != "devel" ]; then
|
||||
echo "Pushing to 'latest' tag for full release of ${GITHUB_REF_NAME} ..."
|
||||
docker tag postgrest:latest "$DOCKER_REPO"/postgrest:latest
|
||||
docker push "$DOCKER_REPO"/postgrest:latest
|
||||
docker buildx imagetools create --append \
|
||||
-t "$DOCKER_REPO/postgrest:latest" \
|
||||
"$DOCKER_REPO/postgrest@$SHA256_ARM"
|
||||
docker manifest create "$DOCKER_REPO/postgrest:latest" \
|
||||
"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-arm64" \
|
||||
"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-amd64"
|
||||
docker manifest push "$DOCKER_REPO/postgrest:latest"
|
||||
else
|
||||
echo "Skipping push to 'latest' tag for pre-release..."
|
||||
fi
|
||||
@@ -194,7 +186,7 @@ jobs:
|
||||
vars.DOCKER_REPO && vars.DOCKER_USER &&
|
||||
github.ref == 'refs/tags/devel'
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
|
||||
with:
|
||||
username: ${{ vars.DOCKER_USER }}
|
||||
|
||||
+19
-14
@@ -17,6 +17,7 @@ on:
|
||||
- .github/actions/setup-nix/**
|
||||
- default.nix
|
||||
- nix/**
|
||||
- flake.lock
|
||||
- .stylish-haskell.yaml
|
||||
- cabal.project
|
||||
- postgrest.cabal
|
||||
@@ -39,7 +40,7 @@ jobs:
|
||||
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -48,10 +49,10 @@ jobs:
|
||||
|
||||
- run: postgrest-cabal-update
|
||||
|
||||
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
|
||||
- name: Run coverage (IO tests and Spec tests against latest supported PostgreSQL)
|
||||
run: postgrest-coverage
|
||||
- name: Upload coverage to codecov
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
with:
|
||||
files: ./coverage/codecov.json
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -69,7 +70,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pgVersion: [13, 14, 15, 16, 17]
|
||||
pgVersion: [14, 15, 16, 17, 18]
|
||||
name: PG ${{ matrix.pgVersion }}
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
@@ -78,33 +79,37 @@ jobs:
|
||||
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
tools: tests.testSpec.bin tests.testIO.bin tests.testBigSchema.bin withTools.postgresql-${{ matrix.pgVersion }}.bin cabalTools.update.bin
|
||||
tools: tests.testSpec.bin tests.testObservability.bin tests.testIO.bin tests.testBigSchema.bin withTools.pg-${{ matrix.pgVersion }}.bin cabalTools.update.bin
|
||||
|
||||
- run: postgrest-cabal-update
|
||||
|
||||
- name: Run spec tests
|
||||
if: always()
|
||||
run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-spec
|
||||
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-spec
|
||||
|
||||
- name: Run observability tests
|
||||
if: always()
|
||||
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-observability
|
||||
|
||||
- name: Run IO tests
|
||||
if: always()
|
||||
run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-io -vv
|
||||
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-io -vv
|
||||
|
||||
- name: Run IO tests on a big schema
|
||||
if: always()
|
||||
run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-big-schema -vv
|
||||
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-big-schema -vv
|
||||
|
||||
|
||||
memory:
|
||||
name: Memory
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -120,11 +125,11 @@ jobs:
|
||||
loadtest:
|
||||
strategy:
|
||||
matrix:
|
||||
kind: ['mixed', 'jwt-hs', 'jwt-hs-cache', 'jwt-hs-cache-worst', 'jwt-rsa', 'jwt-rsa-cache', 'jwt-rsa-cache-worst']
|
||||
kind: ['mixed', 'errors', 'jwt-hs', 'jwt-hs-cache', 'jwt-hs-cache-worst', 'jwt-rsa', 'jwt-rsa-cache', 'jwt-rsa-cache-worst']
|
||||
name: Loadtest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nix Environment
|
||||
@@ -152,14 +157,14 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runs-on:
|
||||
- macos-13 # x86_64-darwin
|
||||
- macos-15-intel # x86_64-darwin
|
||||
- macos-14 # aarch64-darwin
|
||||
- ubuntu-24.04 # x86_64-linux
|
||||
- ubuntu-24.04-arm # aarch64-linux
|
||||
name: Flake Check
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nix Environment
|
||||
|
||||
@@ -26,3 +26,4 @@ loadtest
|
||||
.docs-build
|
||||
gen_targets.http
|
||||
gen_jwk.json
|
||||
gen_private.json
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@ python:
|
||||
build:
|
||||
os: ubuntu-24.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
python: "3.12"
|
||||
|
||||
+13
-8
@@ -13,26 +13,26 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/neon.jpg">
|
||||
<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.svg">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/tembo.png">
|
||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||
<img width="296px" src="static/euronodes.svg">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||
<img width="296px" src="static/euronodes.svg">
|
||||
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/neon.jpg">
|
||||
</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 href="https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/bytebase.svg">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -93,6 +93,11 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
||||
<img width="222px" src="static/code-build.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/tembo.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
+106
-2
@@ -4,6 +4,110 @@ All notable changes to this project will be documented in this file. From versio
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
|
||||
- Add string slicing operator for `jwt-role-claim-key` by @taimoorzaeem in #4599
|
||||
- Optimize requests with `Prefer: count=exact` that do not use ranges or `db-max-rows` by @laurenceisla in #3957
|
||||
+ Removed unnecessary double count when building the `Content-Range`.
|
||||
- Add config `client-error-verbosity` to customize error verbosity by @taimoorzaeem in #4088, #3980, #3824
|
||||
- Add `Vary` header to responses by @develop7 in #4609
|
||||
- Add config `db-timezone-enabled` for optional querying of timezones by @taimoorzaeem in #4751
|
||||
- Log schema cache queries timings on `log-level=debug` by @steve-chavez in #4805
|
||||
|
||||
### Fixed
|
||||
|
||||
- Shutdown should wait for in flight requests by @mkleczek in #4702
|
||||
- Fix login with uppercase and mixed case role names by @taimoorzaeem in #4678
|
||||
- Remove automatic transaction retries on `40001 (serialization_failure)` errors to prevent replication lag by @laurenceisla in #3673
|
||||
- Fix unexpected results when embedding and filtering the same table more than once by @laurenceisla in #4075
|
||||
|
||||
### Changed
|
||||
|
||||
- Drop support for PostgreSQL EOL version 13 by @wolfgangwalther in #4193
|
||||
- All responses now include a `Vary` header by @develop7 in #4609
|
||||
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
|
||||
+ Now fails at startup. Prior to this, it failed with `PGRST205` on requests related to these schemas.
|
||||
- Build a static executable for aarch64-linux by @wolfgangwalther in #4193
|
||||
- Build the minimal docker image for aarch64-linux by @wolfgangwalther in #4193
|
||||
- The name of an embedded table can no longer be used in filters if it has an alias by @laurenceisla in #4075
|
||||
+ e.g. `?select=alias:table(*)&table.id=eq.1` is not possible anymore, use `?select=alias:table(*)&alias.id=eq.1` instead.
|
||||
|
||||
## [14.10] - 2026-04-16
|
||||
|
||||
### Added
|
||||
|
||||
- Log when the pool is released during schema cache reload on `log-level=debug` by @mkleczek in #4668
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix unnecessary connection pool flushes during schema cache reloading by @mkleczek in #4645
|
||||
|
||||
## [14.9] - 2026-04-10
|
||||
|
||||
### Added
|
||||
|
||||
- Log host, port and pg version of listener database connection by @mkleczek in #4617 #4618
|
||||
|
||||
### Fixed
|
||||
|
||||
- Remove red herring warp logs on default log-level, only emit them on `log-level=debug` by @steve-chavez in #4799
|
||||
|
||||
## [14.8] - 2026-04-03
|
||||
|
||||
### Added
|
||||
|
||||
- Log a `HINT` when the LISTEN channel stops working due to a PostgreSQL bug by @laurenceisla in #4581
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix invalid OpenAPI 2.0 format for integer types (`smallint`, `integer`, `bigint`) by @arturbent0 in #4641
|
||||
|
||||
## [14.7] - 2026-03-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix not logging SIGTERM and SIGINT by @steve-chavez in #4728
|
||||
|
||||
## [14.6] - 2026-03-06
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix leaking table and function names when calculating error hint by @taimoorzaeem in #4675
|
||||
|
||||
## [14.5] - 2026-02-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't hide async exceptions in logs by @stevechavez in #4646
|
||||
|
||||
## [14.4] - 2026-01-29
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure Listener connections are released by @mkleczek in #4614
|
||||
- Fix incorrectly filtering the returned representation for PATCH requests when using `or/and` filters by @laurenceisla in #3707
|
||||
- Fix listener running with exception masked after first failure by @mkleczek in #4615
|
||||
|
||||
## [14.3] - 2026-01-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix performance and high memory usage of relation hint calculation by @mkleczek in #4462, #4463
|
||||
|
||||
## [14.2] - 2025-12-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix `hasSingleUnnamedParam` incorrectly matching functions with named parameters by @joelonsql in #4553
|
||||
+ Functions with a single named parameter (e.g., `foo(data json)`) no longer incorrectly match the single-param fallback, returning a clean `PGRST202` error instead of a confusing PostgreSQL `42883` error.
|
||||
- Fix misleading logs on unsupported PostgreSQL versions by @taimoorzaeem in #4519
|
||||
- Fix regression where the `PGRST103` error response was truncated by @laurenceisla in #4455
|
||||
+ Happened when an `offset` was greater than the rows requested and `Prefer: count=exact` was sent.
|
||||
- Fix not returning `Content-Length` on empty HTTP `201` responses by @laurenceisla in #4518
|
||||
- Fix inaccurate Server-Timing header durations by @steve-chavez in #4522
|
||||
- Fix inaccurate "Schema cache queried" logs by @steve-chavez in #4522
|
||||
|
||||
## [14.1] - 2025-11-05
|
||||
|
||||
## Fixed
|
||||
@@ -587,7 +691,7 @@ All notable changes to this project will be documented in this file. From versio
|
||||
### Added
|
||||
|
||||
- #1933, #2109, Add a minimal health check endpoint - @steve-chavez
|
||||
+ For enabling this, the `admin-server-port` config must be set explictly
|
||||
+ For enabling this, the `admin-server-port` config must be set explicitly
|
||||
+ 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
|
||||
@@ -1070,7 +1174,7 @@ All notable changes to this project will be documented in this file. From versio
|
||||
- Customize content negotiation per route - @begriffs
|
||||
- Allow using nulls order without explicit order direction - @steve-chavez
|
||||
- Fatal error on postgres unsupported version, format supported version in error message - @steve-chavez
|
||||
- Prevent database memory cosumption by prepared statements caches - @ruslantalpa
|
||||
- Prevent database memory consumption by prepared statements caches - @ruslantalpa
|
||||
- Use specific columns in the RETURNING section - @ruslantalpa
|
||||
- Fix columns alias for RETURNING - @steve-chavez
|
||||
|
||||
|
||||
+21
-14
@@ -1,17 +1,12 @@
|
||||
# Contributing to PostgREST
|
||||
|
||||
**First:** if you're unsure or afraid of _anything_, just ask or
|
||||
submit the issue or pull request anyways. You won't be yelled at
|
||||
for giving your best effort. The worst that can happen is that
|
||||
you'll be politely asked to change something. We appreciate any
|
||||
sort of contributions, and don't want a wall of rules to get in the
|
||||
way of that.
|
||||
## AI Policy
|
||||
|
||||
However, for those individuals who want a bit more guidance on the
|
||||
best way to contribute to the project, read on. This document will
|
||||
cover what we're looking for. By addressing all the points we're
|
||||
looking for, it raises the chances we can quickly merge or address
|
||||
your contributions.
|
||||
We adhere to [Gentoo's AI policy](https://wiki.gentoo.org/wiki/Project:Council/AI_policy):
|
||||
|
||||
> It is expressly forbidden to contribute [...] any content that has been created with the assistance of Natural Language Processing artificial intelligence tools. This motion can be revisited, should a case been made over such a tool that does not pose copyright, ethical and quality concerns.
|
||||
|
||||
You can find more about its rationale [here](https://wiki.gentoo.org/wiki/Project:Council/AI_policy#Rationale).
|
||||
|
||||
## Issues
|
||||
|
||||
@@ -40,12 +35,12 @@ For questions on how to use PostgREST, please use
|
||||
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/) and [stylish-haskell](https://github.com/jaspervdj/stylish-haskell)
|
||||
* All fixes or features must have a test proving the improvement.
|
||||
|
||||
* All code must also pass a [linter](http://community.haskell.org/~ndm/hlint/) and [styler](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.
|
||||
@@ -53,3 +48,15 @@ Check the [development docs](https://github.com/PostgREST/postgrest/blob/main/ni
|
||||
### Running Tests
|
||||
|
||||
For instructions on running tests, see the [development docs](https://github.com/PostgREST/postgrest/blob/main/nix/README.md#testing).
|
||||
|
||||
### Structuring commits in pull requests
|
||||
|
||||
To simplify reviews, make it easy to split pull requests if deemed necessary, and to maintain clean and meaningful history of changes, you will be asked to update your PR if it does not follow the below rules:
|
||||
|
||||
* It must be possible to merge the PR branch into target using `git merge --ff-only`, ie. the source branch must be rebased on top of target.
|
||||
* No merge commits in the source branch.
|
||||
* All commits in the source branch must be self contained, meaning: it should be possible to treat each commit as a separate PR.
|
||||
* Commits in the source branch must contain only related changes (related means the changes target a single problem/goal). For example, any refactorings should be isolated from the actual change implementation into separate commits.
|
||||
* Tests, documentation, and changelog updates should be contained in the same commits as the actual code changes they relate to. An exception to this rule is when test or documentation changes are made in separate PR.
|
||||
* Commit messages must be prefixed with one of the prefixes defined in [the list used by commit verification scripts](https://github.com/PostgREST/postgrest/blob/main/nix/tools/gitTools.nix#L11).
|
||||
* Commit messages should contain a longer description of the purpose of the changes contained in the commit and, for non-trivial changes, a description of the changes themselves.
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
# PostgREST Docker Hub image for aarch64.
|
||||
# The x86-64 is a single-static-binary image built via Nix, see:
|
||||
# nix/tools/docker/README.md
|
||||
|
||||
FROM ubuntu:noble@sha256:66460d557b25769b102175144d538d88219c077c678a49af4afca6fbfc1b5252 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
|
||||
RUN chmod +x /usr/bin/postgrest
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
USER 1000
|
||||
|
||||
# Use the array form to avoid running the command using bash, which does not handle `SIGTERM` properly.
|
||||
# See https://docs.docker.com/compose/faq/#why-do-my-services-take-10-seconds-to-recreate-or-stop
|
||||
CMD ["postgrest"]
|
||||
@@ -1,5 +1,4 @@
|
||||
Copyright (c) 2014 Joe Nelson
|
||||
Copyright (c) 2019 Steve Chavez
|
||||
Copyright (c) 2014-2026 The PostgREST contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
||||
@@ -22,26 +22,26 @@ API than you are likely to write from scratch.
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/neon.jpg">
|
||||
<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.svg">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/tembo.png">
|
||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||
<img width="296px" src="static/euronodes.svg">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||
<img width="296px" src="static/euronodes.svg">
|
||||
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/neon.jpg">
|
||||
</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 href="https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/bytebase.svg">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -125,7 +125,7 @@ and limited with - range headers. More about
|
||||
## Data Integrity
|
||||
|
||||
Rather than relying on an Object Relational Mapper and custom
|
||||
imperative coding, this system requires you put declarative constraints
|
||||
imperative coding, this system requires you to put declarative constraints
|
||||
directly into your database. Hence no application can corrupt your
|
||||
data (including your API server).
|
||||
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
packages: postgrest.cabal
|
||||
tests: true
|
||||
allow-newer:
|
||||
hasql:postgresql-libpq
|
||||
|
||||
@@ -1 +1 @@
|
||||
index-state: hackage.haskell.org 2025-10-29T04:02:18Z
|
||||
index-state: hackage.haskell.org 2026-04-18T18:42:36Z
|
||||
|
||||
+9
-7
@@ -1,6 +1,6 @@
|
||||
{ system ? builtins.currentSystem
|
||||
|
||||
, compiler ? "ghc948"
|
||||
, compiler ? "ghc9123"
|
||||
|
||||
, # Commit of the Nixpkgs repository that we want to use.
|
||||
# It defaults to reading the inputs from flake.lock, which serves
|
||||
@@ -44,7 +44,6 @@ let
|
||||
allOverlays.checked-shell-script
|
||||
allOverlays.gitignore
|
||||
(allOverlays.haskell-packages { inherit compiler; })
|
||||
allOverlays.slocat
|
||||
];
|
||||
|
||||
# Evaluated expression of the Nixpkgs repository.
|
||||
@@ -53,11 +52,11 @@ let
|
||||
|
||||
postgresqlVersions =
|
||||
[
|
||||
{ name = "postgresql-17"; postgresql = pkgs.postgresql_17.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
{ name = "postgresql-16"; postgresql = pkgs.postgresql_16.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
{ name = "postgresql-15"; postgresql = pkgs.postgresql_15.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
{ name = "postgresql-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
{ name = "postgresql-13"; postgresql = pkgs.postgresql_13.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
{ name = "pg-18"; postgresql = pkgs.postgresql_18.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
{ name = "pg-17"; postgresql = pkgs.postgresql_17.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
{ name = "pg-16"; postgresql = pkgs.postgresql_16.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
{ name = "pg-15"; postgresql = pkgs.postgresql_15.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
{ name = "pg-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
];
|
||||
|
||||
haskellPackages = pkgs.haskell.packages."${compiler}";
|
||||
@@ -108,6 +107,9 @@ rec {
|
||||
inherit (pkgs.haskell.packages."${compiler}") ghcWithPackages;
|
||||
};
|
||||
|
||||
# Used by CI on MacOS
|
||||
inherit (pkgs) nix-build-uncached;
|
||||
|
||||
### Tools
|
||||
|
||||
cabalTools =
|
||||
|
||||
+8
-10
@@ -19,26 +19,26 @@ write from scratch.
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/neon.jpg">
|
||||
<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.svg">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/tembo.png">
|
||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/euronodes.svg">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/euronodes.svg">
|
||||
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/neon.jpg">
|
||||
</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 href="https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/bytebase.svg">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -61,5 +61,3 @@ The image is built from scratch using
|
||||
no commands are listed in the image history. See the [PostgREST
|
||||
repository](https://github.com/PostgREST/postgrest/tree/main/nix/tools/docker) for
|
||||
details on the build process and how to inspect the image.
|
||||
|
||||
This does not apply to the arm64 variant, which is based on Ubuntu.
|
||||
|
||||
+4
-3
@@ -48,14 +48,14 @@ source_suffix = ".rst"
|
||||
# The master toctree document.
|
||||
master_doc = "index"
|
||||
|
||||
# This is overriden by readthedocs with the version tag anyway
|
||||
version = "14"
|
||||
# This is overridden by readthedocs with the version tag anyway
|
||||
version = "devel"
|
||||
# To avoid repetition in <title> we set this to an empty string.
|
||||
release = ""
|
||||
|
||||
# General information about the project.
|
||||
project = "PostgREST " + version
|
||||
author = "Joe Nelson, Steve Chavez"
|
||||
author = "The PostgREST contributors"
|
||||
copyright = "2017, " + author
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
@@ -302,6 +302,7 @@ linkcheck_ignore = [
|
||||
r"https://www.cybertec-postgresql.com/.*",
|
||||
# Odd SSL error
|
||||
r"https://www.dripdepot.com",
|
||||
r"https://www.euronodes.com",
|
||||
# New GitHub UI delays comment load, so anchor fails
|
||||
r"https://github.com/.*#issuecomment",
|
||||
# Random 500 Internal Server Error
|
||||
|
||||
+2
-1
@@ -6,7 +6,7 @@ Community Tutorials
|
||||
* `Building a Contacts List with PostgREST and Vue.js <https://www.youtube.com/watch?v=iHtsALtD5-U>`_ -
|
||||
In this video series, DigitalOcean shows how to build and deploy an Nginx + PostgREST(using a managed PostgreSQL database) + Vue.js webapp in an Ubuntu server droplet.
|
||||
|
||||
* `PostgREST + Auth0: Create REST API in mintutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
|
||||
* `PostgREST + Auth0: Create REST API in minutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest-1-introduction/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
|
||||
|
||||
* `"CodeLess" backend using postgres, postgrest and oauth2 authentication with keycloak <https://www.mathieupassenaud.fr/codeless_backend/>`_ -
|
||||
A step-by-step tutorial for using PostgREST with KeyCloak(hosted on a managed service).
|
||||
@@ -37,6 +37,7 @@ Example Apps
|
||||
* `archtika <https://github.com/thiloho/archtika>`_ - self-hosted CMS
|
||||
* `delibrium-postgrest <https://gitlab.com/delibrium/delibrium-postgrest/>`_ - example school API and front-end in Vue.js
|
||||
* `ETH-transactions-storage <https://github.com/Adamant-im/ETH-transactions-storage>`_ - indexer for Ethereum to get transaction list by ETH address
|
||||
* `fullstack template <https://github.com/jenstroeger/fullstack-webapp-template>`_ - a complete fullstack webapp template using PG as db and message queue, Python and Dramatiq to implement async jobs, db migrations, test runners, and more.
|
||||
* `general <https://github.com/PierreRochard/general>`_ - example auth back-end
|
||||
* `guild-operators <https://github.com/cardano-community/koios-artifacts/tree/main/files/grest>`_ - example queries and functions that the Cardano Community uses for their Guild Operators' Repository
|
||||
* `PostGUI <https://github.com/priyank-purohit/PostGUI>`_ - React Material UI admin panel
|
||||
|
||||
@@ -163,7 +163,7 @@ Another option is to define the function with the :code:`SECURITY DEFINER` optio
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- login as a user wich has privileges on the private schemas
|
||||
-- login as a user which has privileges on the private schemas
|
||||
|
||||
-- create a sample function
|
||||
create or replace function login(email text, pass text, out token text) as $$
|
||||
|
||||
@@ -16,7 +16,7 @@ Supported PostgreSQL versions
|
||||
=============================
|
||||
|
||||
=============== =================================
|
||||
**Supported** PostgreSQL >= 12
|
||||
**Supported** PostgreSQL >= 14
|
||||
=============== =================================
|
||||
|
||||
PostgREST works with all PostgreSQL versions still `officially supported <https://www.postgresql.org/support/versioning/>`_.
|
||||
@@ -146,7 +146,7 @@ To avoid having to install the database at all, you can run both it and the serv
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
PGRST_SERVER_HOST: localhost # necessary for `postgrest --ready` flag to work
|
||||
PGRST_SERVER_HOST: 0.0.0.0 # necessary for `postgrest --ready` flag to work
|
||||
PGRST_DB_URI: postgres://app_user:password@db:5432/app_db
|
||||
PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000
|
||||
depends_on:
|
||||
@@ -181,6 +181,23 @@ If you want to have a visual overview of your API in your browser you can add sw
|
||||
|
||||
With this you can see the swagger-ui in your browser on port 8080.
|
||||
|
||||
.. _docker_cpu_contraint:
|
||||
|
||||
Docker Resource Constraints
|
||||
---------------------------
|
||||
|
||||
PostgREST does not support ``--cpus`` `constraint option <https://docs.docker.com/engine/containers/resource_constraints/#configure-the-default-cfs-scheduler>`_.
|
||||
|
||||
As a workaround, you may use the `GHC RTS <https://ghc.gitlab.haskell.org/ghc/doc/users_guide/runtime_control.html#runtime-system-rts-options>`_ ``-N`` option. For instance, to limit it to 2 CPU cores, do:
|
||||
|
||||
.. code::
|
||||
|
||||
# Set environment variable GHCRTS set to "-N2"
|
||||
docker run --rm -p 3000:3000 \
|
||||
-e PGRST_DB_URI="postgres://app_user:password@10.0.0.10/postgres" \
|
||||
-e GHCRTS="-N2"
|
||||
postgrest/postgrest
|
||||
|
||||
.. _build_source:
|
||||
|
||||
Building from Source
|
||||
|
||||
+26
-20
@@ -38,6 +38,28 @@ Sponsors
|
||||
.. image:: ../static/cybertec.svg
|
||||
:target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest
|
||||
|
||||
.. container:: img-dark
|
||||
|
||||
.. image:: ../static/supabase-dark.svg
|
||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||
|
||||
.. container:: img-light
|
||||
|
||||
.. image:: ../static/supabase.svg
|
||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||
|
||||
.. container:: img-dark
|
||||
|
||||
.. image:: ../static/euronodes.svg
|
||||
:target: https://www.euronodes.com/postgrest
|
||||
|
||||
.. container:: img-light
|
||||
|
||||
.. image:: ../static/euronodes.svg
|
||||
:target: https://www.euronodes.com/postgrest
|
||||
|
||||
|
|
||||
|
||||
.. container:: img-dark
|
||||
|
||||
.. image:: ../static/neon-dark.jpg
|
||||
@@ -48,30 +70,15 @@ Sponsors
|
||||
.. image:: ../static/neon.jpg
|
||||
:target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||
|
||||
.. image:: ../static/tembo.png
|
||||
:target: https://www.tembo.io/?utm_source=sponsor&utm_campaign=postgrest
|
||||
|
||||
|
|
||||
|
||||
.. container:: img-dark
|
||||
|
||||
.. image:: ../static/euronodes.svg
|
||||
:target: https://www.euronodes.com/postgrest
|
||||
.. image:: ../static/bytebase-dark.svg
|
||||
:target: https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||
|
||||
.. container:: img-light
|
||||
|
||||
.. image:: ../static/euronodes.svg
|
||||
:target: https://www.euronodes.com/postgrest
|
||||
|
||||
.. container:: img-dark
|
||||
|
||||
.. image:: ../static/supabase-dark.png
|
||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||
|
||||
.. container:: img-light
|
||||
|
||||
.. image:: ../static/supabase.png
|
||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||
.. image:: ../static/bytebase.svg
|
||||
:target: https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||
|
||||
.. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround
|
||||
to create space and center the logos. It's not easy to layout with restructuredText.
|
||||
@@ -213,7 +220,6 @@ In Production
|
||||
Here are some companies that use PostgREST in production.
|
||||
|
||||
* `Catarse <https://www.catarse.me>`_
|
||||
* `Datrium <https://www.datrium.com>`_
|
||||
* `Drip Depot <https://www.dripdepot.com>`_
|
||||
* `Image-charts <https://www.image-charts.com>`_
|
||||
* `Netwo <https://www.netwo.io>`_
|
||||
|
||||
@@ -4,6 +4,7 @@ API's
|
||||
APIs
|
||||
APISIX
|
||||
AST
|
||||
async
|
||||
aud
|
||||
Auth
|
||||
auth
|
||||
@@ -14,6 +15,7 @@ BOM
|
||||
Bytea
|
||||
Cardano
|
||||
cd
|
||||
CDNs
|
||||
centric
|
||||
CLI
|
||||
CMS
|
||||
@@ -30,6 +32,7 @@ DDL
|
||||
DOM
|
||||
DSL
|
||||
DevOps
|
||||
Dramatiq
|
||||
dockerize
|
||||
enum
|
||||
Enums
|
||||
@@ -41,6 +44,7 @@ EveryLayout
|
||||
filename
|
||||
FreeBSD
|
||||
fts
|
||||
fullstack
|
||||
GeoJSON
|
||||
Github
|
||||
Google
|
||||
@@ -188,9 +192,12 @@ verifier
|
||||
versioning
|
||||
Vondra
|
||||
Vue
|
||||
webapp
|
||||
webhooks
|
||||
websearch
|
||||
Websockets
|
||||
webuser
|
||||
wfts
|
||||
www
|
||||
debouncing
|
||||
deduplicates
|
||||
@@ -21,6 +21,7 @@ PostgREST exposes three database objects of a schema as resources: tables, views
|
||||
api/aggregate_functions.rst
|
||||
api/openapi.rst
|
||||
api/preferences.rst
|
||||
api/vary_header.rst
|
||||
api/*
|
||||
|
||||
.. raw:: html
|
||||
|
||||
@@ -69,6 +69,26 @@ If the function doesn't modify the database, it will also run under the GET meth
|
||||
|
||||
The function parameter names match the JSON object keys in the POST case, for the GET case they match the query parameters ``?a=1&b=2``.
|
||||
|
||||
If the function is defined to have default values for the parameters then arguments for these parameters can be omitted in the request. For instance:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE FUNCTION greet_user(username TEXT DEFAULT 'guest')
|
||||
RETURNS TEXT AS $$
|
||||
SELECT 'Hello ' || username || '!';
|
||||
$$ LANGUAGE SQL IMMUTABLE;
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -i "http://localhost:3000/rpc/greet_user"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Context-Type: application/json; charset=utf-8
|
||||
|
||||
"Hello guest!"
|
||||
|
||||
.. _function_single_json:
|
||||
|
||||
Functions with an array of JSON objects
|
||||
|
||||
@@ -15,7 +15,7 @@ Using these domains, :ref:`functions <functions>` can become handlers and `user-
|
||||
|
||||
.. important::
|
||||
|
||||
- PostgREST vendor media types (``application/vnd.pgrst.plan``, ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``) cannot be overriden.
|
||||
- PostgREST vendor media types (``application/vnd.pgrst.plan``, ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``) cannot be overridden.
|
||||
- Long media types like ``application/vnd.openxmlformats-officedocument.wordprocessingml.document`` cannot be expressed as domains since they surpass `PostgreSQL identifier length <https://www.postgresql.org/docs/current/limits.html#LIMITS-TABLE>`_.
|
||||
For these you can use the :ref:`any_handler`.
|
||||
|
||||
|
||||
@@ -117,6 +117,10 @@ However, with ``handling=strict``, an invalid time zone preference will throw an
|
||||
|
||||
HTTP/1.1 400 Bad Request
|
||||
|
||||
.. note::
|
||||
|
||||
This feature requires querying `pg_timezone_names <https://www.postgresql.org/docs/current/view-pg-timezone-names.html>`_ during :ref:`schema_cache` load. If this is not desired, you can disable the feature with :ref:`db-timezone-enabled`.
|
||||
|
||||
.. _prefer_return:
|
||||
|
||||
Return Representation
|
||||
|
||||
@@ -1244,7 +1244,7 @@ You can order the correlated arrays explicitly. For example, to order by the fil
|
||||
|
||||
.. warning::
|
||||
|
||||
Aliasing spreaded columns is recommended since JSON allows duplicate keys. Example:
|
||||
Aliasing spread columns is recommended since JSON allows duplicate keys. Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ Schemas
|
||||
|
||||
PostgREST can expose a single or multiple schema's tables, views and functions. The :ref:`active database role <roles>` must have the usage privilege on the schemas to access them.
|
||||
|
||||
.. important::
|
||||
|
||||
``pg_catalog`` and ``information_schema`` are not allowed in :ref:`db-schemas`. This is done to prevent leaking sensitive information and hence they cannot be accessed directly. If you wish to expose objects of these schemas, expose another schema that contains wrapper views or functions over ``pg_catalog`` or ``information_schema`` objects.
|
||||
|
||||
Single schema
|
||||
-------------
|
||||
|
||||
|
||||
@@ -639,7 +639,7 @@ However, it can work with surrogate primary keys (e.g. ``id serial primary key``
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/employees?colums=id,name,salary" \
|
||||
curl "http://localhost:3000/employees?columns=id,name,salary" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-H "Prefer: resolution=merge-duplicates, missing=default" \
|
||||
-d @- << EOF
|
||||
|
||||
@@ -14,7 +14,7 @@ Custom Queries
|
||||
|
||||
The PostgREST URL grammar limits the kinds of queries clients can perform. It prevents arbitrary, potentially poorly constructed and slow client queries. It's good for quality of service, but means database administrators must create custom views and functions to provide richer endpoints. The most common causes for custom endpoints are
|
||||
|
||||
* Table unions
|
||||
* SET operators like `UNION, INTERSECT and EXCEPT <https://www.postgresql.org/docs/current/queries-union.html>`_.
|
||||
* More complicated joins than those provided by :ref:`resource_embedding`.
|
||||
* Geo-spatial queries that require an argument, like "points near (lat,lon)"
|
||||
|
||||
@@ -51,7 +51,7 @@ You can request table/columns with spaces in them by percent encoding the spaces
|
||||
Reserved characters
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If filters include PostgREST reserved characters(``,``, ``.``, ``:``, ``()``) you'll have to surround them in percent encoded double quotes ``%22`` for correct processing.
|
||||
If filters include PostgREST reserved characters(``,``, ``.``, ``:``, ``*``, ``(``, ``)``) you'll have to surround them in percent encoded double quotes ``%22`` for correct processing.
|
||||
|
||||
Here ``Hebdon,John`` and ``Williams,Mary`` are values.
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.. _vary_header:
|
||||
|
||||
Vary Header
|
||||
===========
|
||||
|
||||
In order to assist caching proxies and CDNs, PostgREST includes a ``Vary`` header of value
|
||||
``Accept, Prefer, Range`` in its responses which should fit most of the bills. As any other
|
||||
response header, it's available for override
|
||||
by updating ``response.headers`` GUC variable accordingly, for example:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- Override the Vary header to include Accept, Prefer and X-Test-Vary headers
|
||||
perform set_config('response.headers', '[{"Vary": "Accept, Prefer, X-Test-Vary"}]', true);
|
||||
|
||||
In this case PostgREST will use provided value verbatim.
|
||||
@@ -217,7 +217,7 @@ It's recommended to leave the JWT cache enabled as our load tests indicate ~20%
|
||||
|
||||
- If the ``jwt-secret`` is changed and the config is reloaded, the JWT cache will reset.
|
||||
- JWTs that pass :ref:`jwt_signature` are cached, regardless if they pass :ref:`jwt_claims_validation`. We do this to ensure responses stays fast under common failure cases (such as expired JWTs).
|
||||
- You can use the :ref:`server-timing_header` to see the peformance benefit of JWT caching.
|
||||
- You can use the :ref:`server-timing_header` to see the performance benefit of JWT caching.
|
||||
|
||||
.. _jwt_role_extract:
|
||||
|
||||
@@ -234,6 +234,17 @@ The DSL follows the `JSONPath <https://goessner.net/articles/JsonPath/>`_ expres
|
||||
- ``==^`` selects the first array element that ends with the right operand
|
||||
- ``*==`` selects the first array element that contains the right operand
|
||||
|
||||
The selected role value can also be sliced using the slice operator ``[a:b]``. It is similar to `slice operator in python <https://docs.python.org/3/library/functions.html#slice>`_. Negative index values are also supported. The syntax is as:
|
||||
|
||||
- ``[a:b]`` take slice from index ``a`` up to ``b``
|
||||
- ``[a:]`` take slice from index ``a`` to end
|
||||
- ``[:b]`` take slice from start to index ``b``
|
||||
- ``[:]`` select everything, no slicing
|
||||
|
||||
.. important::
|
||||
|
||||
Make sure that you are not taking a slice where the start index comes after the end index like ``[11:2]``. The result of this would be empty string and so no role would get selected.
|
||||
|
||||
Usage examples:
|
||||
|
||||
.. code:: bash
|
||||
@@ -255,6 +266,11 @@ Usage examples:
|
||||
jwt-role-claim-key = ".postgrest.roles[?(@ ==^ \"hor\")]"
|
||||
jwt-role-claim-key = ".postgrest.roles[?(@ *== \"utho\")]"
|
||||
|
||||
# {"postgrest":{"wlcg": ["/groupa", "/groupb/"]}}
|
||||
# skip the "/" character using slice operator
|
||||
jwt-role-claim-key = ".postgrest.wlcg[0][1:]"
|
||||
jwt-role-claim-key = ".postgrest.wlcg[1][1:-1]"
|
||||
|
||||
.. note::
|
||||
|
||||
The string comparison operators are implemented as a custom extension to the JSPath and does not strictly follow the `RFC 9535 <https://www.rfc-editor.org/rfc/rfc9535.html>`_.
|
||||
|
||||
@@ -195,6 +195,33 @@ app.settings.*
|
||||
|
||||
The :code:`current_setting` function has `an optional boolean second <https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SET>`_ argument to avoid it from raising an error if the value was not defined. Default values to :code:`app.settings` can then be given by combining this argument with :code:`coalesce` and :code:`nullif` : :code:`coalesce(nullif(current_setting('app.settings.my_custom_variable', true), ''), 'default value')`. The use of :code:`nullif` is necessary because if set in a transaction, the setting is sometimes not "rolled back" to :code:`null`. See also :ref:`this section <guc_req_headers_cookies_claims>` for more information on this behaviour.
|
||||
|
||||
.. _client-error-verbosity:
|
||||
|
||||
client-error-verbosity
|
||||
----------------------
|
||||
|
||||
=============== =======================
|
||||
**Type** String
|
||||
**Default** verbose
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_CLIENT_ERROR_VERBOSITY
|
||||
**In-Database** pgrst.client_error_verbosity
|
||||
=============== =======================
|
||||
|
||||
Specifies the verbosity of PostgREST errors. See :ref:`client_error_verbosity`.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
# Return error "code", "message", "details" and "hint"
|
||||
client-error-verbosity = "verbose"
|
||||
|
||||
# Return only "code" and "message"
|
||||
client-error-verbosity = "minimal"
|
||||
|
||||
.. note::
|
||||
|
||||
This setting only affects client side error messages. Server side logs are not affected by this setting.
|
||||
|
||||
.. _db-aggregates-enabled:
|
||||
|
||||
db-aggregates-enabled
|
||||
@@ -264,7 +291,7 @@ db-channel-enabled
|
||||
|
||||
When this is set to :code:`true`, the notification channel specified in :ref:`db-channel` is enabled.
|
||||
|
||||
You should set this to ``false`` when using PostgresSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.
|
||||
You should set this to ``false`` when using PostgreSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.
|
||||
|
||||
.. _db-config:
|
||||
|
||||
@@ -479,7 +506,7 @@ db-prepared-statements
|
||||
|
||||
When disabled, the generated queries will be parameterized (invulnerable to SQL injection) but they will not be prepared (cached in the database session). Not using prepared statements will noticeably decrease performance, so it's recommended to always have this setting enabled.
|
||||
|
||||
You should only set this to ``false`` when using PostgresSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.
|
||||
You should only set this to ``false`` when using PostgreSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.
|
||||
|
||||
.. _db-root-spec:
|
||||
|
||||
@@ -513,6 +540,21 @@ db-schemas
|
||||
|
||||
The list of database schemas to expose to clients. See :ref:`schemas`.
|
||||
|
||||
.. _db-timezone-enabled:
|
||||
|
||||
db-timezone-enabled
|
||||
-------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** Boolean
|
||||
**Default** True
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_TIMEZONE_ENABLED
|
||||
**In-Database** pgrst.db_timezone_enabled
|
||||
=============== =================================
|
||||
|
||||
Enables the use of :ref:`prefer_timezone` preference header. Disabled when set to ``false``.
|
||||
|
||||
.. _db-tx-end:
|
||||
|
||||
db-tx-end
|
||||
@@ -526,7 +568,7 @@ db-tx-end
|
||||
**In-Database** pgrst.db_tx_end
|
||||
=============== =================================
|
||||
|
||||
Specifies how to terminate the database transactions.
|
||||
Specifies how to terminate the database transactions. See :ref:`prefer_tx`.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
|
||||
@@ -473,3 +473,38 @@ For example, doing a request on a table with high count (say 30_000_000), we get
|
||||
Proxy-Status: PostgREST; error=57014
|
||||
|
||||
The PostgreSQL error code ``57014`` (`ref <https://www.postgresql.org/docs/current/errcodes-appendix.html>`_) reveals that the error is due to a short ``statement_timeout`` value.
|
||||
|
||||
.. _client_error_verbosity:
|
||||
|
||||
Client Error Verbosity
|
||||
======================
|
||||
|
||||
For HTTP clients, the error verbosity can be set via :ref:`client-error-verbosity` config.
|
||||
|
||||
With ``verbose``, it returns ``code``, ``message``, ``details`` and ``hint``.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl "localhost:3000/itemsxx"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"code": "PGRST205",
|
||||
"message": "Could not find the table 'public.itemsxx' in the schema cache",
|
||||
"details": "Perhaps you meant the table 'public.items'",
|
||||
"hint": null
|
||||
}
|
||||
|
||||
With ``minimal``, just ``code`` and ``message`` is returned.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl "localhost:3000/itemsxx"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"code": "PGRST205",
|
||||
"message": "Could not find the table 'public.itemsxx' in the schema cache"
|
||||
}
|
||||
|
||||
@@ -46,7 +46,9 @@ This will cause the :ref:`connection_pool` to connect to the read replica host a
|
||||
|
||||
.. note::
|
||||
|
||||
Under the hood, PostgREST forces `target_session_attrs=read-write <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS>`_ for the ``LISTEN`` session.
|
||||
- Under the hood, PostgREST forces `target_session_attrs=read-write <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS>`_ for the ``LISTEN`` session.
|
||||
So if you specify ``target_session_attrs=read-only`` as mentioned above, PostgREST will override it for the ``LISTEN``.
|
||||
- ``read-only`` is only available on libpq >= 14, if you use a lower version you will get an error like ``invalid target_session_attrs value: \"read-only\"``.
|
||||
|
||||
.. _listener_automatic_recovery:
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ Restart the database and watch the log file in real-time to understand how HTTP
|
||||
Metrics
|
||||
=======
|
||||
|
||||
The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in `Prometheus text format <https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format>`_.
|
||||
The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in `Prometheus text format <https://prometheus.io/docs/instrumenting/exposition_formats/#prometheus-text-format>`_.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
||||
@@ -3,10 +3,16 @@
|
||||
Schema Cache
|
||||
============
|
||||
|
||||
PostgREST requires metadata from the database schema to provide a REST API that abstracts SQL details. One example of this is the interface for :ref:`resource_embedding`.
|
||||
PostgREST requires metadata from the database to provide a REST API that abstracts SQL details. One example of this is the interface for :ref:`resource_embedding`.
|
||||
|
||||
Getting this metadata requires expensive queries. To avoid repeating this work, PostgREST uses a schema cache.
|
||||
|
||||
.. note::
|
||||
|
||||
- Schema cache queries have been optimized over time to stay fast, even on complex databases. You can see a summary of their execution time in :ref:`pgrst_logging` and :ref:`metrics`.
|
||||
- If the schema cache queries are slow, the most likely cause is *system catalog bloat*, see `issue#3212 <https://github.com/PostgREST/postgrest/issues/3212>`_ for more details.
|
||||
- You can turn the :ref:`log-level` to ``debug`` to see the time of each schema cache query.
|
||||
|
||||
.. _schema_reloading:
|
||||
|
||||
Schema Cache Reloading
|
||||
@@ -53,6 +59,19 @@ To reload the schema cache from within the database, you can use the ``NOTIFY``
|
||||
|
||||
NOTIFY pgrst, 'reload schema'
|
||||
|
||||
Debouncing
|
||||
~~~~~~~~~~
|
||||
|
||||
PostgREST does not reload the schema cache for each notification when several ``NOTIFY pgrst`` events are generated quickly after one another.
|
||||
|
||||
There are two cases to consider: when notifications are sent within a single transaction and when they are sent across multiple transactions.
|
||||
|
||||
In the first case, PostgreSQL deduplicates identical ``NOTIFY`` events within the same transaction. This means that even if multiple ``NOTIFY pgrst`` statements are executed before a ``COMMIT``, only a single notification is delivered to PostgREST.
|
||||
|
||||
In the second case, when notifications are sent from separate transactions in a short time span, PostgREST applies a debouncing mechanism to avoid excessive schema cache reloads.
|
||||
|
||||
Instead of reloading the schema cache for each notification, events are grouped within a small time window of 100 milliseconds. The reload function is executed once immediately when the first notification is received and once more after the burst of events settles, resulting in at most two executions within that time window.
|
||||
|
||||
.. _auto_schema_reloading:
|
||||
|
||||
Automatic Schema Cache Reloading
|
||||
|
||||
@@ -221,7 +221,7 @@ Notice that the ``response.headers`` should be set to an *array* of single-key o
|
||||
|
||||
.. note::
|
||||
|
||||
PostgREST provided headers such as ``Content-Type``, ``Location``, etc. can be overriden this way. Note that irrespective of overridden ``Content-Type`` response header, the content will still be converted to JSON, unless you use :ref:`custom_media`.
|
||||
PostgREST provided headers such as ``Content-Type``, ``Location``, etc. can be overridden this way. Note that irrespective of overridden ``Content-Type`` response header, the content will still be converted to JSON, unless you use :ref:`custom_media`.
|
||||
|
||||
.. _guc_resp_status:
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This file is auto-generated by postgrest-nixpkgs-upgrade
|
||||
sphinx==8.2.3
|
||||
sphinx==9.1.0
|
||||
sphinx-copybutton==0.5.2
|
||||
sphinx-rtd-dark-mode==1.3.0
|
||||
sphinx-rtd-theme==3.0.2
|
||||
sphinx-tabs==3.4.7
|
||||
sphinxext-opengraph==0.9.1
|
||||
sphinx-rtd-theme==3.1.0
|
||||
sphinx-tabs==3.5.0
|
||||
sphinxext-opengraph==0.13.0
|
||||
@@ -22,7 +22,7 @@ Step 1. Install PostgreSQL
|
||||
|
||||
If you're already familiar with using PostgreSQL and have it installed on your system you can use the existing installation (see :ref:`pg-dependency` for minimum requirements). For this tutorial we'll describe how to use the database in Docker because database configuration is otherwise too complicated for a simple tutorial.
|
||||
|
||||
If Docker is not installed, you can get it `here <https://www.docker.com/get-started>`_. Next, let's pull and start the database image:
|
||||
If Docker is not installed, you can get it `here <https://www.docker.com/get-started>`_. Make sure that Docker service is `started <https://docs.docker.com/engine/daemon/start/#start-the-daemon-using-operating-system-utilities>`_. Next, let's pull and start the database image:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
||||
Generated
+4
-4
@@ -2,16 +2,16 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1752006229,
|
||||
"narHash": "sha256-BeuAPwNM2RBc5bvUTb0j4GRs2yBkDeRCw/8Y3v9Xesc=",
|
||||
"lastModified": 1776949667,
|
||||
"narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c80edd02003fe3d8af527215a3ac069be9cfd47f",
|
||||
"rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixpkgs-25.05-darwin",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "REST API for any Postgres database";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-25.05-darwin";
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
||||
};
|
||||
|
||||
nixConfig = {
|
||||
@@ -46,5 +46,9 @@
|
||||
meta.description = "REST API for any Postgres database";
|
||||
};
|
||||
});
|
||||
|
||||
devShells = genSystems (postgrest: {
|
||||
default = import ./shell.nix { inherit postgrest; };
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
+7
-7
@@ -91,11 +91,11 @@ postgrest-gen-ctags postgrest-watch
|
||||
postgrest-gen-jwt postgrest-with-all
|
||||
postgrest-gen-secret postgrest-with-git
|
||||
postgrest-git-hooks postgrest-with-pgrst
|
||||
postgrest-hsie-graph-modules postgrest-with-postgresql-13
|
||||
postgrest-hsie-graph-symbols postgrest-with-postgresql-14
|
||||
postgrest-hsie-minimal-imports postgrest-with-postgresql-15
|
||||
postgrest-lint postgrest-with-postgresql-16
|
||||
postgrest-loadtest postgrest-with-postgresql-17
|
||||
postgrest-hsie-graph-modules postgrest-with-pg-14
|
||||
postgrest-hsie-graph-symbols postgrest-with-pg-15
|
||||
postgrest-hsie-minimal-imports postgrest-with-pg-16
|
||||
postgrest-lint postgrest-with-pg-17
|
||||
postgrest-loadtest postgrest-with-pg-18
|
||||
postgrest-loadtest-against postgrest-with-slow-pg
|
||||
postgrest-loadtest-report postgrest-with-slow-postgrest
|
||||
postgrest-nixpkgs-upgrade
|
||||
@@ -174,7 +174,7 @@ $ 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"
|
||||
$ nix-shell --run "postgrest-with-pg-17 postgrest-test-spec"
|
||||
|
||||
```
|
||||
|
||||
@@ -284,7 +284,7 @@ Tools like `postgrest-build`, `postgrest-run`, `postgrest-repl` etc. are simple
|
||||
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
|
||||
`postgrest-with-pg-*` 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.
|
||||
|
||||
+6
-24
@@ -16,11 +16,8 @@ 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):
|
||||
in [`flake.nix`](../flake.nix). To upgrade it, you can use a small utility
|
||||
script defined in [`nix/tools/nixpkgsTools.nix`](tools/nixpkgsTools.nix):
|
||||
|
||||
```bash
|
||||
# From the root of the repository, enter nix-shell
|
||||
@@ -30,21 +27,12 @@ nix-shell
|
||||
postgrest-nixpkgs-upgrade
|
||||
|
||||
# Exit the nix-shell with Ctrl-d
|
||||
|
||||
```
|
||||
|
||||
## 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
|
||||
@@ -58,25 +46,19 @@ errors, this is probably due to one of our patches. Try to fix them and re-run
|
||||
|
||||
## Update the PostgREST binary cache
|
||||
|
||||
If you have access to the PostgREST cachix signing key, you can push the
|
||||
If you have access to the PostgREST cachix project, 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 your shell history.
|
||||
You'll need to login with your token with `cachix authtoken <token>`.
|
||||
|
||||
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
|
||||
The `postgrest-push-cachix` command will query the nix-store to list all
|
||||
dependencies and build artifacts of PostgREST. It will then push
|
||||
everything that is not yet cached to the binary cache.
|
||||
|
||||
+20
-7
@@ -4,6 +4,7 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
|
||||
-- | Haskell Imports and Exports tool
|
||||
@@ -33,13 +34,16 @@ import Data.Function ((&))
|
||||
import Data.List (intercalate)
|
||||
import Data.Maybe (catMaybes, mapMaybe)
|
||||
import Data.Text (Text)
|
||||
import GHC.Driver.Errors.Types (GhcMessage)
|
||||
import GHC.Generics (Generic)
|
||||
import GHC.Hs.Extension (GhcPs)
|
||||
import GHC.Types.Error (getMessages)
|
||||
import GHC.Types.Error (Messages, defaultDiagnosticOpts,
|
||||
getMessages)
|
||||
import GHC.Types.Name.Occurrence (occNameString)
|
||||
import GHC.Types.Name.Reader (rdrNameOcc)
|
||||
import GHC.Unit.Module.Name (moduleNameString)
|
||||
import GHC.Unit.Module (moduleNameString)
|
||||
import GHC.Utils.Error (pprMsgEnvelopeBagWithLoc)
|
||||
import GHC.Utils.Outputable (showSDocUnsafe)
|
||||
import System.Directory.Recursive (getFilesRecursive)
|
||||
import System.Exit (exitFailure)
|
||||
|
||||
@@ -198,7 +202,7 @@ sourceSymbols source = do
|
||||
return $ concatMap (importSymbols source filepath . GHC.unLoc) hsmodImports
|
||||
|
||||
-- | Parse a Haskell module
|
||||
parseModule :: FilePath -> IO GHC.HsModule
|
||||
parseModule :: FilePath -> IO (GHC.HsModule GhcPs)
|
||||
parseModule filepath = do
|
||||
result <- ExactPrint.parseModule GHC.Paths.libdir filepath
|
||||
case result of
|
||||
@@ -206,7 +210,13 @@ parseModule filepath = do
|
||||
return $ GHC.unLoc hsmod
|
||||
Left errs ->
|
||||
fail $ "Errors with " <> show filepath <> ":\n "
|
||||
<> show (pprMsgEnvelopeBagWithLoc $ getMessages errs)
|
||||
<> formatParseErrors errs
|
||||
|
||||
formatParseErrors :: Messages GhcMessage -> String
|
||||
formatParseErrors errs =
|
||||
intercalate "\n "
|
||||
. fmap showSDocUnsafe
|
||||
$ pprMsgEnvelopeBagWithLoc (defaultDiagnosticOpts @GhcMessage) (getMessages errs)
|
||||
|
||||
-- | Symbols imported in an import declaration.
|
||||
--
|
||||
@@ -214,9 +224,12 @@ parseModule filepath = do
|
||||
-- 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
|
||||
case ideclImportList of
|
||||
Just (importListInterpretation, syms) ->
|
||||
symbol (if importListInterpretation == GHC.EverythingBut then Hiding else Explicit)
|
||||
. Just
|
||||
. GHC.unLoc
|
||||
<$> GHC.unLoc syms
|
||||
Nothing ->
|
||||
[ symbol Wildcard Nothing ]
|
||||
where
|
||||
|
||||
@@ -104,8 +104,7 @@ let
|
||||
''
|
||||
|
||||
+ lib.optionalString withTmpDir ''
|
||||
mkdir -p "''${TMPDIR:-/tmp}/postgrest"
|
||||
tmpdir="$(${coreutils}/bin/mktemp -d --tmpdir postgrest/${name}-XXX)"
|
||||
tmpdir="$(${coreutils}/bin/mktemp -d --tmpdir=/tmp ${name}-XXX)"
|
||||
|
||||
# we keep the tmpdir when an error occurs for debugging
|
||||
trap 'echo Temporary directory kept at: $tmpdir' ERR
|
||||
|
||||
@@ -3,5 +3,4 @@
|
||||
checked-shell-script = import ./checked-shell-script;
|
||||
gitignore = import ./gitignore.nix;
|
||||
haskell-packages = import ./haskell-packages.nix;
|
||||
slocat = import ./slocat.nix;
|
||||
}
|
||||
|
||||
@@ -47,37 +47,43 @@ let
|
||||
# - To modify and try packages locally, see "Working with locally modified Haskell packages" in the Nix README.
|
||||
|
||||
# Before upgrading fuzzyset to 0.3, check: https://github.com/PostgREST/postgrest/issues/3329
|
||||
# jailbreak, because hspec limit for tests
|
||||
fuzzyset = prev.fuzzyset_0_2_4;
|
||||
|
||||
# TODO: Remove once available in nixpkgs haskellPackages
|
||||
configurator-pg =
|
||||
http2 =
|
||||
prev.callHackageDirect
|
||||
{
|
||||
pkg = "configurator-pg";
|
||||
ver = "0.2.11";
|
||||
sha256 = "sha256-mtGtNawDJgz2ZIEVca+IYXVu4oNw9xsfJiYWAqAbbgc=";
|
||||
pkg = "http2";
|
||||
ver = "5.4.0";
|
||||
sha256 = "sha256-PeEWVd61bQ8G7LvfLeXklzXqNJFaAjE2ecRMWJZESPE=";
|
||||
}
|
||||
{ };
|
||||
|
||||
# TODO: Remove once available in nixpkgs haskellPackages
|
||||
streaming-commons =
|
||||
http-semantics =
|
||||
prev.callHackageDirect
|
||||
{
|
||||
pkg = "streaming-commons";
|
||||
ver = "0.2.3.1";
|
||||
sha256 = "sha256-Gl2eaJcWe1sxmcE/octWlH9uSnERguf+5H66K4fV87s=";
|
||||
pkg = "http-semantics";
|
||||
ver = "0.4.0";
|
||||
sha256 = "sha256-rh0z51EKvsu5rQd5n2z3fSRjjEObouNZSBPO9NFYOF0=";
|
||||
}
|
||||
{ };
|
||||
|
||||
# Downgrade hasql and related packages while we are still on GHC 9.4 for the static build.
|
||||
hasql = lib.dontCheck (lib.doJailbreak prev.hasql_1_6_4_4);
|
||||
hasql-dynamic-statements = lib.dontCheck prev.hasql-dynamic-statements_0_3_1_5;
|
||||
hasql-implicits = lib.dontCheck prev.hasql-implicits_0_1_1_3;
|
||||
hasql-notifications = lib.dontCheck prev.hasql-notifications_0_2_2_2;
|
||||
hasql-pool = lib.dontCheck prev.hasql-pool_1_0_1;
|
||||
hasql-transaction = lib.dontCheck prev.hasql-transaction_1_1_0_1;
|
||||
postgresql-binary = lib.dontCheck (lib.doJailbreak prev.postgresql-binary_0_13_1_3);
|
||||
network-run =
|
||||
prev.callHackageDirect
|
||||
{
|
||||
pkg = "network-run";
|
||||
ver = "0.5.0";
|
||||
sha256 = "sha256-vbXh+CzxDsGApjqHxCYf/ijpZtUCApFbkcF5gyN0THU=";
|
||||
}
|
||||
{ };
|
||||
|
||||
warp =
|
||||
lib.dontCheck (prev.callHackageDirect
|
||||
{
|
||||
pkg = "warp";
|
||||
ver = "3.4.13";
|
||||
sha256 = "sha256-jmr8kpeSPDkOhT0i9PhozZapX4nUs92cOX7POAGb7/M=";
|
||||
}
|
||||
{ });
|
||||
};
|
||||
in
|
||||
{
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
_: prev:
|
||||
{
|
||||
slocat = prev.buildGoModule {
|
||||
name = "slocat";
|
||||
src = prev.fetchFromGitHub {
|
||||
owner = "robx";
|
||||
repo = "slocat";
|
||||
rev = "52e7512c6029fd00483e41ccce260a3b4b9b3b64";
|
||||
sha256 = "sha256-qn6luuh5wqREu3s8RfuMCP5PKdS2WdwPrujRYTpfzQ8=";
|
||||
};
|
||||
vendorHash = null;
|
||||
};
|
||||
}
|
||||
@@ -82,6 +82,7 @@ let
|
||||
}
|
||||
''
|
||||
${tests}/bin/postgrest-test-spec
|
||||
${tests}/bin/postgrest-test-observability
|
||||
${tests}/bin/postgrest-test-doctests
|
||||
${tests}/bin/postgrest-test-io
|
||||
${tests}/bin/postgrest-test-big-schema
|
||||
@@ -171,7 +172,7 @@ let
|
||||
# The following unsets all GIT_ variables.
|
||||
unset "''${!GIT_@}"
|
||||
|
||||
# shellcheck disable=SC2317
|
||||
# shellcheck disable=SC2329
|
||||
function restore () {
|
||||
ref="$(git stash list --format=format:%gD --grep "$1" -n1)"
|
||||
# this will avoid merge conflicts when applying the stash
|
||||
@@ -204,7 +205,7 @@ let
|
||||
${git}/bin/git add .
|
||||
;;
|
||||
pre-push)
|
||||
# Create a clean working tree without any uncomitted changes.
|
||||
# Create a clean working tree without any uncommitted changes.
|
||||
${withTools.withGit} HEAD ${style}/bin/postgrest-lint
|
||||
;;
|
||||
esac
|
||||
@@ -231,7 +232,7 @@ let
|
||||
${style}/bin/postgrest-lint
|
||||
;;
|
||||
pre-push)
|
||||
# Create a clean working tree without any uncomitted changes.
|
||||
# Create a clean working tree without any uncommitted changes.
|
||||
${withTools.withGit} HEAD ${check}
|
||||
;;
|
||||
esac
|
||||
|
||||
+2
-2
@@ -43,7 +43,7 @@ let
|
||||
}
|
||||
|
||||
if [ "$_arg_language" == "" ]; then
|
||||
# clean previous build, otherwise some errors might be supressed
|
||||
# clean previous build, otherwise some errors might be suppressed
|
||||
rm -rf "../.docs-build/html/default"
|
||||
|
||||
if [ -d languages ]; then
|
||||
@@ -54,7 +54,7 @@ let
|
||||
|
||||
build html "../.docs-build/html/default"
|
||||
else
|
||||
# clean previous build, otherwise some errors might be supressed
|
||||
# clean previous build, otherwise some errors might be suppressed
|
||||
rm -rf "../.docs-build/html/$_arg_language"
|
||||
|
||||
# update and build specific locale, can be used to create new locale
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Generate RSA JWK/public material for loadtests.
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import jwcrypto.jwk as jwk
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate RSA JWK/private key pair for loadtests"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rsa",
|
||||
dest="jwk_path",
|
||||
metavar="JWK_PATH",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Path to write the RSA JWK file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--private-key",
|
||||
dest="private_key_path",
|
||||
metavar="PRIVATE_KEY_PATH",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Path to write the RSA private key file",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
key = jwk.JWK.generate(kty="RSA", size=4096)
|
||||
private_jwk, public_jwk = key.export_private(), key.export_public()
|
||||
|
||||
try:
|
||||
args.jwk_path.write_text(public_jwk)
|
||||
print(f"Created RSA JWK on {args.jwk_path}")
|
||||
except OSError as e:
|
||||
print(f"Error writing to {args.jwk_path}:{e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
args.private_key_path.write_text(private_jwk)
|
||||
print(f"Created private key on {args.private_key_path}")
|
||||
except OSError as e:
|
||||
print(f"Error writing to {args.private_key_path}:{e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+106
-25
@@ -12,23 +12,24 @@
|
||||
# from an array
|
||||
import time
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
import random
|
||||
import jwt
|
||||
import jwcrypto.jwk as jwk
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
|
||||
URL = "http://postgrest"
|
||||
|
||||
secret_key = b"reallyreallyreallyreallyverysafe"
|
||||
|
||||
key = jwk.JWK.generate(kty="RSA", size=4096)
|
||||
private_key = jwt.algorithms.RSAAlgorithm.from_jwk(key.export_private())
|
||||
public_key = key.export_public()
|
||||
|
||||
|
||||
def generate_jwt(now: int, exp_inc: Optional[int], is_hs: bool) -> str:
|
||||
def generate_jwt(
|
||||
now: int,
|
||||
exp_inc: Optional[int],
|
||||
rsa_private_key: Optional[jwt.algorithms.RSAAlgorithm],
|
||||
) -> str:
|
||||
"""Generate an HS256 or RS256 JWT"""
|
||||
payload = {
|
||||
"sub": f"user_{random.getrandbits(32)}",
|
||||
@@ -39,25 +40,72 @@ def generate_jwt(now: int, exp_inc: Optional[int], is_hs: bool) -> str:
|
||||
if exp_inc is not None:
|
||||
payload["exp"] = now + exp_inc
|
||||
|
||||
k = secret_key if is_hs else private_key
|
||||
alg = "HS256" if is_hs else "RS256"
|
||||
return jwt.encode(payload, k, alg)
|
||||
if rsa_private_key is None:
|
||||
key = secret_key
|
||||
alg = "HS256"
|
||||
else:
|
||||
key = rsa_private_key
|
||||
alg = "RS256"
|
||||
return jwt.encode(payload, key, alg)
|
||||
|
||||
|
||||
def append_targets(lines: list[str], token: str):
|
||||
lines.append(f"OPTIONS {URL}/authors_only")
|
||||
HTTP_METHODS = (
|
||||
"GET",
|
||||
"OPTIONS",
|
||||
)
|
||||
|
||||
HttpMethod = Enum(
|
||||
"HttpMethod",
|
||||
{method: method for method in HTTP_METHODS},
|
||||
type=str,
|
||||
module=__name__,
|
||||
)
|
||||
|
||||
|
||||
def append_targets(lines: list[str], token: str, http_method: HttpMethod):
|
||||
lines.append(f"{http_method.value} {URL}/authors_only")
|
||||
lines.append(f"Authorization: Bearer {token}")
|
||||
lines.append("") # blank line to separate requests
|
||||
|
||||
|
||||
# we use this to chain commands on loadtest.nix
|
||||
def run_command(command: list[str]):
|
||||
if not command:
|
||||
return
|
||||
|
||||
if command[0] == "--":
|
||||
command = command[1:]
|
||||
|
||||
if not command:
|
||||
return
|
||||
|
||||
try:
|
||||
subprocess.run(command, check=True)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(
|
||||
f"Error executing command {' '.join(command)}: {exc}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(exc.returncode)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate Vegeta targets with unique JWTs"
|
||||
)
|
||||
parser.add_argument(
|
||||
"output",
|
||||
"targets_path",
|
||||
metavar="TARGETS_PATH",
|
||||
help="Path to write the generated targets file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--private-key",
|
||||
dest="private_key_path",
|
||||
metavar="PRIVATE_KEY_PATH",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Path to the RSA private key file (required when --rsa is used)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--worst",
|
||||
dest="worst",
|
||||
@@ -71,14 +119,31 @@ def main():
|
||||
metavar="JWK_PATH",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Path for generating a RSA JWK file to sign tokens with",
|
||||
help="Path to an existing RSA JWK file used for signing tokens",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--method",
|
||||
dest="http_method",
|
||||
choices=list(HTTP_METHODS),
|
||||
required=True,
|
||||
help="HTTP method for the vegeta targets",
|
||||
)
|
||||
parser.add_argument(
|
||||
"command",
|
||||
nargs=argparse.REMAINDER,
|
||||
help="Command (and arguments) to run after generating the targets",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
rsa_private_key: Optional[jwt.algorithms.RSAAlgorithm] = None
|
||||
|
||||
is_hs = args.jwk_path is None
|
||||
|
||||
http_method = HttpMethod(args.http_method)
|
||||
|
||||
nsamples = 1000
|
||||
|
||||
if is_hs:
|
||||
ntargets = 200000
|
||||
else:
|
||||
@@ -86,12 +151,25 @@ def main():
|
||||
ntargets = 50000
|
||||
|
||||
if not is_hs:
|
||||
if args.private_key_path is None:
|
||||
parser.error("--rsa requires the --private-key option")
|
||||
try:
|
||||
with open(args.jwk_path, "w") as jwk:
|
||||
jwk.write(public_key)
|
||||
print(f"Created {args.jwk_path} file containing the RSA JWK")
|
||||
except IOError as e:
|
||||
print(f"Error writing to {args.jwk_path}: {e}", file=sys.stderr)
|
||||
private_key_data = args.private_key_path.read_text()
|
||||
except OSError as e:
|
||||
err = (
|
||||
f"Error reading RSA private key from {args.private_key_path}: "
|
||||
f"{e}. Generate RSA materials first with gen_rsa_materials.py."
|
||||
)
|
||||
print(err, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
rsa_private_key = jwt.algorithms.RSAAlgorithm.from_jwk(private_key_data)
|
||||
except Exception as exc: # broad exception to capture parsing errors
|
||||
err = (
|
||||
f"Error loading RSA private key from {args.private_key_path}: " f"{exc}"
|
||||
)
|
||||
print(err, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Generating {ntargets} targets...")
|
||||
@@ -110,6 +188,7 @@ def main():
|
||||
if args.worst:
|
||||
# estimated time takes to build and run postgrest itself
|
||||
build_run_postgrest_time = 2
|
||||
|
||||
# estimated time it takes to generate the targets file
|
||||
# the division numbers are tuned by hand
|
||||
if is_hs: # hs generation is much faster
|
||||
@@ -121,25 +200,27 @@ def main():
|
||||
inc = build_run_postgrest_time + gen_time
|
||||
|
||||
for i in range(ntargets):
|
||||
token = generate_jwt(now, inc + i // 1000, is_hs)
|
||||
append_targets(lines, token)
|
||||
token = generate_jwt(now, inc + i // 1000, rsa_private_key)
|
||||
append_targets(lines, token, http_method)
|
||||
|
||||
else:
|
||||
tokens = [generate_jwt(now, None, is_hs) for _ in range(nsamples)]
|
||||
tokens = [generate_jwt(now, None, rsa_private_key) for _ in range(nsamples)]
|
||||
for i in range(ntargets):
|
||||
token = random.choice(tokens)
|
||||
append_targets(lines, token)
|
||||
append_targets(lines, token, http_method)
|
||||
|
||||
try:
|
||||
with open(args.output, "w") as f:
|
||||
with open(args.targets_path, "w") as f:
|
||||
f.write("\n".join(lines))
|
||||
except IOError as e:
|
||||
print(f"Error writing to {args.output}: {e}", file=sys.stderr)
|
||||
print(f"Error writing to {args.targets_path}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
print(f"Created {ntargets} targets", end=" ")
|
||||
print(f"in {args.output} ({elapsed:.2f}s)")
|
||||
print(f"in {args.targets_path} ({elapsed:.2f}s)")
|
||||
|
||||
run_command(args.command)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+105
-32
@@ -18,6 +18,8 @@ let
|
||||
];
|
||||
}
|
||||
''
|
||||
echo "Starting vegeta loadtest..."
|
||||
|
||||
# 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
|
||||
@@ -42,7 +44,9 @@ let
|
||||
"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_OPTIONAL_SINGLE([kind], [k], [Kind of loadtest], [mixed])"
|
||||
"ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,jwt-hs,jwt-hs-cache,jwt-hs-cache-worst,jwt-rsa,jwt-rsa-cache,jwt-rsa-cache-worst])"
|
||||
"ARG_OPTIONAL_SINGLE([method],, [HTTP method used for the jwt loadtests], [OPTIONS])"
|
||||
"ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,errors,jwt-hs,jwt-hs-cache,jwt-hs-cache-worst,jwt-rsa,jwt-rsa-cache,jwt-rsa-cache-worst])"
|
||||
"ARG_TYPE_GROUP_SET([METHOD], [METHOD], [method], [OPTIONS,GET])"
|
||||
"ARG_OPTIONAL_SINGLE([monitor], [m], [Monitoring file], [./loadtest/result.csv])"
|
||||
"ARG_LEFTOVERS([additional vegeta arguments])"
|
||||
];
|
||||
@@ -59,67 +63,127 @@ let
|
||||
export PGRST_DB_TX_END="rollback-allow-override"
|
||||
export PGRST_LOG_LEVEL="crit"
|
||||
export PGRST_JWT_SECRET="reallyreallyreallyreallyverysafe"
|
||||
# set previous PGRST_JWT_CACHE_MAX_LIFETIME configuration so that
|
||||
# load test works across branches
|
||||
# TODO clean once PGRST_JWT_CACHE_MAX_ENTRIES merged and released
|
||||
export PGRST_JWT_CACHE_MAX_LIFETIME="86400"
|
||||
|
||||
mkdir -p "$(dirname "$_arg_output")"
|
||||
abs_output="$(realpath "$_arg_output")"
|
||||
|
||||
case "$_arg_kind" in
|
||||
jwt-hs)
|
||||
${genTargetsHS} "$_arg_testdir"/gen_targets.http
|
||||
export PGRST_JWT_CACHE_MAX_ENTRIES="0"
|
||||
export PGRST_JWT_CACHE_MAX_LIFETIME="0"
|
||||
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
${withGenTargets} --method "$_arg_method" "$_arg_testdir"/gen_targets.http \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
;;
|
||||
|
||||
jwt-hs-cache)
|
||||
${genTargetsHS} "$_arg_testdir"/gen_targets.http
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
${withGenTargets} --method "$_arg_method" "$_arg_testdir"/gen_targets.http \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
;;
|
||||
|
||||
jwt-hs-cache-worst)
|
||||
${genTargetsHS} --worst "$_arg_testdir"/gen_targets.http
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
${withGenTargets} --method "$_arg_method" --worst "$_arg_testdir"/gen_targets.http \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
;;
|
||||
|
||||
jwt-rsa)
|
||||
${genTargetsHS} --rsa="$_arg_testdir"/gen_jwk.json "$_arg_testdir"/gen_targets.http
|
||||
export PGRST_JWT_CACHE_MAX_ENTRIES="0"
|
||||
export PGRST_JWT_CACHE_MAX_LIFETIME="0"
|
||||
|
||||
${genRsaMaterials} --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json
|
||||
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
|
||||
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
${withGenTargets} --method "$_arg_method" --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json "$_arg_testdir"/gen_targets.http \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
;;
|
||||
|
||||
jwt-rsa-cache)
|
||||
${genTargetsHS} --rsa="$_arg_testdir"/gen_jwk.json "$_arg_testdir"/gen_targets.http
|
||||
${genRsaMaterials} --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json
|
||||
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
|
||||
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
${withGenTargets} --method "$_arg_method" --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json "$_arg_testdir"/gen_targets.http \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
;;
|
||||
|
||||
jwt-rsa-cache-worst)
|
||||
${genTargetsHS} --worst --rsa="$_arg_testdir"/gen_jwk.json "$_arg_testdir"/gen_targets.http
|
||||
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
|
||||
|
||||
${genRsaMaterials} --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json
|
||||
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
|
||||
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
${withGenTargets} --method "$_arg_method" --worst --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json "$_arg_testdir"/gen_targets.http \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
;;
|
||||
|
||||
*)
|
||||
mixed)
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
;;
|
||||
|
||||
# here we sleep purposefully to check how much memory does the schema cache consume in the final report
|
||||
errors)
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/errors.sql \
|
||||
${withTools.withPgrst} --timeout 2 --sleep 5 -m "$_arg_monitor" \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -targets errors.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$_arg_kind" == "mixed" ]; then
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withSlowPg} \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
${withTools.withSlowPgrst} \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
else
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
fi
|
||||
|
||||
${vegeta}/bin/vegeta report -type=text "$_arg_output"
|
||||
|
||||
if [ "$_arg_kind" != "errors" ]; then
|
||||
# fail in case 401 happened on jwt loadtests
|
||||
unauthorized_count="$(${vegeta}/bin/vegeta report -type=json "$_arg_output" \
|
||||
| ${jq}/bin/jq -r '.status_codes["401"] // 0')"
|
||||
|
||||
if [ "$unauthorized_count" -gt 0 ]; then
|
||||
last_unauthorized_body="$(${vegeta}/bin/vegeta encode "$_arg_output" \
|
||||
| ${jq}/bin/jq -rn '
|
||||
reduce inputs as $item (null;
|
||||
if $item.code == 401 then $item else . end
|
||||
)
|
||||
| if . == null then
|
||||
empty
|
||||
else
|
||||
(.body | @base64d)
|
||||
end
|
||||
')"
|
||||
|
||||
echo "loadtest failed: found $unauthorized_count 401 Unauthorized responses" >&2
|
||||
if [ -n "$last_unauthorized_body" ]; then
|
||||
printf '%s\n' "Last 401 response body:" >&2
|
||||
printf '%s\n' "$last_unauthorized_body" >&2
|
||||
fi
|
||||
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
'';
|
||||
|
||||
loadtestAgainst =
|
||||
@@ -250,13 +314,22 @@ let
|
||||
| ${mergeMonitorResults}
|
||||
'';
|
||||
|
||||
genTargetsHS =
|
||||
writers.writePython3 "postgrest-gen-loadtest-targets-hs"
|
||||
withGenTargets =
|
||||
writers.writePython3 "postgrest-with-gen-loadtest-targets"
|
||||
{
|
||||
libraries = [ python3Packages.pyjwt python3Packages.jwcrypto ];
|
||||
doCheck = false; # postgrest-style conflicts with this
|
||||
}
|
||||
(builtins.readFile ./generate_targets.py);
|
||||
|
||||
genRsaMaterials =
|
||||
writers.writePython3 "postgrest-gen-rsa-materials"
|
||||
{
|
||||
libraries = [ python3Packages.jwcrypto ];
|
||||
doCheck = false; # postgrest-style conflicts with this
|
||||
}
|
||||
(builtins.readFile ./gen_rsa_materials.py);
|
||||
|
||||
mergeMonitorResults =
|
||||
writers.writePython3 "postgrest-merge-monitor-results"
|
||||
{
|
||||
|
||||
@@ -68,7 +68,7 @@ let
|
||||
bump devel
|
||||
|
||||
# The order of operations is important here:
|
||||
# - bump devel is run and $A is upated to the new version
|
||||
# - bump devel is run and $A is updated to the new version
|
||||
# - the branch is created with the new A, but the commit before the devel bump
|
||||
# - the devel bump is committed
|
||||
git branch "v$A"
|
||||
|
||||
+24
-7
@@ -12,6 +12,7 @@
|
||||
, silver-searcher
|
||||
, statix
|
||||
, stylish-haskell
|
||||
, writeText
|
||||
}:
|
||||
let
|
||||
style =
|
||||
@@ -28,19 +29,20 @@ let
|
||||
|
||||
# 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$' . \
|
||||
# TODO: fix style issues in src/protolude and include it
|
||||
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' --ignore-dir=src/protolude . \
|
||||
| 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
|
||||
# Script to check whether any uncommitted changes result from postgrest-style
|
||||
styleCheck =
|
||||
checkedShellScript
|
||||
{
|
||||
name = "postgrest-style-check";
|
||||
docs = "Check whether postgrest-style results in any uncommited changes.";
|
||||
docs = "Check whether postgrest-style results in any uncommitted changes.";
|
||||
workingDir = "/";
|
||||
}
|
||||
''
|
||||
@@ -51,6 +53,20 @@ let
|
||||
${git}/bin/git diff-index --exit-code HEAD -- '*.hs' '*.lhs' '*.nix' '*.py'
|
||||
'';
|
||||
|
||||
hlintConfig = writeText "hlintConfig.yml" ''
|
||||
|
||||
# Arguments passed to hlint
|
||||
- arguments: [-j, -XQuasiQuotes, -XNoPatternSynonyms]
|
||||
|
||||
# Warnings
|
||||
- warn: { lhs: "a == a", rhs: "True", note: "This comparison always evaluates to True" }
|
||||
- warn: { lhs: "a /= a", rhs: "False", note: "This comparison always evaluates to False" }
|
||||
- warn: { lhs: "a < a", rhs: "False", note: "This comparison always evaluates to False" }
|
||||
- warn: { lhs: "a > a", rhs: "False", note: "This comparison always evaluates to False" }
|
||||
- warn: { lhs: "a <= a", rhs: "True", note: "This comparison always evaluates to True" }
|
||||
- warn: { lhs: "a >= a", rhs: "True", note: "This comparison always evaluates to True" }
|
||||
'';
|
||||
|
||||
lint =
|
||||
checkedShellScript
|
||||
{
|
||||
@@ -68,18 +84,19 @@ let
|
||||
# ruff has gaps in scanning for unused code, so we use vulture
|
||||
echo "Scanning python files for unused code..."
|
||||
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?py$' . \
|
||||
| xargs ${python3Packages.vulture}/bin/vulture --exclude docs/conf.py
|
||||
| xargs ${python3Packages.vulture}/bin/vulture --exclude docs/conf.py --min-confidence 80
|
||||
|
||||
echo "Linting python files..."
|
||||
${ruff}/bin/ruff check .
|
||||
|
||||
echo "Checking consistency of import aliases in Haskell code..."
|
||||
${hsie} check-aliases main src
|
||||
${hsie} check-aliases main src/PostgREST
|
||||
|
||||
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 -j -X QuasiQuotes -X NoPatternSynonyms
|
||||
# TODO: fix lint issues in src/protolude and include it
|
||||
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' --ignore-dir=src/protolude . \
|
||||
| xargs ${hlint}/bin/hlint --hint=${hlintConfig}
|
||||
'';
|
||||
|
||||
in
|
||||
|
||||
+29
-10
@@ -7,7 +7,6 @@
|
||||
, glibcLocales ? null
|
||||
, gnugrep
|
||||
, hpc-codecov
|
||||
, hostPlatform
|
||||
, jq
|
||||
, lib
|
||||
, postgrest
|
||||
@@ -32,6 +31,20 @@ let
|
||||
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec -- "''${_arg_leftovers[@]}"
|
||||
'';
|
||||
|
||||
testObservability =
|
||||
checkedShellScript
|
||||
{
|
||||
name = "postgrest-test-observability";
|
||||
docs = "Run the Haskell observability test suite.";
|
||||
args = [ "ARG_LEFTOVERS([hspec arguments])" ];
|
||||
workingDir = "/";
|
||||
withEnv = postgrest.env;
|
||||
}
|
||||
''
|
||||
${withTools.withPg} -f test/observability/fixtures/load.sql \
|
||||
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:observability -- "''${_arg_leftovers[@]}"
|
||||
'';
|
||||
|
||||
testDoctests =
|
||||
checkedShellScript
|
||||
{
|
||||
@@ -83,7 +96,7 @@ let
|
||||
}
|
||||
''
|
||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
|
||||
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/fixtures.sql \
|
||||
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/fixtures/load.sql \
|
||||
${ioTestPython}/bin/pytest --ignore=test/io/test_big_schema.py --ignore=test/io/test_replica.py -v test/io "''${_arg_leftovers[@]}"
|
||||
'';
|
||||
|
||||
@@ -98,7 +111,7 @@ let
|
||||
}
|
||||
''
|
||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
|
||||
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/big_schema.sql \
|
||||
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/fixtures/big_schema.sql \
|
||||
${ioTestPython}/bin/pytest -v test/io/test_big_schema.py "''${_arg_leftovers[@]}"
|
||||
'';
|
||||
|
||||
@@ -113,7 +126,7 @@ let
|
||||
}
|
||||
''
|
||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
|
||||
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} --replica -f test/io/replica.sql \
|
||||
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} --replica -f test/io/fixtures/replica.sql \
|
||||
${ioTestPython}/bin/pytest -v test/io/test_replica.py "''${_arg_leftovers[@]}"
|
||||
'';
|
||||
|
||||
@@ -145,7 +158,7 @@ let
|
||||
}
|
||||
(
|
||||
# required for `hpc markup` in CI; glibcLocales is not available e.g. on Darwin
|
||||
lib.optionalString (stdenv.isLinux && hostPlatform.libc == "glibc") ''
|
||||
lib.optionalString (stdenv.isLinux && stdenv.hostPlatform.libc == "glibc") ''
|
||||
export LOCALE_ARCHIVE="${glibcLocales}/lib/locale/locale-archive"
|
||||
'' +
|
||||
|
||||
@@ -155,7 +168,7 @@ let
|
||||
rm -rf coverage/*
|
||||
|
||||
# build once before running all the tests
|
||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec
|
||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec test:observability
|
||||
|
||||
(
|
||||
trap 'echo Found dead code: Check file list above.' ERR ;
|
||||
@@ -164,26 +177,31 @@ let
|
||||
|
||||
# collect all tests
|
||||
HPCTIXFILE="$tmpdir"/io.tix \
|
||||
${withTools.withPg} -f test/io/fixtures.sql \
|
||||
${withTools.withPg} -f test/io/fixtures/load.sql \
|
||||
${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest --ignore=test/io/test_big_schema.py --ignore=test/io/test_replica.py -v test/io
|
||||
|
||||
HPCTIXFILE="$tmpdir"/big_schema.tix \
|
||||
${withTools.withPg} -f test/io/big_schema.sql \
|
||||
${withTools.withPg} -f test/io/fixtures/big_schema.sql \
|
||||
${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest -v test/io/test_big_schema.py
|
||||
|
||||
HPCTIXFILE="$tmpdir"/replica.tix \
|
||||
${withTools.withPg} --replica -f test/io/replica.sql \
|
||||
${withTools.withPg} --replica -f test/io/fixtures/replica.sql \
|
||||
${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest -v test/io/test_replica.py
|
||||
|
||||
HPCTIXFILE="$tmpdir"/spec.tix \
|
||||
${withTools.withPg} -f test/spec/fixtures/load.sql \
|
||||
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec
|
||||
|
||||
HPCTIXFILE="$tmpdir"/observability.tix \
|
||||
${withTools.withPg} -f test/observability/fixtures/load.sql \
|
||||
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:observability
|
||||
|
||||
# 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"/big_schema*.tix "$tmpdir"/replica*.tix "$tmpdir"/spec.tix
|
||||
"$tmpdir"/io*.tix "$tmpdir"/big_schema*.tix "$tmpdir"/replica*.tix "$tmpdir"/spec.tix \
|
||||
"$tmpdir"/observability.tix
|
||||
|
||||
# prepare the overlay
|
||||
${ghc}/bin/hpc overlay --output="$tmpdir"/overlay.tix test/coverage.overlay
|
||||
@@ -250,6 +268,7 @@ buildToolbox
|
||||
tools = {
|
||||
inherit
|
||||
testSpec
|
||||
testObservability
|
||||
testDoctests
|
||||
testSpecIdempotence
|
||||
testIO
|
||||
|
||||
+55
-106
@@ -6,7 +6,6 @@
|
||||
, postgresqlVersions
|
||||
, postgrest
|
||||
, python3Packages
|
||||
, slocat
|
||||
, writeText
|
||||
, writers
|
||||
}:
|
||||
@@ -25,7 +24,7 @@ let
|
||||
"ARG_OPTIONAL_SINGLE([fixtures], [f], [SQL file to load fixtures from])"
|
||||
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||
"ARG_LEFTOVERS([command arguments])"
|
||||
"ARG_USE_ENV([PGUSER], [postgrest_test_authenticator], [Authenticator PG role])"
|
||||
"ARG_USE_ENV([PGUSER], [Postgrest_Test_Authenticator], [Authenticator PG role])" # user is written in mixed case to implicitly test that it is being properly quoted in schema cache queries
|
||||
"ARG_USE_ENV([PGDATABASE], [postgres], [PG database name])"
|
||||
"ARG_USE_ENV([PGRST_DB_SCHEMAS], [test], [Schema to expose])"
|
||||
"ARG_USE_ENV([PGTZ], [utc], [Timezone to use])"
|
||||
@@ -46,7 +45,7 @@ let
|
||||
}
|
||||
|
||||
# Avoid starting multiple layers of withTmpDb, but make sure to have the last invocation
|
||||
# load fixtures. Otherwise postgrest-with-postgresql-xx postgrest-test-io would not be possible.
|
||||
# load fixtures. Otherwise postgrest-with-pg-xx postgrest-test-io would not be possible.
|
||||
if ! test -v PGHOST; then
|
||||
|
||||
mkdir -p "$tmpdir"/{db,socket}
|
||||
@@ -74,7 +73,13 @@ let
|
||||
>> "$setuplog"
|
||||
|
||||
log "Starting the database cluster..."
|
||||
|
||||
# Instead of listening on a local port, we will listen on a unix domain socket.
|
||||
# NOTE: unix domain socket filename name must remain under max limit.
|
||||
# On Linux, it's 108 chars (including '\0' terminator)
|
||||
# On MacOS, it's 104 chars
|
||||
# See: https://serverfault.com/questions/641347/check-if-a-path-exceeds-maximum-for-unix-domain-socket
|
||||
|
||||
pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $PGHOST -c log_statement=\"all\" " \
|
||||
>> "$setuplog"
|
||||
|
||||
@@ -100,7 +105,8 @@ let
|
||||
|
||||
log "Starting replica on $replica_host"
|
||||
|
||||
pg_ctl -D "$replica_dir" -l "$replica_dblog" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $replica_host -c log_statement=\"all\" " \
|
||||
# We set a low max_standby_streaming_delay to make the replication conflict fail faster in tests (otherwise it waits for the default 30s)
|
||||
pg_ctl -D "$replica_dir" -l "$replica_dblog" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $replica_host -c log_statement=\"all\" -c max_standby_streaming_delay=\"3s\" " \
|
||||
>> "$setuplog"
|
||||
|
||||
>&2 echo "${commandName}: Replica enabled. You can connect to it with: psql 'postgres:///$PGDATABASE?host=$replica_host' -U postgres"
|
||||
@@ -111,7 +117,7 @@ let
|
||||
export PGRST_DB_URI="postgres:///$PGDATABASE?host=$PGREPLICAHOST,$PGHOST"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2317
|
||||
# shellcheck disable=SC2329
|
||||
stop () {
|
||||
log "Stopping the database cluster..."
|
||||
pg_ctl stop --mode=immediate >> "$setuplog"
|
||||
@@ -126,9 +132,11 @@ let
|
||||
fi
|
||||
|
||||
if test "$_arg_fixtures"; then
|
||||
log "Loading fixtures under the postgres role..."
|
||||
load_start=$SECONDS
|
||||
>&2 printf "${commandName}: Loading fixtures under the postgres role..."
|
||||
psql -U postgres -v PGUSER="$PGUSER" -v ON_ERROR_STOP=1 -f "$_arg_fixtures" >> "$setuplog"
|
||||
log "Done. Running command..."
|
||||
load_end=$((SECONDS - load_start))
|
||||
>&2 printf " done in %ss. Running command...\n" "$load_end"
|
||||
fi
|
||||
|
||||
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||
@@ -177,81 +185,6 @@ let
|
||||
|
||||
withPg = withTmpDb (builtins.head postgresqlVersions);
|
||||
|
||||
withSlowPg =
|
||||
checkedShellScript
|
||||
{
|
||||
name = "postgrest-with-slow-pg";
|
||||
docs = "Run the given command with simulated high latency postgresql";
|
||||
args =
|
||||
[
|
||||
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||
"ARG_LEFTOVERS([command arguments])"
|
||||
"ARG_USE_ENV([PGHOST], [], [PG host (socket name)])"
|
||||
"ARG_USE_ENV([PGDELAY], [0ms], [extra PG latency (duration)])"
|
||||
];
|
||||
positionalCompletion = "_command";
|
||||
workingDir = "/";
|
||||
redirectTixFiles = false;
|
||||
withTmpDir = true;
|
||||
}
|
||||
''
|
||||
delay="''${PGDELAY:-0ms}"
|
||||
echo "delaying data to/from postgres by $delay"
|
||||
|
||||
REALPGHOST="$PGHOST"
|
||||
export PGHOST="$tmpdir/socket"
|
||||
mkdir -p "$PGHOST"
|
||||
|
||||
${slocat}/bin/slocat -delay "$delay" -src "$PGHOST/.s.PGSQL.5432" -dst "$REALPGHOST/.s.PGSQL.5432" &
|
||||
SLOCAT_PID=$!
|
||||
# shellcheck disable=SC2317
|
||||
stop_slocat() {
|
||||
kill "$SLOCAT_PID" || true
|
||||
wait "$SLOCAT_PID" || true
|
||||
}
|
||||
trap stop_slocat EXIT
|
||||
sleep 1 # should wait for socket file to appear instead
|
||||
|
||||
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||
'';
|
||||
|
||||
withSlowPgrst =
|
||||
checkedShellScript
|
||||
{
|
||||
name = "postgrest-with-slow-postgrest";
|
||||
docs = "Run the given command with simulated high latency postgrest";
|
||||
args =
|
||||
[
|
||||
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||
"ARG_LEFTOVERS([command arguments])"
|
||||
"ARG_USE_ENV([PGRST_SERVER_UNIX_SOCKET], [], [PostgREST host (socket name)])"
|
||||
"ARG_USE_ENV([PGRST_DELAY], [0ms], [extra PostgREST latency (duration)])"
|
||||
];
|
||||
positionalCompletion = "_command";
|
||||
workingDir = "/";
|
||||
redirectTixFiles = false;
|
||||
withTmpDir = true;
|
||||
}
|
||||
''
|
||||
delay="''${PGRST_DELAY:-0ms}"
|
||||
echo "delaying data to/from PostgREST by $delay"
|
||||
|
||||
REAL_PGRST_SERVER_UNIX_SOCKET="$PGRST_SERVER_UNIX_SOCKET"
|
||||
export PGRST_SERVER_UNIX_SOCKET="$tmpdir/postgrest.socket"
|
||||
|
||||
${slocat}/bin/slocat -delay "$delay" -src "$PGRST_SERVER_UNIX_SOCKET" -dst "$REAL_PGRST_SERVER_UNIX_SOCKET" &
|
||||
SLOCAT_PID=$!
|
||||
# shellcheck disable=SC2317
|
||||
stop_slocat() {
|
||||
kill "$SLOCAT_PID" || true
|
||||
wait "$SLOCAT_PID" || true
|
||||
}
|
||||
trap stop_slocat EXIT
|
||||
sleep 1 # should wait for socket file to appear instead
|
||||
|
||||
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||
'';
|
||||
|
||||
withGit =
|
||||
let
|
||||
name = "postgrest-with-git";
|
||||
@@ -346,15 +279,21 @@ let
|
||||
'';
|
||||
|
||||
withPgrst =
|
||||
let
|
||||
commandName = "postgrest-with-pgrst";
|
||||
in
|
||||
checkedShellScript
|
||||
{
|
||||
name = "postgrest-with-pgrst";
|
||||
name = commandName;
|
||||
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])"
|
||||
"ARG_OPTIONAL_SINGLE([monitor], [m], [Enable CPU and memory monitoring of the PostgREST process and output to the designated file as markdown])"
|
||||
"ARG_OPTIONAL_SINGLE([timeout], [t], [Maximum time to wait for PostgREST to be ready], [5])"
|
||||
"ARG_OPTIONAL_SINGLE([sleep], [s], [Sleep time after PostgREST is ready, this is useful for monitoring])"
|
||||
"ARG_USE_ENV([PGRST_CMD], [], [PostgREST executable to run])"
|
||||
];
|
||||
positionalCompletion = "_command";
|
||||
workingDir = "/";
|
||||
@@ -364,30 +303,34 @@ let
|
||||
''
|
||||
export PGRST_SERVER_UNIX_SOCKET="$tmpdir"/postgrest.socket
|
||||
|
||||
rm -f result
|
||||
if [ -z "''${PGRST_BUILD_CABAL:-}" ]; then
|
||||
echo -n "Building postgrest (nix)... "
|
||||
# Using lib.getBin to also make this work with older checkouts, where .bin was not a thing, yet.
|
||||
nix-build -E 'with import ./. {}; pkgs.lib.getBin postgrestPackage' > "$tmpdir"/build.log 2>&1 || {
|
||||
echo "failed, output:"
|
||||
cat "$tmpdir"/build.log
|
||||
exit 1
|
||||
}
|
||||
PGRST_CMD=$(echo ./result*/bin/postgrest)
|
||||
else
|
||||
echo -n "Building postgrest (cabal)... "
|
||||
postgrest-build
|
||||
PGRST_CMD=postgrest-run
|
||||
if [ -z "''${PGRST_CMD:-}" ]; then
|
||||
rm -f result
|
||||
build_start=$SECONDS
|
||||
if [ -z "''${PGRST_BUILD_CABAL:-}" ]; then
|
||||
echo -n "${commandName}: Building postgrest (nix)... "
|
||||
# Using lib.getBin to also make this work with older checkouts, where .bin was not a thing, yet.
|
||||
nix-build -E 'with import ./. {}; pkgs.lib.getBin postgrestPackage' > "$tmpdir"/build.log 2>&1 || {
|
||||
echo "failed, output:"
|
||||
cat "$tmpdir"/build.log
|
||||
exit 1
|
||||
}
|
||||
PGRST_CMD=$(echo ./result*/bin/postgrest)
|
||||
else
|
||||
echo -n "${commandName}: Building postgrest (cabal)... "
|
||||
postgrest-build
|
||||
PGRST_CMD=postgrest-run
|
||||
fi
|
||||
build_end=$((SECONDS - build_start))
|
||||
printf "done in %ss.\n" "$build_end"
|
||||
fi
|
||||
echo "done."
|
||||
|
||||
ver=$($PGRST_CMD ${legacyConfig} --version)
|
||||
|
||||
echo -n "Starting $ver... "
|
||||
echo -n "${commandName}: Starting $ver... "
|
||||
|
||||
$PGRST_CMD ${legacyConfig} > "$tmpdir"/run.log 2>&1 &
|
||||
pid=$!
|
||||
# shellcheck disable=SC2317
|
||||
# shellcheck disable=SC2329
|
||||
cleanup() {
|
||||
# Send INT to all postgrest processes.
|
||||
# Workaround to trigger dumping postgrest.prof for postgrest-profiled-run
|
||||
@@ -401,17 +344,25 @@ let
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
timeout -s TERM 5 ${waitForPgrstReady} || {
|
||||
wait_start=$SECONDS
|
||||
timeout -s TERM "$_arg_timeout" ${waitForPgrstReady} || {
|
||||
echo "timed out, output:"
|
||||
cat "$tmpdir"/run.log
|
||||
exit 1
|
||||
}
|
||||
echo "done."
|
||||
wait_duration=$((SECONDS - wait_start))
|
||||
printf "done in %ss.\n" "$wait_duration"
|
||||
|
||||
echo "${commandName}: You can tail the server logs with: tail -f $tmpdir/run.log"
|
||||
|
||||
if [[ -n "$_arg_monitor" ]]; then
|
||||
${monitorPid} "$pid" > "$_arg_monitor" &
|
||||
fi
|
||||
|
||||
if [[ -n "$_arg_sleep" ]]; then
|
||||
sleep "$_arg_sleep"
|
||||
fi
|
||||
|
||||
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||
'';
|
||||
|
||||
@@ -429,11 +380,9 @@ buildToolbox
|
||||
inherit
|
||||
withGit
|
||||
withPgAll
|
||||
withPgrst
|
||||
withSlowPg
|
||||
withSlowPgrst;
|
||||
withPgrst;
|
||||
} // builtins.listToAttrs (
|
||||
# Create a `postgrest-with-postgresql-` for each PostgreSQL version
|
||||
# Create a `postgrest-with-pg-` for each PostgreSQL version
|
||||
builtins.map (pg: { inherit (pg) name; value = withTmpDb pg; }) postgresqlVersions
|
||||
);
|
||||
# make latest withPg available for other nix files
|
||||
|
||||
+119
-36
@@ -1,28 +1,27 @@
|
||||
cabal-version: 3.0
|
||||
name: postgrest
|
||||
version: 14.1
|
||||
version: 15
|
||||
synopsis: REST API for any Postgres database
|
||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||
for tables, views, and functions, supporting all HTTP methods that security
|
||||
permits.
|
||||
license: MIT
|
||||
license-file: LICENSE
|
||||
author: Joe Nelson, Adam Baker, Steve Chavez
|
||||
author: Joe Nelson, Adam Baker, Steve Chavez, Wolfgang Walther
|
||||
maintainer: Steve Chavez <stevechavezast@gmail.com>
|
||||
category: Executable, PostgreSQL, Network APIs
|
||||
homepage: https://postgrest.org
|
||||
bug-reports: https://github.com/PostgREST/postgrest/issues
|
||||
build-type: Simple
|
||||
extra-source-files: CHANGELOG.md
|
||||
cabal-version: >= 1.10
|
||||
|
||||
tested-with:
|
||||
-- nix
|
||||
GHC == 9.4.8
|
||||
-- cabal on Ubuntu
|
||||
-- stack on FreeBSD, MacOS, Ubuntu, Windows
|
||||
, GHC == 9.6.7
|
||||
, GHC == 9.10.3
|
||||
-- cabal on Ubuntu
|
||||
, GHC == 9.8.4
|
||||
-- nix
|
||||
, GHC == 9.12.3
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
@@ -55,6 +54,7 @@ library
|
||||
PostgREST.Client
|
||||
PostgREST.Config
|
||||
PostgREST.Config.Database
|
||||
PostgREST.Debounce
|
||||
PostgREST.Config.JSPath
|
||||
PostgREST.Config.PgVersion
|
||||
PostgREST.Config.Proxy
|
||||
@@ -66,6 +66,7 @@ library
|
||||
PostgREST.SchemaCache.Representations
|
||||
PostgREST.SchemaCache.Table
|
||||
PostgREST.Error
|
||||
PostgREST.Error.Types
|
||||
PostgREST.Listener
|
||||
PostgREST.Logger
|
||||
PostgREST.MainTx
|
||||
@@ -81,6 +82,7 @@ library
|
||||
PostgREST.Plan
|
||||
PostgREST.Plan.CallPlan
|
||||
PostgREST.Plan.MutatePlan
|
||||
PostgREST.Plan.Negotiate
|
||||
PostgREST.Plan.ReadPlan
|
||||
PostgREST.Plan.Types
|
||||
PostgREST.RangeQuery
|
||||
@@ -94,35 +96,35 @@ library
|
||||
PostgREST.Response.OpenAPI
|
||||
PostgREST.Response.GucHeader
|
||||
PostgREST.Response.Performance
|
||||
PostgREST.TimeIt
|
||||
PostgREST.Version
|
||||
build-depends: base >= 4.9 && < 4.20
|
||||
build-depends: base >= 4.9 && < 4.22
|
||||
, HTTP >= 4000.3.7 && < 4000.5
|
||||
, Ranged-sets >= 0.3 && < 0.5
|
||||
, Ranged-sets >= 0.3 && < 0.6
|
||||
, aeson >= 2.0.3 && < 2.3
|
||||
, auto-update >= 0.1.4 && < 0.3
|
||||
, base64-bytestring >= 1 && < 1.3
|
||||
, bytestring >= 0.10.8 && < 0.13
|
||||
, cache >= 0.1.3 && < 0.2.0
|
||||
, case-insensitive >= 1.2 && < 1.3
|
||||
, cassava >= 0.4.5 && < 0.6
|
||||
, clock >= 0.8.3 && < 0.9.0
|
||||
, configurator-pg >= 0.2.11 && < 0.3
|
||||
, containers >= 0.5.7 && < 0.7
|
||||
, containers >= 0.5.7 && < 0.8
|
||||
, cookie >= 0.4.2 && < 0.6
|
||||
-- crypton 1.1.0 moved from `memory` to `ram`, which jose-jwt fails to build with right now.
|
||||
-- should be possible to remove this once jose-jwt had a new release.
|
||||
, crypton < 1.1.0
|
||||
, directory >= 1.2.6 && < 1.4
|
||||
, either >= 4.4.1 && < 5.1
|
||||
, extra >= 1.7.0 && < 2.0
|
||||
, fuzzyset >= 0.2.4 && < 0.3
|
||||
, hasql >= 1.6.1.1 && < 1.7
|
||||
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
||||
, hasql-notifications >= 0.2.2.2 && < 0.2.3
|
||||
, hasql-pool >= 1.0.1 && < 1.1
|
||||
, hasql-transaction >= 1.0.1 && < 1.2
|
||||
, heredoc >= 0.2 && < 0.3
|
||||
, hasql >= 1.9 && <= 1.9.3.1
|
||||
, hasql-dynamic-statements >= 0.3.1 && <= 0.3.1.8
|
||||
, hasql-notifications >= 0.2.4.0 && < 0.3
|
||||
, hasql-pool >= 1.1 && <= 1.3.0.4
|
||||
, hasql-transaction >= 1.0.1 && <= 1.2.1
|
||||
, http-client >= 0.7.19 && < 0.8
|
||||
, http-types >= 0.12.2 && < 0.13
|
||||
, insert-ordered-containers >= 0.2.2 && < 0.3
|
||||
, iproute >= 1.7.0 && < 1.8
|
||||
, jose-jwt >= 0.9.6 && < 0.11
|
||||
, lens >= 4.14 && < 5.4
|
||||
, lens-aeson >= 1.0.1 && < 1.3
|
||||
@@ -132,18 +134,16 @@ library
|
||||
, network-uri >= 2.6.1 && < 2.8
|
||||
, optparse-applicative >= 0.13 && < 0.19
|
||||
, parsec >= 3.1.11 && < 3.2
|
||||
-- Technically unused, can be removed after updating to hasql >= 1.7
|
||||
, postgresql-libpq >= 0.10
|
||||
, prometheus-client >= 1.1.1 && < 1.2.0
|
||||
, protolude >= 0.3.1 && < 0.4
|
||||
, protolude
|
||||
, regex-tdfa >= 1.2.2 && < 1.4
|
||||
, retry >= 0.7.4 && < 0.10
|
||||
, scientific >= 0.3.4 && < 0.4
|
||||
, streaming-commons >= 0.2.3.1 && < 0.3
|
||||
, swagger2 >= 2.4 && < 2.9
|
||||
, text >= 1.2.2 && < 2.2
|
||||
, time >= 1.6 && < 1.13
|
||||
, timeit >= 2.0 && < 2.1
|
||||
, time >= 1.6 && < 1.15
|
||||
, unordered-containers >= 0.2.8 && < 0.3
|
||||
, unix-compat >= 0.5.4 && < 0.8
|
||||
, vault >= 0.3.1.5 && < 0.4
|
||||
@@ -156,7 +156,7 @@ library
|
||||
-- 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.5
|
||||
, warp >= 3.4.13 && < 3.5
|
||||
, stm >= 2.5 && < 3
|
||||
, stm-hamt >= 1.2 && < 2
|
||||
, focus >= 1.0 && < 2
|
||||
@@ -180,16 +180,64 @@ library
|
||||
build-depends:
|
||||
unix
|
||||
|
||||
library protolude
|
||||
visibility: private
|
||||
default-language: Haskell2010
|
||||
default-extensions: NoImplicitPrelude
|
||||
FlexibleContexts
|
||||
MultiParamTypeClasses
|
||||
OverloadedStrings
|
||||
hs-source-dirs: src/protolude
|
||||
exposed-modules: Protolude
|
||||
Protolude.Applicative
|
||||
Protolude.Base
|
||||
Protolude.Bifunctor
|
||||
Protolude.Bool
|
||||
Protolude.CallStack
|
||||
Protolude.Conv
|
||||
Protolude.ConvertText
|
||||
Protolude.Debug
|
||||
Protolude.Either
|
||||
Protolude.Error
|
||||
Protolude.Exceptions
|
||||
Protolude.Functor
|
||||
Protolude.List
|
||||
Protolude.Monad
|
||||
Protolude.Panic
|
||||
Protolude.Partial
|
||||
Protolude.Safe
|
||||
Protolude.Semiring
|
||||
Protolude.Show
|
||||
Protolude.Unsafe
|
||||
build-depends: array >= 0.4 && < 0.6
|
||||
, async >= 2.0 && < 2.3
|
||||
, base >= 4.6 && < 4.22
|
||||
, bytestring >= 0.10.8 && < 0.13
|
||||
, containers >= 0.5.7 && < 0.8
|
||||
, deepseq >= 1.3 && < 1.6
|
||||
, ghc-prim >= 0.3 && < 0.14
|
||||
, hashable >= 1.2 && < 1.6
|
||||
, mtl >= 2.1 && < 2.4
|
||||
, mtl-compat >= 0.2 && < 0.3
|
||||
, stm >= 2.5 && < 3
|
||||
, text >= 1.2.2 && < 2.2
|
||||
, transformers >= 0.2 && < 0.7
|
||||
, transformers-compat >= 0.4 && < 0.8
|
||||
-- Protolude has some partial functions, so
|
||||
-- it is fine to disable that specific warning
|
||||
ghc-options: -Werror -Wall -fwarn-identities -Wno-x-partial
|
||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||
|
||||
executable postgrest
|
||||
default-language: Haskell2010
|
||||
default-extensions: OverloadedStrings
|
||||
NoImplicitPrelude
|
||||
hs-source-dirs: main
|
||||
main-is: Main.hs
|
||||
build-depends: base >= 4.9 && < 4.20
|
||||
, containers >= 0.5.7 && < 0.7
|
||||
build-depends: base >= 4.9 && < 4.22
|
||||
, containers >= 0.5.7 && < 0.8
|
||||
, postgrest
|
||||
, protolude >= 0.3.1 && < 0.4
|
||||
, protolude
|
||||
ghc-options: -threaded -rtsopts "-with-rtsopts=-N -I0 -qg"
|
||||
-O2 -Werror -Wall -fwarn-identities
|
||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||
@@ -215,7 +263,6 @@ test-suite spec
|
||||
Feature.Auth.AudienceJwtSecretSpec
|
||||
Feature.Auth.AuthSpec
|
||||
Feature.Auth.BinaryJwtSecretSpec
|
||||
Feature.Auth.JwtCacheSpec
|
||||
Feature.Auth.NoAnonSpec
|
||||
Feature.Auth.NoJwtSecretSpec
|
||||
Feature.ConcurrentSpec
|
||||
@@ -245,7 +292,9 @@ test-suite spec
|
||||
Feature.Query.PgSafeUpdateSpec
|
||||
Feature.Query.PlanSpec
|
||||
Feature.Query.PostGISSpec
|
||||
Feature.Query.PreferencesSpec
|
||||
Feature.Query.Preferences.HandlingSpec
|
||||
Feature.Query.Preferences.MaxAffectedSpec
|
||||
Feature.Query.Preferences.TimezoneSpec
|
||||
Feature.Query.QueryLimitedSpec
|
||||
Feature.Query.QuerySpec
|
||||
Feature.Query.RangeSpec
|
||||
@@ -261,16 +310,16 @@ test-suite spec
|
||||
Feature.RollbackSpec
|
||||
Feature.RpcPreRequestGucsSpec
|
||||
SpecHelper
|
||||
build-depends: base >= 4.9 && < 4.20
|
||||
build-depends: base >= 4.9 && < 4.22
|
||||
, aeson >= 2.0.3 && < 2.3
|
||||
, aeson-qq >= 0.8.1 && < 0.9
|
||||
, async >= 2.1.1 && < 2.3
|
||||
, base64-bytestring >= 1 && < 1.3
|
||||
, bytestring >= 0.10.8 && < 0.13
|
||||
, case-insensitive >= 1.2 && < 1.3
|
||||
, containers >= 0.5.7 && < 0.7
|
||||
, hasql-pool >= 1.0.1 && < 1.1
|
||||
, hasql-transaction >= 1.0.1 && < 1.2
|
||||
, containers >= 0.5.7 && < 0.8
|
||||
, hasql-pool >= 1.0.1 && <= 1.3.0.4
|
||||
, hasql-transaction >= 1.0.1 && <= 1.2.1
|
||||
, heredoc >= 0.2 && < 0.3
|
||||
, hspec >= 2.3 && < 2.12
|
||||
, hspec-expectations >= 0.8.4 && < 0.9
|
||||
@@ -284,7 +333,7 @@ test-suite spec
|
||||
, postgrest
|
||||
, process >= 1.4.2 && < 1.7
|
||||
, prometheus-client >= 1.1.1 && < 1.2.0
|
||||
, protolude >= 0.3.1 && < 0.4
|
||||
, protolude
|
||||
, regex-tdfa >= 1.2.2 && < 1.4
|
||||
, scientific >= 0.3.4 && < 0.4
|
||||
, text >= 1.2.2 && < 2.2
|
||||
@@ -298,6 +347,40 @@ test-suite spec
|
||||
-- https://github.com/PostgREST/postgrest/issues/387
|
||||
-with-rtsopts=-K33K
|
||||
|
||||
test-suite observability
|
||||
type: exitcode-stdio-1.0
|
||||
default-language: Haskell2010
|
||||
default-extensions: OverloadedStrings
|
||||
QuasiQuotes
|
||||
NoImplicitPrelude
|
||||
hs-source-dirs: test/observability
|
||||
main-is: Main.hs
|
||||
other-modules: ObsHelper
|
||||
Observation.JwtCache
|
||||
Observation.MetricsSpec
|
||||
Observation.SchemaCacheSpec
|
||||
build-depends: base >= 4.9 && < 4.22
|
||||
, base64-bytestring >= 1 && < 1.3
|
||||
, bytestring >= 0.10.8 && < 0.13
|
||||
, hasql-pool >= 1.0.1 && <= 1.3.0.4
|
||||
, hasql-transaction >= 1.0.1 && <= 1.2.1
|
||||
, hspec >= 2.3 && < 2.12
|
||||
, hspec-expectations >= 0.8.4 && < 0.9
|
||||
, hspec-wai >= 0.10 && < 0.12
|
||||
, hspec-wai-json >= 0.10 && < 0.12
|
||||
, http-types >= 0.12.3 && < 0.13
|
||||
, jose-jwt >= 0.9.6 && < 0.11
|
||||
, postgrest
|
||||
, prometheus-client >= 1.1.1 && < 1.2.0
|
||||
, protolude
|
||||
, text >= 1.2.2 && < 2.2
|
||||
, wai >= 3.2.1 && < 3.3
|
||||
ghc-options: -threaded -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=-K33K
|
||||
|
||||
test-suite doctests
|
||||
type: exitcode-stdio-1.0
|
||||
default-language: Haskell2010
|
||||
@@ -305,10 +388,10 @@ test-suite doctests
|
||||
NoImplicitPrelude
|
||||
hs-source-dirs: test/doc
|
||||
main-is: Main.hs
|
||||
build-depends: base >= 4.9 && < 4.20
|
||||
build-depends: base >= 4.9 && < 4.22
|
||||
, doctest >= 0.8
|
||||
, postgrest
|
||||
, pretty-simple
|
||||
, protolude >= 0.3.1 && < 0.4
|
||||
, protolude
|
||||
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
|
||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||
|
||||
@@ -7,11 +7,9 @@
|
||||
# We highly recommend that use the PostgREST binary cache by installing cachix
|
||||
# (https://app.cachix.org/) and running `cachix use postgrest`.
|
||||
{ docker ? false
|
||||
, postgrest ? import ./default.nix { }
|
||||
}:
|
||||
let
|
||||
postgrest =
|
||||
import ./default.nix { };
|
||||
|
||||
inherit (postgrest) pkgs;
|
||||
|
||||
inherit (pkgs) lib;
|
||||
@@ -37,10 +35,7 @@ lib.overrideDerivation postgrest.env (
|
||||
buildInputs =
|
||||
base.buildInputs ++ [
|
||||
pkgs.cabal-install
|
||||
pkgs.cabal2nix
|
||||
pkgs.git
|
||||
pkgs.postgresql
|
||||
pkgs.update-nix-fetchgit
|
||||
postgrest.hsie.bin
|
||||
]
|
||||
++ toolboxes;
|
||||
@@ -49,6 +44,10 @@ lib.overrideDerivation postgrest.env (
|
||||
''
|
||||
export HISTFILE=.history
|
||||
|
||||
# Bypass proxy for all hosts, it prevents HTTP client failures used in test
|
||||
# suites. See: https://github.com/PostgREST/postgrest/issues/4633 for more info
|
||||
export NO_PROXY=*
|
||||
|
||||
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}
|
||||
|
||||
@@ -19,22 +19,23 @@ import PostgREST.Observation (Observation (..))
|
||||
|
||||
import qualified PostgREST.AppState as AppState
|
||||
|
||||
import Protolude
|
||||
import qualified Network.Socket as NS
|
||||
import Protolude
|
||||
|
||||
runAdmin :: AppState -> Warp.Settings -> IO ()
|
||||
runAdmin appState settings = do
|
||||
whenJust (AppState.getSocketAdmin appState) $ \adminSocket -> do
|
||||
runAdmin :: AppState -> Maybe NS.Socket -> NS.Socket -> Warp.Settings -> IO ()
|
||||
runAdmin appState maybeAdminSocket socketREST settings = do
|
||||
whenJust maybeAdminSocket $ \adminSocket -> do
|
||||
address <- resolveSocketToAddress adminSocket
|
||||
observer $ AdminStartObs address
|
||||
void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp
|
||||
where
|
||||
adminApp = admin appState
|
||||
adminApp = admin appState socketREST
|
||||
observer = AppState.getObserver appState
|
||||
|
||||
-- | PostgREST admin application
|
||||
admin :: AppState.AppState -> Wai.Application
|
||||
admin appState req respond = do
|
||||
isMainAppReachable <- isRight <$> reachMainApp (AppState.getSocketREST appState)
|
||||
admin :: AppState.AppState -> NS.Socket -> Wai.Application
|
||||
admin appState socketREST req respond = do
|
||||
isMainAppReachable <- isRight <$> reachMainApp socketREST
|
||||
isLoaded <- AppState.isLoaded appState
|
||||
isPending <- AppState.isPending appState
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ data ApiRequest = ApiRequest {
|
||||
, iPayload :: Maybe Payload -- ^ Data sent by client and used for mutation actions
|
||||
, iPreferences :: Preferences.Preferences -- ^ Prefer header values
|
||||
, iQueryParams :: QueryParams.QueryParams
|
||||
, iColumns :: S.Set FieldName -- ^ parsed colums from &columns parameter and payload
|
||||
, iColumns :: S.Set FieldName -- ^ parsed columns from &columns parameter and payload
|
||||
, iHeaders :: [(ByteString, ByteString)] -- ^ HTTP request headers
|
||||
, iCookies :: [(ByteString, ByteString)] -- ^ Request Cookies
|
||||
, iPath :: ByteString -- ^ Raw request path
|
||||
|
||||
+117
-37
@@ -9,18 +9,25 @@ Some of its functionality includes:
|
||||
- Producing HTTP Headers according to RFCs.
|
||||
- Content Negotiation
|
||||
-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
module PostgREST.App
|
||||
( postgrest
|
||||
, run
|
||||
) where
|
||||
|
||||
|
||||
import GHC.IO.Exception (IOErrorType (..))
|
||||
import System.IO.Error (ioeGetErrorType)
|
||||
|
||||
import Control.Monad.Except (liftEither)
|
||||
import Control.Monad.Extra (whenJust)
|
||||
import Data.Either.Combinators (mapLeft, whenLeft)
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.String (IsString (..))
|
||||
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
|
||||
import Network.Wai.Handler.Warp (defaultSettings, setHost,
|
||||
setOnException, setPort,
|
||||
setServerName)
|
||||
|
||||
import qualified Data.Text.Encoding as T
|
||||
@@ -51,35 +58,61 @@ import PostgREST.Observation (Observation (..))
|
||||
import PostgREST.Response.Performance (ServerTiming (..),
|
||||
serverTimingHeader)
|
||||
import PostgREST.SchemaCache (SchemaCache (..))
|
||||
import PostgREST.TimeIt (timeItT)
|
||||
import PostgREST.Version (docsVersion, prettyVersion)
|
||||
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.List as L
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import Protolude hiding (Handler)
|
||||
import System.TimeIt (timeItT)
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.List as L
|
||||
import Data.Streaming.Network (bindPortTCP,
|
||||
bindRandomPortTCP)
|
||||
import qualified Data.Text as T
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Network.HTTP.Types.Header as HTTP (hVary)
|
||||
import qualified Network.Socket as NS
|
||||
import PostgREST.Unix (createAndBindDomainSocket)
|
||||
import Protolude hiding (Handler)
|
||||
|
||||
type Handler = ExceptT Error
|
||||
|
||||
run :: AppState -> IO ()
|
||||
run appState = do
|
||||
let observer = AppState.getObserver appState
|
||||
conf@AppConfig{..} <- AppState.getConfig appState
|
||||
|
||||
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
|
||||
Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
|
||||
(mainSocket, adminSocket) <- initSockets conf
|
||||
let closeSockets = do
|
||||
whenJust adminSocket NS.close
|
||||
NS.close mainSocket
|
||||
Unix.installSignalHandlers observer closeSockets (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
|
||||
|
||||
Listener.runListener appState
|
||||
|
||||
Admin.runAdmin appState (serverSettings conf)
|
||||
Admin.runAdmin appState adminSocket mainSocket (serverSettings conf)
|
||||
|
||||
let app = postgrest configLogLevel appState (AppState.schemaCacheLoader appState)
|
||||
|
||||
do
|
||||
address <- resolveSocketToAddress (AppState.getSocketREST appState)
|
||||
address <- resolveSocketToAddress mainSocket
|
||||
observer $ AppServerAddressObs address
|
||||
|
||||
Warp.runSettingsSocket (serverSettings conf) (AppState.getSocketREST appState) app
|
||||
Warp.runSettingsSocket (serverSettings conf & setOnException onWarpException) mainSocket app
|
||||
where
|
||||
observer = AppState.getObserver appState
|
||||
|
||||
onWarpException :: Maybe Wai.Request -> SomeException -> IO ()
|
||||
onWarpException _ ex =
|
||||
when (shouldDisplayException ex) $
|
||||
observer $ WarpServerObs $ show ex
|
||||
|
||||
-- Similar to wai defaultShouldDisplayException in
|
||||
-- https://github.com/yesodweb/wai//blob/8c3882c60f6abe043889fc20c7efd3fa9747fa4a/warp/Network/Wai/Handler/Warp/Settings.hs#L251-L258
|
||||
-- but without omitting AsyncException since it's important to log for ThreadKilled, StackOverflow and other cases.
|
||||
-- We want to reuse this to avoid flooding the logs for some transient failure cases.
|
||||
shouldDisplayException :: SomeException -> Bool
|
||||
shouldDisplayException se
|
||||
| Just (_ :: Warp.InvalidRequest) <- fromException se = False
|
||||
| Just (ioeGetErrorType -> et) <- fromException se, et == ResourceVanished || et == InvalidArgument = False
|
||||
| otherwise = True
|
||||
|
||||
serverSettings :: AppConfig -> Warp.Settings
|
||||
serverSettings AppConfig{..} =
|
||||
@@ -97,30 +130,31 @@ postgrest logLevel appState connWorker =
|
||||
Logger.middleware logLevel Auth.getRole $
|
||||
-- fromJust can be used, because the auth middleware will **always** add
|
||||
-- some AuthResult to the vault.
|
||||
\req respond -> case fromJust $ Auth.getResult req of
|
||||
Left err -> respond $ Error.errorResponseFor err
|
||||
Right authResult -> do
|
||||
appConf <- AppState.getConfig appState -- the config must be read again because it can reload
|
||||
maybeSchemaCache <- AppState.getSchemaCache appState
|
||||
\req respond -> do
|
||||
appConf@AppConfig{..} <- AppState.getConfig appState -- the config must be read again because it can reload
|
||||
case fromJust $ Auth.getResult req of
|
||||
Left err -> respond $ Error.errorResponseFor configClientErrorVerbosity err
|
||||
Right authResult -> do
|
||||
maybeSchemaCache <- AppState.getSchemaCache appState
|
||||
|
||||
let
|
||||
eitherResponse :: IO (Either Error Wai.Response)
|
||||
eitherResponse =
|
||||
runExceptT $ postgrestResponse appState appConf maybeSchemaCache authResult req
|
||||
let
|
||||
eitherResponse :: IO (Either Error Wai.Response)
|
||||
eitherResponse =
|
||||
runExceptT $ postgrestResponse appState appConf maybeSchemaCache authResult req
|
||||
|
||||
response <- either Error.errorResponseFor identity <$> eitherResponse
|
||||
-- Launch the connWorker when the connection is down. The postgrest
|
||||
-- function can respond successfully (with a stale schema cache) before
|
||||
-- the connWorker is done. However, when there's an empty schema cache
|
||||
-- postgrest responds with the error `PGRST002`; this means that the schema
|
||||
-- cache is still loading, so we don't launch the connWorker here because
|
||||
-- it would duplicate the loading process, e.g. https://github.com/PostgREST/postgrest/issues/3704
|
||||
-- TODO: this process may be unnecessary when the Listener is enabled. Revisit once https://github.com/PostgREST/postgrest/issues/1766 is done
|
||||
when (isServiceUnavailable response && isJust maybeSchemaCache) connWorker
|
||||
resp <- do
|
||||
delay <- AppState.getNextDelay appState
|
||||
return $ addRetryHint delay response
|
||||
respond resp
|
||||
response <- either (Error.errorResponseFor configClientErrorVerbosity) identity <$> eitherResponse
|
||||
-- Launch the connWorker when the connection is down. The postgrest
|
||||
-- function can respond successfully (with a stale schema cache) before
|
||||
-- the connWorker is done. However, when there's an empty schema cache
|
||||
-- postgrest responds with the error `PGRST002`; this means that the schema
|
||||
-- cache is still loading, so we don't launch the connWorker here because
|
||||
-- it would duplicate the loading process, e.g. https://github.com/PostgREST/postgrest/issues/3704
|
||||
-- TODO: this process may be unnecessary when the Listener is enabled. Revisit once https://github.com/PostgREST/postgrest/issues/1766 is done
|
||||
when (isServiceUnavailable response && isJust maybeSchemaCache) connWorker
|
||||
resp <- do
|
||||
delay <- AppState.getNextDelay appState
|
||||
return $ addRetryHint delay response
|
||||
respond resp
|
||||
|
||||
postgrestResponse
|
||||
:: AppState.AppState
|
||||
@@ -146,7 +180,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache authResult@AuthRe
|
||||
timezones = dbTimezones sCache
|
||||
prefs = ApiRequest.userPreferences conf req timezones
|
||||
|
||||
(parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestError $ ApiRequest.userApiRequest conf prefs req body
|
||||
(parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body
|
||||
(planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
|
||||
|
||||
let mainQ = Query.mainQuery plan conf apiReq authResult configDbPreRequest
|
||||
@@ -167,7 +201,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache authResult@AuthRe
|
||||
liftEither eitherResp
|
||||
|
||||
(respTime, resp) <- withTiming $ do
|
||||
let response = Response.actionResponse txResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache iSchema iNegotiatedByProfile
|
||||
let response = Response.actionResponse txResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache
|
||||
status' = either Error.status Response.pgrstStatus response
|
||||
|
||||
-- TODO: see above obsQuery, only this obsQuery should remain after refactoring (because the QueryObs depends on the status)
|
||||
@@ -178,7 +212,17 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache authResult@AuthRe
|
||||
|
||||
where
|
||||
toWaiResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response
|
||||
toWaiResponse timing (Response.PgrstResponse st hdrs bod) = Wai.responseLBS st (hdrs ++ ([serverTimingHeader timing | configServerTimingEnabled])) bod
|
||||
toWaiResponse timing (Response.PgrstResponse st hdrs bod) =
|
||||
Wai.responseLBS st (hdrs ++ serverTimingHeaders timing ++ [varyHeader | not $ varyHeaderPresent hdrs]) bod
|
||||
|
||||
serverTimingHeaders :: ServerTiming -> [HTTP.Header]
|
||||
serverTimingHeaders timing = [serverTimingHeader timing | configServerTimingEnabled]
|
||||
|
||||
varyHeader :: HTTP.Header
|
||||
varyHeader = (HTTP.hVary, "Accept, Prefer, Range")
|
||||
|
||||
varyHeaderPresent :: [HTTP.Header] -> Bool
|
||||
varyHeaderPresent = any (\(h, _v) -> h == HTTP.hVary)
|
||||
|
||||
withTiming :: Handler IO a -> Handler IO (Maybe Double, a)
|
||||
withTiming f = if configServerTimingEnabled
|
||||
@@ -206,3 +250,39 @@ addRetryHint delay response = do
|
||||
|
||||
isServiceUnavailable :: Wai.Response -> Bool
|
||||
isServiceUnavailable response = Wai.responseStatus response == HTTP.status503
|
||||
|
||||
type AppSockets = (NS.Socket, Maybe NS.Socket)
|
||||
|
||||
initSockets :: AppConfig -> IO AppSockets
|
||||
initSockets AppConfig{..} = do
|
||||
let
|
||||
cfg'usp = configServerUnixSocket
|
||||
cfg'uspm = configServerUnixSocketMode
|
||||
cfg'host = configServerHost
|
||||
cfg'port = configServerPort
|
||||
cfg'adminHost = configAdminServerHost
|
||||
cfg'adminPort = configAdminServerPort
|
||||
|
||||
sock <- case cfg'usp of
|
||||
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
|
||||
-- but we need to have runtime error if we try to use it in Windows, not compile time error
|
||||
Just path -> createAndBindDomainSocket path cfg'uspm
|
||||
Nothing -> do
|
||||
(_, sock) <-
|
||||
if cfg'port /= 0
|
||||
then do
|
||||
sock <- bindPortTCP cfg'port (fromString $ T.unpack cfg'host)
|
||||
pure (cfg'port, sock)
|
||||
else do
|
||||
-- explicitly bind to a random port, returning bound port number
|
||||
(num, sock) <- bindRandomPortTCP (fromString $ T.unpack cfg'host)
|
||||
pure (num, sock)
|
||||
pure sock
|
||||
|
||||
adminSock <- case cfg'adminPort of
|
||||
Just adminPort -> do
|
||||
adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'adminHost)
|
||||
pure $ Just adminSock
|
||||
Nothing -> pure Nothing
|
||||
|
||||
pure (sock, adminSock)
|
||||
|
||||
+89
-131
@@ -1,6 +1,7 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE RecursiveDo #-}
|
||||
|
||||
module PostgREST.AppState
|
||||
( AppState
|
||||
@@ -13,11 +14,9 @@ module PostgREST.AppState
|
||||
, getNextListenerDelay
|
||||
, getTime
|
||||
, getJwtCacheState
|
||||
, getSocketREST
|
||||
, getSocketAdmin
|
||||
, init
|
||||
, initSockets
|
||||
, initWithPool
|
||||
, putConfig -- For tests TODO refactoring
|
||||
, putNextListenerDelay
|
||||
, putSchemaCache
|
||||
, putPgVersion
|
||||
@@ -32,24 +31,21 @@ module PostgREST.AppState
|
||||
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import Data.Either.Combinators (whenLeft)
|
||||
import qualified Data.Text as T (unpack)
|
||||
import qualified Hasql.Pool as SQL
|
||||
import qualified Hasql.Pool.Config as SQL
|
||||
import qualified Hasql.Session as SQL
|
||||
import qualified Hasql.Transaction.Sessions as SQL
|
||||
import qualified Network.HTTP.Types.Status as HTTP
|
||||
import qualified Network.Socket as NS
|
||||
import qualified PostgREST.Auth.JwtCache as JwtCache
|
||||
import qualified PostgREST.Error as Error
|
||||
import qualified PostgREST.Logger as Logger
|
||||
import qualified PostgREST.Metrics as Metrics
|
||||
import PostgREST.Observation
|
||||
import PostgREST.TimeIt (timeItT)
|
||||
import PostgREST.Version (prettyVersion)
|
||||
import System.TimeIt (timeItT)
|
||||
|
||||
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
|
||||
updateAction)
|
||||
import Control.Debounce
|
||||
import Control.Retry (RetryPolicy, RetryStatus (..), capDelay,
|
||||
exponentialBackoff, retrying,
|
||||
rsPreviousDelay)
|
||||
@@ -59,21 +55,19 @@ import Data.Time.Clock (UTCTime, getCurrentTime)
|
||||
|
||||
import PostgREST.Auth.JwtCache (JwtCacheState, update)
|
||||
import PostgREST.Config (AppConfig (..),
|
||||
addFallbackAppName,
|
||||
readAppConfig)
|
||||
readAppConfig,
|
||||
toConnectionSettings)
|
||||
import PostgREST.Config.Database (queryDbSettings,
|
||||
queryPgVersion,
|
||||
queryRoleSettings)
|
||||
import PostgREST.Config.PgVersion (PgVersion (..),
|
||||
minimumPgVersion)
|
||||
import PostgREST.Debounce (makeDebouncer)
|
||||
import PostgREST.SchemaCache (SchemaCache (..),
|
||||
querySchemaCache,
|
||||
showSummary)
|
||||
import PostgREST.SchemaCache.Identifiers (quoteQi)
|
||||
import PostgREST.Unix (createAndBindDomainSocket)
|
||||
|
||||
import Data.Streaming.Network (bindPortTCP, bindRandomPortTCP)
|
||||
import Data.String (IsString (..))
|
||||
import Protolude
|
||||
|
||||
data AppState = AppState
|
||||
@@ -84,7 +78,7 @@ data AppState = AppState
|
||||
-- | Schema cache
|
||||
, stateSchemaCache :: IORef (Maybe SchemaCache)
|
||||
-- | The schema cache status
|
||||
, stateSCacheStatus :: IORef SchemaCacheStatus
|
||||
, stateSCacheStatus :: SchemaCacheStatus
|
||||
-- | State of the LISTEN channel
|
||||
, stateIsListenerOn :: IORef Bool
|
||||
-- | starts the connection worker with a debounce
|
||||
@@ -99,10 +93,6 @@ data AppState = AppState
|
||||
, stateNextDelay :: IORef Int
|
||||
-- | Keeps track of the next delay for the listener
|
||||
, stateNextListenerDelay :: IORef Int
|
||||
-- | Network socket for REST API
|
||||
, stateSocketREST :: NS.Socket
|
||||
-- | Network socket for the admin UI
|
||||
, stateSocketAdmin :: Maybe NS.Socket
|
||||
-- | Observation handler
|
||||
, stateObserver :: ObservationHandler
|
||||
-- | JWT Cache
|
||||
@@ -111,13 +101,11 @@ data AppState = AppState
|
||||
, stateMetrics :: Metrics.MetricsState
|
||||
}
|
||||
|
||||
-- | Schema cache status
|
||||
data SchemaCacheStatus
|
||||
= SCLoaded
|
||||
| SCPending
|
||||
deriving Eq
|
||||
|
||||
type AppSockets = (NS.Socket, Maybe NS.Socket)
|
||||
-- | Schema cache status.
|
||||
-- Empty means pending and full means loaded.
|
||||
newtype SchemaCacheStatus = SchemaCacheStatus
|
||||
{ getSCStatusMVar :: MVar ()
|
||||
}
|
||||
|
||||
init :: AppConfig -> IO AppState
|
||||
init conf@AppConfig{configLogLevel, configDbPoolSize} = do
|
||||
@@ -128,107 +116,74 @@ init conf@AppConfig{configLogLevel, configDbPoolSize} = do
|
||||
observer $ AppStartObs prettyVersion
|
||||
|
||||
pool <- initPool conf observer
|
||||
(sock, adminSock) <- initSockets conf
|
||||
state' <- initWithPool (sock, adminSock) pool conf loggerState metricsState observer
|
||||
pure state' { stateSocketREST = sock, stateSocketAdmin = adminSock}
|
||||
initWithPool pool conf loggerState metricsState observer
|
||||
|
||||
initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
|
||||
initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
|
||||
initWithPool :: SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
|
||||
initWithPool pool conf loggerState metricsState observer = mdo
|
||||
|
||||
appState <- AppState pool
|
||||
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
||||
<*> newIORef Nothing
|
||||
<*> newIORef SCPending
|
||||
<*> newSchemaCacheStatus
|
||||
<*> newIORef False
|
||||
<*> pure (pure ())
|
||||
<*> makeDebouncer (retryingSchemaCacheLoad appState *> threadDelay 100000) -- 100ms cooldown
|
||||
<*> newIORef conf
|
||||
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
|
||||
<*> myThreadId
|
||||
<*> newIORef 0
|
||||
<*> newIORef 1
|
||||
<*> pure sock
|
||||
<*> pure adminSock
|
||||
<*> pure observer
|
||||
<*> JwtCache.init conf observer
|
||||
<*> pure loggerState
|
||||
<*> pure metricsState
|
||||
|
||||
deb <-
|
||||
let decisecond = 100000 in
|
||||
mkDebounce defaultDebounceSettings
|
||||
{ debounceAction = retryingSchemaCacheLoad appState
|
||||
, debounceFreq = decisecond
|
||||
, debounceEdge = leadingEdge -- runs the worker at the start and the end
|
||||
}
|
||||
|
||||
return appState { debouncedSCacheLoader = deb}
|
||||
return appState
|
||||
|
||||
destroy :: AppState -> IO ()
|
||||
destroy = destroyPool
|
||||
|
||||
initSockets :: AppConfig -> IO AppSockets
|
||||
initSockets AppConfig{..} = do
|
||||
let
|
||||
cfg'usp = configServerUnixSocket
|
||||
cfg'uspm = configServerUnixSocketMode
|
||||
cfg'host = configServerHost
|
||||
cfg'port = configServerPort
|
||||
cfg'adminHost = configAdminServerHost
|
||||
cfg'adminPort = configAdminServerPort
|
||||
|
||||
sock <- case cfg'usp of
|
||||
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
|
||||
-- but we need to have runtime error if we try to use it in Windows, not compile time error
|
||||
Just path -> createAndBindDomainSocket path cfg'uspm
|
||||
Nothing -> do
|
||||
(_, sock) <-
|
||||
if cfg'port /= 0
|
||||
then do
|
||||
sock <- bindPortTCP cfg'port (fromString $ T.unpack cfg'host)
|
||||
pure (cfg'port, sock)
|
||||
else do
|
||||
-- explicitly bind to a random port, returning bound port number
|
||||
(num, sock) <- bindRandomPortTCP (fromString $ T.unpack cfg'host)
|
||||
pure (num, sock)
|
||||
pure sock
|
||||
|
||||
adminSock <- case cfg'adminPort of
|
||||
Just adminPort -> do
|
||||
adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'adminHost)
|
||||
pure $ Just adminSock
|
||||
Nothing -> pure Nothing
|
||||
|
||||
pure (sock, adminSock)
|
||||
|
||||
initPool :: AppConfig -> ObservationHandler -> IO SQL.Pool
|
||||
initPool AppConfig{..} observer = do
|
||||
initPool cfg@AppConfig{..} observer = do
|
||||
SQL.acquire $ SQL.settings
|
||||
[ SQL.size configDbPoolSize
|
||||
, SQL.acquisitionTimeout $ fromIntegral configDbPoolAcquisitionTimeout
|
||||
, SQL.agingTimeout $ fromIntegral configDbPoolMaxLifetime
|
||||
, SQL.idlenessTimeout $ fromIntegral configDbPoolMaxIdletime
|
||||
, SQL.staticConnectionSettings (toUtf8 $ addFallbackAppName prettyVersion configDbUri)
|
||||
, SQL.staticConnectionSettings $ toConnectionSettings identity cfg
|
||||
, SQL.observationHandler $ observer . HasqlPoolObs
|
||||
]
|
||||
|
||||
-- | Run an action with a database connection.
|
||||
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
|
||||
usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} sess = do
|
||||
observer PoolRequest
|
||||
observer PoolRequest
|
||||
|
||||
res <- SQL.use statePool sess
|
||||
res <- SQL.use statePool sess
|
||||
|
||||
observer PoolRequestFullfilled
|
||||
observer PoolRequestFullfilled
|
||||
|
||||
whenLeft res (\case
|
||||
SQL.AcquisitionTimeoutUsageError ->
|
||||
observer $ PoolAcqTimeoutObs SQL.AcquisitionTimeoutUsageError
|
||||
err@(SQL.ConnectionUsageError e) ->
|
||||
let failureMessage = BS.unpack $ fromMaybe mempty e in
|
||||
when (("FATAL: password authentication failed" `isInfixOf` failureMessage) || ("no password supplied" `isInfixOf` failureMessage)) $ do
|
||||
observer $ ExitDBFatalError ServerAuthError err
|
||||
killThread mainThreadId
|
||||
err@(SQL.SessionUsageError (SQL.QueryError tpl _ (SQL.ResultError resultErr))) -> do
|
||||
whenLeft res (\case
|
||||
SQL.AcquisitionTimeoutUsageError ->
|
||||
observer PoolAcqTimeoutObs
|
||||
err@(SQL.ConnectionUsageError e) ->
|
||||
let failureMessage = BS.unpack $ fromMaybe mempty e in
|
||||
when (("FATAL: password authentication failed" `isInfixOf` failureMessage) || ("no password supplied" `isInfixOf` failureMessage)) $ do
|
||||
observer $ ExitDBFatalError ServerAuthError err
|
||||
killThread mainThreadId
|
||||
err@(SQL.SessionUsageError (SQL.QueryError tpl _ (SQL.ResultError resultErr))) ->
|
||||
handleResultError err tpl resultErr
|
||||
err@(SQL.SessionUsageError (SQL.PipelineError (SQL.ResultError resultErr))) ->
|
||||
-- Passing the empty template will not work for schema cache queries, see TODO further below.
|
||||
handleResultError err mempty resultErr
|
||||
err@(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) ->
|
||||
-- An error on the client-side, usually indicates problems with connection
|
||||
observer $ QueryErrorCodeHighObs err
|
||||
SQL.SessionUsageError (SQL.PipelineError (SQL.ClientError _)) -> pure ()
|
||||
)
|
||||
|
||||
return res
|
||||
where
|
||||
handleResultError err tpl resultErr = do
|
||||
case resultErr of
|
||||
SQL.UnexpectedResult{} -> do
|
||||
observer $ ExitDBFatalError ServerPgrstBug err
|
||||
@@ -261,19 +216,17 @@ usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} ses
|
||||
SQL.ServerError{} ->
|
||||
when (Error.status (Error.PgError False err) >= HTTP.status500) $
|
||||
observer $ QueryErrorCodeHighObs err
|
||||
err@(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) ->
|
||||
-- An error on the client-side, usually indicates problems wth connection
|
||||
observer $ QueryErrorCodeHighObs err
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
-- | Flush the connection pool so that any future use of the pool will
|
||||
-- use connections freshly established after this call.
|
||||
-- | Emits PoolFlushed observation
|
||||
flushPool :: AppState -> IO ()
|
||||
flushPool AppState{..} = SQL.release statePool
|
||||
flushPool AppState{..} = do
|
||||
SQL.release statePool
|
||||
stateObserver PoolFlushed
|
||||
|
||||
-- | Destroy the pool on shutdown.
|
||||
-- | Differs from flushPool in not emiting PoolFlushed observation.
|
||||
destroyPool :: AppState -> IO ()
|
||||
destroyPool AppState{..} = SQL.release statePool
|
||||
|
||||
@@ -313,12 +266,6 @@ getTime = stateGetTime
|
||||
getJwtCacheState :: AppState -> JwtCacheState
|
||||
getJwtCacheState = stateJwtCache
|
||||
|
||||
getSocketREST :: AppState -> NS.Socket
|
||||
getSocketREST = stateSocketREST
|
||||
|
||||
getSocketAdmin :: AppState -> Maybe NS.Socket
|
||||
getSocketAdmin = stateSocketAdmin
|
||||
|
||||
getMainThreadId :: AppState -> ThreadId
|
||||
getMainThreadId = stateMainThreadId
|
||||
|
||||
@@ -335,18 +282,15 @@ putIsListenerOn = atomicWriteIORef . stateIsListenerOn
|
||||
|
||||
isLoaded :: AppState -> IO Bool
|
||||
isLoaded x = do
|
||||
scacheStatus <- readIORef $ stateSCacheStatus x
|
||||
scacheLoaded <- isSchemaCacheLoaded x
|
||||
connEstablished <- isConnEstablished x
|
||||
return $ scacheStatus == SCLoaded && connEstablished
|
||||
return $ scacheLoaded && connEstablished
|
||||
|
||||
isPending :: AppState -> IO Bool
|
||||
isPending x = do
|
||||
scacheStatus <- readIORef $ stateSCacheStatus x
|
||||
scacheLoaded <- isSchemaCacheLoaded x
|
||||
connEstablished <- isConnEstablished x
|
||||
return $ scacheStatus == SCPending || not connEstablished
|
||||
|
||||
putSCacheStatus :: AppState -> SchemaCacheStatus -> IO ()
|
||||
putSCacheStatus = atomicWriteIORef . stateSCacheStatus
|
||||
return $ not scacheLoaded || not connEstablished
|
||||
|
||||
getObserver :: AppState -> ObservationHandler
|
||||
getObserver = stateObserver
|
||||
@@ -364,9 +308,6 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
||||
when (rsIterNumber > 0) $ do
|
||||
let delay = fromMaybe 0 rsPreviousDelay `div` oneSecondInUs
|
||||
observer $ ConnectionRetryObs delay
|
||||
putNextListenerDelay appState delay
|
||||
|
||||
flushPool appState
|
||||
|
||||
(,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache)
|
||||
)
|
||||
@@ -374,7 +315,7 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
||||
qPgVersion :: IO (Maybe PgVersion)
|
||||
qPgVersion = do
|
||||
AppConfig{..} <- getConfig appState
|
||||
pgVersion <- usePool appState (queryPgVersion False) -- No need to prepare the query here, as the connection might not be established
|
||||
pgVersion <- usePool appState queryPgVersion
|
||||
case pgVersion of
|
||||
Left e -> do
|
||||
observer $ QueryPgVersionError e
|
||||
@@ -382,14 +323,16 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
||||
observer ExitDBNoRecoveryObs
|
||||
killThread mainThreadId
|
||||
return Nothing
|
||||
Right actualPgVersion -> do
|
||||
when (actualPgVersion < minimumPgVersion) $ do
|
||||
Right actualPgVersion ->
|
||||
if actualPgVersion < minimumPgVersion then do
|
||||
observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion
|
||||
killThread mainThreadId
|
||||
observer $ DBConnectedObs $ pgvFullName actualPgVersion
|
||||
observer $ PoolInit configDbPoolSize
|
||||
putPgVersion appState actualPgVersion
|
||||
return $ Just actualPgVersion
|
||||
return Nothing
|
||||
else do
|
||||
observer $ DBConnectedObs $ pgvFullName actualPgVersion
|
||||
observer $ PoolInit configDbPoolSize
|
||||
putPgVersion appState actualPgVersion
|
||||
return $ Just actualPgVersion
|
||||
|
||||
qInDbConfig :: IO ()
|
||||
qInDbConfig = do
|
||||
@@ -400,24 +343,27 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
||||
qSchemaCache = do
|
||||
conf@AppConfig{..} <- getConfig appState
|
||||
(resultTime, result) <-
|
||||
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
|
||||
timeItT $ usePool appState (transaction SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
|
||||
timeItT $ usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
|
||||
case result of
|
||||
Left e -> do
|
||||
putSCacheStatus appState SCPending
|
||||
markSchemaCachePending appState
|
||||
putSchemaCache appState Nothing
|
||||
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
|
||||
return Nothing
|
||||
|
||||
Right sCache -> do
|
||||
-- IMPORTANT: While the pending schema cache state starts from running the above querySchemaCache, only at this stage we block API requests due to the usage of an
|
||||
-- IORef on putSchemaCache. This is why SCacheStatus is put at SCPending here to signal the Admin server (using isPending) that we're on a recovery state.
|
||||
putSCacheStatus appState SCPending
|
||||
-- IORef on putSchemaCache. This is why schema cache status is marked as pending here to signal the Admin server (using isPending) that we're on a recovery state.
|
||||
markSchemaCachePending appState
|
||||
putSchemaCache appState $ Just sCache
|
||||
observer $ SchemaCacheQueriedObs resultTime
|
||||
(t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache
|
||||
observer $ SchemaCacheLoadedObs t
|
||||
putSCacheStatus appState SCLoaded
|
||||
(loadTime, summary) <- timeItT (evaluate $ showSummary sCache)
|
||||
-- Flush the pool after loading the schema cache to reset any stale session cache entries
|
||||
-- We do it after successfully querying the schema cache (because this can fail and during retries we would flush the pool repeatedly unnecessarily)
|
||||
-- and after marking sCacheStatus as pending,
|
||||
flushPool appState
|
||||
observer $ SchemaCacheQueriedObs resultTime $ dbQueryTimings sCache
|
||||
observer $ SchemaCacheLoadedObs loadTime summary
|
||||
markSchemaCacheLoaded appState
|
||||
return $ Just sCache
|
||||
|
||||
shouldRetry :: RetryStatus -> (Maybe PgVersion, Maybe SchemaCache) -> IO Bool
|
||||
@@ -433,6 +379,18 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
||||
|
||||
oneSecondInUs = 1000000 -- one second in microseconds
|
||||
|
||||
newSchemaCacheStatus :: IO SchemaCacheStatus
|
||||
newSchemaCacheStatus = SchemaCacheStatus <$> newEmptyMVar
|
||||
|
||||
markSchemaCachePending :: AppState -> IO ()
|
||||
markSchemaCachePending = void . tryTakeMVar . getSCStatusMVar . stateSCacheStatus
|
||||
|
||||
markSchemaCacheLoaded :: AppState -> IO ()
|
||||
markSchemaCacheLoaded = void . (`tryPutMVar` ()) . getSCStatusMVar . stateSCacheStatus
|
||||
|
||||
isSchemaCacheLoaded :: AppState -> IO Bool
|
||||
isSchemaCacheLoaded = fmap not . isEmptyMVar . getSCStatusMVar . stateSCacheStatus
|
||||
|
||||
-- | Reads the in-db config and reads the config file again
|
||||
-- | We don't retry reading the in-db config after it fails immediately, because it could have user errors. We just report the error and continue.
|
||||
readInDbConfig :: Bool -> AppState -> IO ()
|
||||
@@ -441,7 +399,7 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
|
||||
pgVer <- getPgVersion appState
|
||||
dbSettings <-
|
||||
if configDbConfig conf then do
|
||||
qDbSettings <- usePool appState (queryDbSettings (quoteQi <$> configDbPreConfig conf) (configDbPreparedStatements conf))
|
||||
qDbSettings <- usePool appState (queryDbSettings (quoteQi <$> configDbPreConfig conf))
|
||||
case qDbSettings of
|
||||
Left e -> do
|
||||
observer $ ConfigReadErrorObs e
|
||||
@@ -451,7 +409,7 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
|
||||
pure mempty
|
||||
(roleSettings, roleIsolationLvl) <-
|
||||
if configDbConfig conf then do
|
||||
rSettings <- usePool appState (queryRoleSettings pgVer (configDbPreparedStatements conf))
|
||||
rSettings <- usePool appState (queryRoleSettings pgVer)
|
||||
case rSettings of
|
||||
Left e -> do
|
||||
observer $ QueryRoleSettingsErrorObs e
|
||||
|
||||
@@ -25,8 +25,8 @@ import qualified Network.Wai as Wai
|
||||
import qualified Network.Wai.Middleware.HttpAuth as Wai
|
||||
|
||||
import Data.List (lookup)
|
||||
import PostgREST.TimeIt (timeItT)
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
import System.TimeIt (timeItT)
|
||||
|
||||
import PostgREST.AppState (AppState, getConfig, getJwtCacheState,
|
||||
getTime)
|
||||
|
||||
@@ -16,14 +16,10 @@ module PostgREST.Auth.Jwt
|
||||
, parseClaims) 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 as BS
|
||||
import qualified Data.ByteString.Internal as BS
|
||||
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||
import qualified Data.Scientific as Sci
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Vector as V
|
||||
import qualified Jose.Jwk as JWT
|
||||
import qualified Jose.Jwt as JWT
|
||||
|
||||
@@ -33,12 +29,11 @@ import Data.Text ()
|
||||
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
|
||||
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
|
||||
|
||||
import PostgREST.Auth.Types (AuthResult (..))
|
||||
import PostgREST.Config (AppConfig (..), FilterExp (..), JSPath,
|
||||
JSPathExp (..), audMatchesCfg)
|
||||
import PostgREST.Error (Error (..),
|
||||
JwtClaimsError (AudClaimNotStringOrArray, ExpClaimNotNumber, IatClaimNotNumber, JWTExpired, JWTIssuedAtFuture, JWTNotInAudience, JWTNotYetValid, NbfClaimNotNumber, ParsingClaimsFailed),
|
||||
JwtDecodeError (..), JwtError (..))
|
||||
import PostgREST.Auth.Types (AuthResult (..))
|
||||
import PostgREST.Config (AppConfig (..), audMatchesCfg)
|
||||
import PostgREST.Config.JSPath (walkJSPath)
|
||||
import PostgREST.Error (Error (..), JwtClaimsError (..),
|
||||
JwtDecodeError (..), JwtError (..))
|
||||
|
||||
import Data.Aeson ((.:?))
|
||||
import Data.Aeson.Types (parseMaybe)
|
||||
@@ -95,13 +90,10 @@ checkForErrors time audMatches = mconcat
|
||||
parseToken :: (MonadError Error m, MonadIO m) => JwkSet -> ByteString -> m JWT.JwtContent
|
||||
parseToken _ "" = throwError $ JwtErr $ JwtDecodeErr EmptyAuthHeader
|
||||
parseToken secret tkn = do
|
||||
-- secret <- liftEither . maybeToRight (JwtErr JwtSecretMissing) $ configJWKS
|
||||
tknWith3Parts <- hasThreeParts tkn
|
||||
eitherContent <- liftIO $ JWT.decode (JWT.keys secret) Nothing tknWith3Parts
|
||||
liftEither . mapLeft (JwtErr . jwtDecodeError) $ eitherContent
|
||||
--liftEither $ mapLeft JwtErr $ verifyClaims content
|
||||
where
|
||||
--hasThreeParts :: ByteString -> Either Error ByteString
|
||||
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of
|
||||
3 -> pure token
|
||||
n -> throwError $ JwtErr $ JwtDecodeErr $ UnexpectedParts n
|
||||
@@ -124,28 +116,10 @@ parseClaims cfg@AppConfig{configJwtRoleClaimKey, configDbAnonRole} time mclaims
|
||||
role <- liftEither . maybeToRight (JwtErr JwtTokenRequired) $
|
||||
unquoted <$> walkJSPath (Just $ JSON.Object mclaims) configJwtRoleClaimKey <|> configDbAnonRole
|
||||
pure AuthResult
|
||||
{ authClaims = mclaims & KM.insert "role" (JSON.toJSON $ decodeUtf8 role)
|
||||
{ authClaims = mclaims
|
||||
, 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 (Just (JSON.Array ar)) [JSPFilter (EqualsCond txt)] = findFirstMatch (==) txt ar
|
||||
walkJSPath (Just (JSON.Array ar)) [JSPFilter (NotEqualsCond txt)] = findFirstMatch (/=) txt ar
|
||||
walkJSPath (Just (JSON.Array ar)) [JSPFilter (StartsWithCond txt)] = findFirstMatch T.isPrefixOf txt ar
|
||||
walkJSPath (Just (JSON.Array ar)) [JSPFilter (EndsWithCond txt)] = findFirstMatch T.isSuffixOf txt ar
|
||||
walkJSPath (Just (JSON.Array ar)) [JSPFilter (ContainsCond txt)] = findFirstMatch T.isInfixOf txt ar
|
||||
walkJSPath _ _ = Nothing
|
||||
|
||||
findFirstMatch matchWith pattern = foldr checkMatch Nothing
|
||||
where
|
||||
checkMatch (JSON.String txt) acc
|
||||
| pattern `matchWith` txt = Just $ JSON.String txt
|
||||
| otherwise = acc
|
||||
checkMatch _ acc = acc
|
||||
|
||||
unquoted :: JSON.Value -> BS.ByteString
|
||||
unquoted (JSON.String t) = encodeUtf8 t
|
||||
unquoted v = LBS.toStrict $ JSON.encode v
|
||||
|
||||
@@ -6,7 +6,9 @@ import qualified Data.Aeson as JSON
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.ByteString as BS
|
||||
|
||||
-- | Parse result for JWT Claims
|
||||
-- |
|
||||
-- Parse and store result for JWT Claims. Can be accessed in
|
||||
-- db through GUCs (for RLS etc)
|
||||
data AuthResult = AuthResult
|
||||
{ authClaims :: KM.KeyMap JSON.Value
|
||||
, authRole :: BS.ByteString
|
||||
|
||||
@@ -62,9 +62,7 @@ dumpSchema :: AppState -> IO LBS.ByteString
|
||||
dumpSchema appState = do
|
||||
conf@AppConfig{..} <- AppState.getConfig appState
|
||||
result <-
|
||||
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
|
||||
AppState.usePool appState
|
||||
(transaction SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
|
||||
AppState.usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
|
||||
case result of
|
||||
Left e -> do
|
||||
let observer = AppState.getObserver appState
|
||||
|
||||
+78
-22
@@ -9,6 +9,7 @@ Description : Manages PostgREST configuration type and parser.
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module PostgREST.Config
|
||||
( AppConfig (..)
|
||||
@@ -27,21 +28,25 @@ module PostgREST.Config
|
||||
, parseSecret
|
||||
, addFallbackAppName
|
||||
, addTargetSessionAttrs
|
||||
, toConnectionSettings
|
||||
, exampleConfigFile
|
||||
, audMatchesCfg
|
||||
, Verbosity (..)
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.Configurator as C
|
||||
import qualified Data.Map.Strict as M
|
||||
import qualified Data.String as S
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Jose.Jwa as JWT
|
||||
import qualified Jose.Jwk as JWT
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.Configurator as C
|
||||
import qualified Data.Map.Strict as M
|
||||
import qualified Data.String as S
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Hasql.Connection.Setting as SQL
|
||||
import qualified Hasql.Connection.Setting.Connection as SQL
|
||||
import qualified Jose.Jwa as JWT
|
||||
import qualified Jose.Jwk as JWT
|
||||
|
||||
import Control.Monad (fail)
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
@@ -63,16 +68,18 @@ import PostgREST.Config.JSPath (FilterExp (..), JSPath,
|
||||
pRoleClaimKey)
|
||||
import PostgREST.Config.Proxy (Proxy (..),
|
||||
isMalformedProxyUri, toURI)
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier, dumpQi,
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||
toQi)
|
||||
|
||||
import Protolude hiding (Proxy, toList)
|
||||
import PostgREST.Version (prettyVersion)
|
||||
import Protolude hiding (Proxy, toList)
|
||||
|
||||
audMatchesCfg :: AppConfig -> Text -> Bool
|
||||
audMatchesCfg = maybe (const True) (==) . configJwtAudience
|
||||
|
||||
data AppConfig = AppConfig
|
||||
{ configAppSettings :: [(Text, Text)]
|
||||
, configClientErrorVerbosity :: Verbosity
|
||||
, configDbAggregates :: Bool
|
||||
, configDbAnonRole :: Maybe BS.ByteString
|
||||
, configDbChannel :: Text
|
||||
@@ -92,6 +99,7 @@ data AppConfig = AppConfig
|
||||
, configDbSchemas :: NonEmpty Text
|
||||
, configDbConfig :: Bool
|
||||
, configDbPreConfig :: Maybe QualifiedIdentifier
|
||||
, configDbTimezoneEnabled :: Bool
|
||||
, configDbTxAllowOverride :: Bool
|
||||
, configDbTxRollbackAll :: Bool
|
||||
, configDbUri :: Text
|
||||
@@ -134,6 +142,15 @@ dumpLogLevel = \case
|
||||
LogInfo -> "info"
|
||||
LogDebug -> "debug"
|
||||
|
||||
data Verbosity
|
||||
= Minimal
|
||||
| Verbose
|
||||
|
||||
dumpClientErrorVerbosity :: Verbosity -> Text
|
||||
dumpClientErrorVerbosity = \case
|
||||
Minimal -> "minimal"
|
||||
Verbose -> "verbose"
|
||||
|
||||
data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled
|
||||
deriving Eq
|
||||
|
||||
@@ -150,7 +167,8 @@ toText conf =
|
||||
where
|
||||
-- apply conf to all pgrst settings
|
||||
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
|
||||
[("db-aggregates-enabled", T.toLower . show . configDbAggregates)
|
||||
[("client-error-verbosity", q . dumpClientErrorVerbosity . configClientErrorVerbosity)
|
||||
,("db-aggregates-enabled", T.toLower . show . configDbAggregates)
|
||||
,("db-anon-role", q . T.decodeUtf8 . fromMaybe "" . configDbAnonRole)
|
||||
,("db-channel", q . configDbChannel)
|
||||
,("db-channel-enabled", T.toLower . show . configDbChannelEnabled)
|
||||
@@ -169,6 +187,7 @@ toText conf =
|
||||
,("db-schemas", q . T.intercalate "," . toList . configDbSchemas)
|
||||
,("db-config", T.toLower . show . configDbConfig)
|
||||
,("db-pre-config", q . maybe mempty dumpQi . configDbPreConfig)
|
||||
,("db-timezone-enabled", T.toLower . show . configDbTimezoneEnabled)
|
||||
,("db-tx-end", q . showTxEnd)
|
||||
,("db-uri", q . configDbUri)
|
||||
,("jwt-aud", q . fromMaybe mempty . configJwtAudience)
|
||||
@@ -198,6 +217,10 @@ toText conf =
|
||||
-- quote strings and replace " with \"
|
||||
q s = "\"" <> T.replace "\"" "\\\"" s <> "\""
|
||||
|
||||
dumpQi :: QualifiedIdentifier -> Text
|
||||
dumpQi (QualifiedIdentifier s i) =
|
||||
(if T.null s then mempty else s <> ".") <> i
|
||||
|
||||
showTxEnd c = case (configDbTxRollbackAll c, configDbTxAllowOverride c) of
|
||||
( False, False ) -> "commit"
|
||||
( False, True ) -> "commit-allow-override"
|
||||
@@ -250,6 +273,7 @@ parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> RoleSettings -> Rol
|
||||
parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
AppConfig
|
||||
<$> parseAppSettings "app.settings"
|
||||
<*> parseErrorVerbosity "client-error-verbosity"
|
||||
<*> (fromMaybe False <$> optBool "db-aggregates-enabled")
|
||||
<*> (fmap encodeUtf8 <$> optString "db-anon-role")
|
||||
<*> (fromMaybe "pgrst" <$> optString "db-channel")
|
||||
@@ -270,10 +294,10 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
<*> (fromMaybe True <$> optBool "db-prepared-statements")
|
||||
<*> (fmap toQi <$> optWithAlias (optString "db-root-spec")
|
||||
(optString "root-spec"))
|
||||
<*> (fromList . maybe ["public"] splitOnCommas <$> optWithAlias (optString "db-schemas")
|
||||
(optString "db-schema"))
|
||||
<*> parseDbSchemas "db-schemas" "db-schema"
|
||||
<*> (fromMaybe True <$> optBool "db-config")
|
||||
<*> (fmap toQi <$> optString "db-pre-config")
|
||||
<*> (fromMaybe True <$> optBool "db-timezone-enabled")
|
||||
<*> parseTxEnd "db-tx-end" snd
|
||||
<*> parseTxEnd "db-tx-end" fst
|
||||
<*> (fromMaybe "postgresql://" <$> optString "db-uri")
|
||||
@@ -307,6 +331,14 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
<*> optInt "internal-schema-cache-load-sleep"
|
||||
<*> optInt "internal-schema-cache-relationship-load-sleep"
|
||||
where
|
||||
parseErrorVerbosity :: C.Key -> C.Parser C.Config Verbosity
|
||||
parseErrorVerbosity k =
|
||||
optString k >>= \case
|
||||
Nothing -> pure Verbose -- default
|
||||
Just "minimal" -> pure Minimal
|
||||
Just "verbose" -> pure Verbose
|
||||
Just _ -> fail "Invalid client-error-verbosity. Check your configuration."
|
||||
|
||||
parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)]
|
||||
parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value
|
||||
where
|
||||
@@ -325,6 +357,18 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
Just asp | asp == serverPort -> fail "admin-server-port cannot be the same as server-port"
|
||||
| otherwise -> pure $ Just asp
|
||||
|
||||
parseDbSchemas :: C.Key -> C.Key -> C.Parser C.Config (NonEmpty Text)
|
||||
parseDbSchemas k al =
|
||||
optWithAlias (optString k) (optString al) >>= \case
|
||||
Nothing -> pure $ fromList ["public"]
|
||||
Just s
|
||||
| "pg_catalog" `elem` schemas -> fail (errMsg "pg_catalog")
|
||||
| "information_schema" `elem` schemas -> fail (errMsg "information_schema")
|
||||
| otherwise -> pure $ fromList schemas
|
||||
where
|
||||
schemas = splitOnCommas s
|
||||
errMsg x = "db-schemas does not allow schema: '" <> x <> "'"
|
||||
|
||||
parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode
|
||||
parseSocketFileMode k =
|
||||
optString k >>= \case
|
||||
@@ -577,10 +621,10 @@ pgConnString conn | uriDesignator `T.isPrefixOf` conn || shortUriDesignator `T.i
|
||||
-- >>> addFallbackAppName ver "postgres://admin2:?pass?special?@localhost:5432/postgres"
|
||||
-- "postgres://admin2:?pass?special?@localhost:5432/postgres?fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
||||
--
|
||||
-- addFallbackAppName ver "postgresql://?dbname=postgres&host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass"
|
||||
-- >>> addFallbackAppName ver "postgresql://?dbname=postgres&host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass"
|
||||
-- "postgresql://?dbname=postgres&host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass&fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
||||
--
|
||||
-- addFallbackAppName ver "postgresql:///postgres?host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass"
|
||||
-- >>> addFallbackAppName ver "postgresql:///postgres?host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass"
|
||||
-- "postgresql:///postgres?host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass&fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
||||
addFallbackAppName :: ByteString -> Text -> Text
|
||||
addFallbackAppName version dbUri = addConnStringOption dbUri "fallback_application_name" pgrstVer
|
||||
@@ -607,6 +651,12 @@ addFallbackAppName version dbUri = addConnStringOption dbUri "fallback_applicati
|
||||
addTargetSessionAttrs :: Text -> Text
|
||||
addTargetSessionAttrs dbUri = addConnStringOption dbUri "target_session_attrs" "read-write"
|
||||
|
||||
toConnectionSettings :: (Text -> Text) -> AppConfig -> [SQL.Setting]
|
||||
toConnectionSettings transformUri AppConfig{configDbUri, configDbPreparedStatements} =
|
||||
[ SQL.connection $ SQL.string $ transformUri . addFallbackAppName prettyVersion $ configDbUri
|
||||
, SQL.usePreparedStatements configDbPreparedStatements
|
||||
]
|
||||
|
||||
addConnStringOption :: Text -> Text -> Text -> Text
|
||||
addConnStringOption dbUri key val = dbUri <>
|
||||
case pgConnString dbUri of
|
||||
@@ -627,6 +677,9 @@ exampleConfigFile = S.unlines
|
||||
[ "## Admin server used for checks. It's disabled by default unless a port is specified."
|
||||
, "# admin-server-port = 3001"
|
||||
, ""
|
||||
, "# PostgREST error json verbosity config"
|
||||
, "# client-error-verbosity = \"verbose\""
|
||||
, ""
|
||||
, "## The database role to use when no client authentication is provided"
|
||||
, "# db-anon-role = \"anon\""
|
||||
, ""
|
||||
@@ -676,16 +729,19 @@ exampleConfigFile = S.unlines
|
||||
, "## The name of which database schema to expose to REST clients"
|
||||
, "db-schemas = \"public\""
|
||||
, ""
|
||||
, "## Enable quering pg_timezone_names from db"
|
||||
, "# db-timezone-enabled = true"
|
||||
, ""
|
||||
, "## How to terminate database transactions"
|
||||
, "## Possible values are:"
|
||||
, "## commit (default)"
|
||||
, "## Transaction is always committed, this can not be overriden"
|
||||
, "## Transaction is always committed, this can not be overridden"
|
||||
, "## commit-allow-override"
|
||||
, "## Transaction is committed, but can be overriden with Prefer tx=rollback header"
|
||||
, "## Transaction is committed, but can be overridden with Prefer tx=rollback header"
|
||||
, "## rollback"
|
||||
, "## Transaction is always rolled back, this can not be overriden"
|
||||
, "## Transaction is always rolled back, this can not be overridden"
|
||||
, "## rollback-allow-override"
|
||||
, "## Transaction is rolled back, but can be overriden with Prefer tx=commit header"
|
||||
, "## Transaction is rolled back, but can be overridden with Prefer tx=commit header"
|
||||
, "db-tx-end = \"commit\""
|
||||
, ""
|
||||
, "## The standard connection URI format, documented at"
|
||||
|
||||
@@ -46,6 +46,7 @@ dbSettingsNames :: [Text]
|
||||
dbSettingsNames =
|
||||
(prefix <>) <$>
|
||||
["db_aggregates_enabled"
|
||||
,"client_error_verbosity"
|
||||
,"db_anon_role"
|
||||
,"db_pre_config"
|
||||
,"db_extra_search_path"
|
||||
@@ -55,6 +56,7 @@ dbSettingsNames =
|
||||
,"db_prepared_statements"
|
||||
,"db_root_spec"
|
||||
,"db_schemas"
|
||||
,"db_timezone_enabled"
|
||||
,"db_tx_end"
|
||||
,"db_hoisted_tx_settings"
|
||||
,"jwt_aud"
|
||||
@@ -70,8 +72,8 @@ dbSettingsNames =
|
||||
,"server_timing_enabled"
|
||||
]
|
||||
|
||||
queryPgVersion :: Bool -> Session PgVersion
|
||||
queryPgVersion prepared = statement mempty $ pgVersionStatement prepared
|
||||
queryPgVersion :: Session PgVersion
|
||||
queryPgVersion = statement mempty $ pgVersionStatement False
|
||||
|
||||
pgVersionStatement :: Bool -> SQL.Statement () PgVersion
|
||||
pgVersionStatement = SQL.Statement sql HE.noParams versionRow
|
||||
@@ -90,10 +92,9 @@ pgVersionStatement = SQL.Statement sql HE.noParams versionRow
|
||||
--
|
||||
-- The example above will result in <prefix>jwt_aud = 'val'
|
||||
-- A setting on the database only will have no effect: ALTER DATABASE postgres SET <prefix>jwt_aud = 'xx'
|
||||
queryDbSettings :: Maybe Text -> Bool -> Session [(Text, Text)]
|
||||
queryDbSettings preConfFunc prepared =
|
||||
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
||||
transaction SQL.ReadCommitted SQL.Read $ SQL.statement dbSettingsNames $ SQL.Statement sql (arrayParam HE.text) decodeSettings prepared
|
||||
queryDbSettings :: Maybe Text -> Session [(Text, Text)]
|
||||
queryDbSettings preConfFunc =
|
||||
SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ SQL.statement dbSettingsNames $ SQL.Statement sql (arrayParam HE.text) decodeSettings True
|
||||
where
|
||||
sql = encodeUtf8 [trimming|
|
||||
WITH
|
||||
@@ -101,7 +102,7 @@ queryDbSettings preConfFunc prepared =
|
||||
SELECT setdatabase as database,
|
||||
unnest(setconfig) as setting
|
||||
FROM pg_catalog.pg_db_role_setting
|
||||
WHERE setrole = CURRENT_USER::regrole::oid
|
||||
WHERE setrole = quote_ident(CURRENT_USER)::regrole::oid
|
||||
AND setdatabase IN (0, (SELECT oid FROM pg_catalog.pg_database WHERE datname = CURRENT_CATALOG))
|
||||
),
|
||||
kv_settings AS (
|
||||
@@ -131,10 +132,9 @@ queryDbSettings preConfFunc prepared =
|
||||
|]::Text
|
||||
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
|
||||
|
||||
queryRoleSettings :: PgVersion -> Bool -> Session (RoleSettings, RoleIsolationLvl)
|
||||
queryRoleSettings pgVer prepared =
|
||||
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
||||
transaction SQL.ReadCommitted SQL.Read $ SQL.statement mempty $ SQL.Statement sql HE.noParams (processRows <$> rows) prepared
|
||||
queryRoleSettings :: PgVersion -> Session (RoleSettings, RoleIsolationLvl)
|
||||
queryRoleSettings pgVer =
|
||||
SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ SQL.statement mempty $ SQL.Statement sql HE.noParams (processRows <$> rows) True
|
||||
where
|
||||
sql = encodeUtf8 [trimming|
|
||||
with
|
||||
@@ -142,7 +142,7 @@ queryRoleSettings pgVer prepared =
|
||||
select r.rolname, unnest(r.rolconfig) as setting
|
||||
from pg_auth_members m
|
||||
join pg_roles r on r.oid = m.roleid
|
||||
where member = current_user::regrole::oid
|
||||
where member = quote_ident(current_user)::regrole::oid
|
||||
),
|
||||
kv_settings AS (
|
||||
SELECT
|
||||
@@ -167,7 +167,7 @@ queryRoleSettings pgVer prepared =
|
||||
|]
|
||||
|
||||
hasParameterPrivilege
|
||||
| pgVer >= pgVersion150 = "or has_parameter_privilege(current_user::regrole::oid, ps.name, 'set')"
|
||||
| pgVer >= pgVersion150 = "or has_parameter_privilege(quote_ident(current_user)::regrole::oid, ps.name, 'set')"
|
||||
| otherwise = ""
|
||||
|
||||
processRows :: [(Text, Maybe Text, [(Text, Text)])] -> (RoleSettings, RoleIsolationLvl)
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
module PostgREST.Config.JSPath
|
||||
( JSPath
|
||||
, JSPathExp(..)
|
||||
, FilterExp(..)
|
||||
, dumpJSPath
|
||||
, pRoleClaimKey
|
||||
, walkJSPath
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.Aeson.Key as K
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Vector as V
|
||||
import qualified Text.ParserCombinators.Parsec as P
|
||||
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
@@ -22,9 +29,10 @@ type JSPath = [JSPathExp]
|
||||
-- NOTE: We only accept one JSPFilter expr (at the end of input)
|
||||
-- | jspath expression
|
||||
data JSPathExp
|
||||
= JSPKey Text -- .property or ."property-dash"
|
||||
| JSPIdx Int -- [0]
|
||||
| JSPFilter FilterExp -- [?(@ == "match")]
|
||||
= JSPKey Text -- .property or ."property-dash"
|
||||
| JSPIdx Int -- [0]
|
||||
| JSPSlice (Maybe Int) (Maybe Int) -- [0:5] or [0:] or [:5] or [:]
|
||||
| JSPFilter FilterExp -- [?(@ == "match")]
|
||||
|
||||
data FilterExp
|
||||
= EqualsCond Text
|
||||
@@ -37,6 +45,7 @@ dumpJSPath :: JSPathExp -> Text
|
||||
-- TODO: this needs to be quoted properly for special chars
|
||||
dumpJSPath (JSPKey k) = "." <> show k
|
||||
dumpJSPath (JSPIdx i) = "[" <> show i <> "]"
|
||||
dumpJSPath (JSPSlice s e) = "[" <> maybe "" show s <> ":" <> maybe "" show e <> "]"
|
||||
dumpJSPath (JSPFilter cond) = "[?(@" <> expr <> ")]"
|
||||
where
|
||||
expr =
|
||||
@@ -47,6 +56,35 @@ dumpJSPath (JSPFilter cond) = "[?(@" <> expr <> ")]"
|
||||
EndsWithCond text -> " ==^ " <> show text
|
||||
ContainsCond text -> " *== " <> show text
|
||||
|
||||
-- | Evaluate JSPath on a JSON
|
||||
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 (Just (JSON.String str)) (JSPSlice start end:rest) =
|
||||
let
|
||||
len = T.length str
|
||||
|
||||
norm :: Maybe Int -> Maybe Int -- Normalize negative indices to positive
|
||||
norm = fmap (\i -> max 0 $ min len $ if i < 0 then len + i else i)
|
||||
|
||||
s = fromMaybe 0 $ norm start -- normalized start index
|
||||
e = fromMaybe len $ norm end -- normalized end index
|
||||
slicedString = if s >= e then T.empty else T.take (e-s) $ T.drop s str
|
||||
in
|
||||
walkJSPath (Just $ JSON.String slicedString) rest
|
||||
|
||||
walkJSPath (Just (JSON.Array ar)) (JSPFilter jspFilter:rest) = case jspFilter of
|
||||
EqualsCond txt -> walkJSPath (findFirstMatch (==) txt ar) rest
|
||||
NotEqualsCond txt -> walkJSPath (findFirstMatch (/=) txt ar) rest
|
||||
StartsWithCond txt -> walkJSPath (findFirstMatch T.isPrefixOf txt ar) rest
|
||||
EndsWithCond txt -> walkJSPath (findFirstMatch T.isSuffixOf txt ar) rest
|
||||
ContainsCond txt -> walkJSPath (findFirstMatch T.isInfixOf txt ar) rest
|
||||
where
|
||||
findFirstMatch matchWith pattern = find (\case
|
||||
JSON.String txt -> pattern `matchWith` txt
|
||||
_ -> False)
|
||||
walkJSPath _ _ = Nothing
|
||||
|
||||
-- Used for the config value "role-claim-key"
|
||||
pRoleClaimKey :: Text -> Either Text JSPath
|
||||
@@ -57,7 +95,7 @@ pJSPath :: P.Parser JSPath
|
||||
pJSPath = P.many1 pJSPathExp <* P.eof
|
||||
|
||||
pJSPathExp :: P.Parser JSPathExp
|
||||
pJSPathExp = pJSPKey <|> pJSPFilter <|> pJSPIdx
|
||||
pJSPathExp = P.try pJSPKey <|> P.try pJSPFilter <|> P.try pJSPIdx <|> pJSPSlice
|
||||
|
||||
pJSPKey :: P.Parser JSPathExp
|
||||
pJSPKey = do
|
||||
@@ -72,13 +110,25 @@ pJSPIdx = do
|
||||
P.char ']'
|
||||
return (JSPIdx num) <?> "pJSPIdx: JSPath array index"
|
||||
|
||||
pJSPSlice :: P.Parser JSPathExp
|
||||
pJSPSlice = do
|
||||
P.char '['
|
||||
startSign <- P.optionMaybe $ P.char '-'
|
||||
startIndex <- P.optionMaybe (read <$> P.many1 P.digit)
|
||||
P.char ':'
|
||||
endSign <- P.optionMaybe $ P.char '-'
|
||||
endIndex <- P.optionMaybe (read <$> P.many1 P.digit)
|
||||
P.char ']'
|
||||
let start' = if isJust startSign then ((-1) *) <$> startIndex else startIndex
|
||||
end' = if isJust endSign then ((-1) *) <$> endIndex else endIndex
|
||||
return (JSPSlice start' end') <?> "pJSPSlice: JSPath string slice"
|
||||
|
||||
pJSPFilter :: P.Parser JSPathExp
|
||||
pJSPFilter = do
|
||||
P.try $ P.string "[?("
|
||||
condition <- pFilterConditionParser
|
||||
P.char ')'
|
||||
P.char ']'
|
||||
P.eof -- this should be the last jspath expression
|
||||
return (JSPFilter condition) <?> "pJSPFilter: JSPath filter exp"
|
||||
|
||||
pFilterConditionParser :: P.Parser FilterExp
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
module PostgREST.Config.PgVersion
|
||||
( PgVersion(..)
|
||||
, minimumPgVersion
|
||||
, pgVersion140
|
||||
, pgVersion150
|
||||
, pgVersion170
|
||||
, pgVersion180
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
@@ -25,10 +25,7 @@ instance Ord PgVersion where
|
||||
|
||||
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
|
||||
minimumPgVersion :: PgVersion
|
||||
minimumPgVersion = pgVersion130
|
||||
|
||||
pgVersion130 :: PgVersion
|
||||
pgVersion130 = PgVersion 130000 "13.0" "13.0"
|
||||
minimumPgVersion = pgVersion140
|
||||
|
||||
pgVersion140 :: PgVersion
|
||||
pgVersion140 = PgVersion 140000 "14.0" "14.0"
|
||||
@@ -38,3 +35,6 @@ pgVersion150 = PgVersion 150000 "15.0" "15.0"
|
||||
|
||||
pgVersion170 :: PgVersion
|
||||
pgVersion170 = PgVersion 170000 "17.0" "17.0"
|
||||
|
||||
pgVersion180 :: PgVersion
|
||||
pgVersion180 = PgVersion 180000 "18.0" "18.0"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
module PostgREST.Debounce
|
||||
( makeDebouncer) where
|
||||
|
||||
import Protolude
|
||||
|
||||
-- | Make a new debouncer action. An internal "worker" thread runs forever
|
||||
-- ensuring "action" runs when the "trigger" is called. The "action" is only
|
||||
-- executed once over a burst of calls.
|
||||
makeDebouncer :: IO () -> IO (IO ())
|
||||
makeDebouncer action = do
|
||||
flag <- newEmptyMVar
|
||||
|
||||
let worker = forever $ do
|
||||
takeMVar flag
|
||||
action
|
||||
trigger = void $ tryPutMVar flag ()
|
||||
|
||||
void $ forkIO worker
|
||||
pure trigger
|
||||
+94
-201
@@ -3,6 +3,7 @@ Module : PostgREST.Error
|
||||
Description : PostgREST error HTTP responses
|
||||
-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module PostgREST.Error
|
||||
@@ -41,6 +42,8 @@ import Network.HTTP.Types.Header (Header)
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import qualified PostgREST.MediaType as MediaType
|
||||
|
||||
import PostgREST.Config (Verbosity (..))
|
||||
import PostgREST.SchemaCache (SchemaCache (SchemaCache, dbTablesFuzzyIndex))
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||
Schema)
|
||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
@@ -49,24 +52,40 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
RelationshipsMap)
|
||||
import PostgREST.SchemaCache.Routine (Routine (..),
|
||||
RoutineParam (..))
|
||||
import PostgREST.SchemaCache.Table (Table (..))
|
||||
|
||||
import PostgREST.Error.Types
|
||||
|
||||
import Protolude
|
||||
|
||||
-- | Encode Error to ByteString
|
||||
errorPayload :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> LByteString
|
||||
errorPayload verb = JSON.encode . toJsonPgrstError verb
|
||||
where
|
||||
toJsonPgrstError :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> JSON.Value
|
||||
toJsonPgrstError Verbose err = JSON.object [
|
||||
"code" .= code err
|
||||
, "message" .= message err
|
||||
, "details" .= details err
|
||||
, "hint" .= hint err
|
||||
]
|
||||
toJsonPgrstError Minimal err = JSON.object [
|
||||
"code" .= code err
|
||||
, "message" .= message err
|
||||
]
|
||||
|
||||
class (ErrorBody a, JSON.ToJSON a) => PgrstError a where
|
||||
status :: a -> HTTP.Status
|
||||
headers :: a -> [Header]
|
||||
-- | Create HTTP response from Error
|
||||
errorResponseFor :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> Response
|
||||
errorResponseFor verb err =
|
||||
let
|
||||
baseHeader = MediaType.toContentType MTApplicationJSON
|
||||
cLHeader body = (,) "Content-Length" (show $ LBS.length body) :: Header
|
||||
pSHeader code' = ("Proxy-Status", "PostgREST; error=" <> T.encodeUtf8 code')
|
||||
in
|
||||
responseLBS (status err) (baseHeader : cLHeader (errorPayload verb err) : pSHeader (code err) : headers err) $ errorPayload verb err
|
||||
|
||||
errorPayload :: a -> LByteString
|
||||
errorPayload = JSON.encode
|
||||
|
||||
errorResponseFor :: a -> Response
|
||||
errorResponseFor err =
|
||||
let
|
||||
baseHeader = MediaType.toContentType MTApplicationJSON
|
||||
cLHeader body = (,) "Content-Length" (show $ LBS.length body) :: Header
|
||||
in
|
||||
responseLBS (status err) (baseHeader : cLHeader (errorPayload err) : headers err) $ errorPayload err
|
||||
class ErrorHeaders a where
|
||||
status :: a -> HTTP.Status
|
||||
headers :: a -> [Header]
|
||||
|
||||
class ErrorBody a where
|
||||
code :: a -> Text
|
||||
@@ -74,49 +93,7 @@ class ErrorBody a where
|
||||
details :: a -> Maybe JSON.Value
|
||||
hint :: a -> Maybe JSON.Value
|
||||
|
||||
data ApiRequestError
|
||||
= AggregatesNotAllowed
|
||||
| MediaTypeError [ByteString]
|
||||
| InvalidBody ByteString
|
||||
| InvalidFilters
|
||||
| InvalidPreferences [ByteString]
|
||||
| InvalidRange RangeError
|
||||
| InvalidRpcMethod ByteString
|
||||
| NotEmbedded Text
|
||||
| NotImplemented Text
|
||||
| PutLimitNotAllowedError
|
||||
| QueryParamError QPError
|
||||
| RelatedOrderNotToOne Text Text
|
||||
| UnacceptableFilter Text
|
||||
| UnacceptableSchema Text [Text]
|
||||
| UnsupportedMethod ByteString
|
||||
| GucHeadersError
|
||||
| GucStatusError
|
||||
| PutMatchingPkError
|
||||
| SingularityError Integer
|
||||
| PGRSTParseError RaiseError
|
||||
| MaxAffectedViolationError Integer
|
||||
| InvalidResourcePath
|
||||
| OpenAPIDisabled
|
||||
| MaxAffectedRpcViolation
|
||||
deriving Show
|
||||
|
||||
data QPError = QPError Text Text
|
||||
deriving Show
|
||||
|
||||
data RaiseError
|
||||
= MsgParseError ByteString
|
||||
| DetParseError ByteString
|
||||
| NoDetail
|
||||
deriving Show
|
||||
|
||||
data RangeError
|
||||
= NegativeLimit
|
||||
| LowerGTUpper
|
||||
| OutOfBounds Text Text
|
||||
deriving Show
|
||||
|
||||
instance PgrstError ApiRequestError where
|
||||
instance ErrorHeaders ApiRequestError where
|
||||
status AggregatesNotAllowed{} = HTTP.status400
|
||||
status MediaTypeError{} = HTTP.status406
|
||||
status InvalidBody{} = HTTP.status400
|
||||
@@ -240,20 +217,7 @@ instance ErrorBody ApiRequestError where
|
||||
|
||||
hint _ = Nothing
|
||||
|
||||
instance JSON.ToJSON ApiRequestError where
|
||||
toJSON err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
data SchemaCacheError
|
||||
= AmbiguousRelBetween Text Text [Relationship]
|
||||
| AmbiguousRpc [Routine]
|
||||
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
||||
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
|
||||
| ColumnNotFound Text Text
|
||||
| TableNotFound Text Text [Table]
|
||||
deriving Show
|
||||
|
||||
instance PgrstError SchemaCacheError where
|
||||
instance ErrorHeaders SchemaCacheError where
|
||||
status AmbiguousRelBetween{} = HTTP.status300
|
||||
status AmbiguousRpc{} = HTTP.status300
|
||||
status NoRelBetween{} = HTTP.status400
|
||||
@@ -313,22 +277,10 @@ instance ErrorBody SchemaCacheError where
|
||||
where
|
||||
onlySingleParams = isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream]
|
||||
hint (AmbiguousRpc _) = Just "Try renaming the parameters or the function itself in the database so function overloading can be resolved"
|
||||
hint (TableNotFound schemaName relName tbls) = JSON.String <$> tableNotFoundHint schemaName relName tbls
|
||||
hint (TableNotFound schemaName relName schemaCache) = JSON.String <$> tableNotFoundHint schemaName relName schemaCache
|
||||
|
||||
hint _ = Nothing
|
||||
|
||||
instance JSON.ToJSON SchemaCacheError where
|
||||
toJSON err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
toJsonPgrstError :: Text -> Text -> Maybe JSON.Value -> Maybe JSON.Value -> JSON.Value
|
||||
toJsonPgrstError code' message' details' hint' = JSON.object [
|
||||
"code" .= code'
|
||||
, "message" .= message'
|
||||
, "details" .= details'
|
||||
, "hint" .= hint'
|
||||
]
|
||||
|
||||
-- |
|
||||
-- If no relationship is found then:
|
||||
--
|
||||
@@ -347,9 +299,6 @@ toJsonPgrstError code' message' details' hint' = JSON.object [
|
||||
-- >>> noRelBetweenHint "films" "role" "api" rels
|
||||
-- Just "Perhaps you meant 'roles' instead of 'role'."
|
||||
--
|
||||
-- >>> noRelBetweenHint "films" "role" "api" rels
|
||||
-- Just "Perhaps you meant 'roles' instead of 'role'."
|
||||
--
|
||||
-- >>> noRelBetweenHint "films" "actors" "api" rels
|
||||
-- Nothing
|
||||
--
|
||||
@@ -386,7 +335,7 @@ noRelBetweenHint parent child schema allRels = ("Perhaps you meant '" <>) <$>
|
||||
-- Just "Perhaps you meant to call the function api.test"
|
||||
--
|
||||
-- >>> noRpcHint "api" "other" [] procs []
|
||||
-- Just "Perhaps you meant to call the function api.another"
|
||||
-- Nothing
|
||||
--
|
||||
-- >>> noRpcHint "api" "noclosealternative" [] procs []
|
||||
-- Nothing
|
||||
@@ -423,18 +372,30 @@ noRpcHint schema procName params allProcs overloadedProcs =
|
||||
-- E.g. ["val", "param", "name"] into "(name, param, val)"
|
||||
listToText = ("(" <>) . (<> ")") . T.intercalate ", " . sort
|
||||
possibleProcs
|
||||
| null overloadedProcs = Fuzzy.getOne fuzzySetOfProcs procName
|
||||
| otherwise = (procName <>) <$> Fuzzy.getOne fuzzySetOfParams (listToText params)
|
||||
| null overloadedProcs = getFuzzyHint HintProcedure fuzzySetOfProcs procName
|
||||
| otherwise = (procName <>) <$> getFuzzyHint HintParams fuzzySetOfParams (listToText params)
|
||||
|
||||
-- |
|
||||
-- Do a fuzzy search in all tables in the same schema and return closest result
|
||||
tableNotFoundHint :: Text -> Text -> [Table] -> Maybe Text
|
||||
tableNotFoundHint schema tblName tblList
|
||||
tableNotFoundHint :: Text -> Text -> SchemaCache -> Maybe Text
|
||||
tableNotFoundHint schema tblName SchemaCache{dbTablesFuzzyIndex}
|
||||
= fmap (\tbl -> "Perhaps you meant the table '" <> schema <> "." <> tbl <> "'") perhapsTable
|
||||
where
|
||||
perhapsTable = Fuzzy.getOne fuzzyTableSet tblName
|
||||
fuzzyTableSet = Fuzzy.fromList [ tableName tbl | tbl <- tblList, tableSchema tbl == schema]
|
||||
perhapsTable = (\fuzzySet -> getFuzzyHint HintTable fuzzySet tblName) =<< HM.lookup schema dbTablesFuzzyIndex
|
||||
|
||||
data HintType
|
||||
= HintTable
|
||||
| HintProcedure
|
||||
| HintParams
|
||||
|
||||
-- | Get hint using Fuzzy Search with at least 0.75 similarity score
|
||||
getFuzzyHint :: HintType -> Fuzzy.FuzzySet -> Text -> Maybe Text
|
||||
getFuzzyHint hintType =
|
||||
let minScore = 0.75 :: Double -- used for table and procedure name hints
|
||||
in case hintType of
|
||||
HintTable -> Fuzzy.getOneWithMinScore minScore
|
||||
HintProcedure -> Fuzzy.getOneWithMinScore minScore
|
||||
HintParams -> Fuzzy.getOne -- For params, we stick to `getOne` which defaults to 0.33 min score, not a security risk to reveal params
|
||||
|
||||
compressedRel :: Relationship -> JSON.Value
|
||||
-- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed
|
||||
@@ -487,12 +448,7 @@ pgrstParseErrorHint err = case err of
|
||||
MsgParseError _ -> "MESSAGE must be a JSON object with obligatory keys: 'code', 'message' and optional keys: 'details', 'hint'."
|
||||
_ -> "DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'."
|
||||
|
||||
data PgError = PgError Authenticated SQL.UsageError
|
||||
deriving Show
|
||||
|
||||
type Authenticated = Bool
|
||||
|
||||
instance PgrstError PgError where
|
||||
instance ErrorHeaders PgError where
|
||||
status (PgError authed usageError) = pgErrorStatus authed usageError
|
||||
|
||||
headers (PgError _ (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p))))) =
|
||||
@@ -507,44 +463,33 @@ instance PgrstError PgError where
|
||||
then [("WWW-Authenticate", "Bearer") :: Header]
|
||||
else mempty
|
||||
|
||||
proxyStatusHeader :: Text -> Header
|
||||
proxyStatusHeader code' = ("Proxy-Status", "PostgREST; error=" <> T.encodeUtf8 code')
|
||||
|
||||
instance JSON.ToJSON PgError where
|
||||
toJSON (PgError _ usageError) = toJsonPgrstError
|
||||
(code usageError) (message usageError) (details usageError) (hint usageError)
|
||||
|
||||
instance ErrorBody PgError where
|
||||
code (PgError _ usageError) = code usageError
|
||||
message (PgError _ usageError) = message usageError
|
||||
details (PgError _ usageError) = details usageError
|
||||
hint (PgError _ usageError) = hint usageError
|
||||
|
||||
instance JSON.ToJSON SQL.UsageError where
|
||||
toJSON err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
instance ErrorBody SQL.UsageError where
|
||||
code (SQL.ConnectionUsageError _) = "PGRST000"
|
||||
code (SQL.SessionUsageError (SQL.PipelineError e)) = code e
|
||||
code (SQL.SessionUsageError (SQL.QueryError _ _ e)) = code e
|
||||
code SQL.AcquisitionTimeoutUsageError = "PGRST003"
|
||||
|
||||
message (SQL.ConnectionUsageError _) = "Database connection error. Retrying the connection."
|
||||
message (SQL.SessionUsageError (SQL.PipelineError e)) = message e
|
||||
message (SQL.SessionUsageError (SQL.QueryError _ _ e)) = message e
|
||||
message SQL.AcquisitionTimeoutUsageError = "Timed out acquiring connection from connection pool."
|
||||
|
||||
details (SQL.ConnectionUsageError e) = JSON.String . T.decodeUtf8 <$> e
|
||||
details (SQL.SessionUsageError (SQL.PipelineError e)) = details e
|
||||
details (SQL.SessionUsageError (SQL.QueryError _ _ e)) = details e
|
||||
details SQL.AcquisitionTimeoutUsageError = Nothing
|
||||
|
||||
hint (SQL.ConnectionUsageError _) = Nothing
|
||||
hint (SQL.SessionUsageError (SQL.PipelineError e)) = hint e
|
||||
hint (SQL.SessionUsageError (SQL.QueryError _ _ e)) = hint e
|
||||
hint SQL.AcquisitionTimeoutUsageError = Nothing
|
||||
|
||||
instance JSON.ToJSON SQL.CommandError where
|
||||
toJSON err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
instance ErrorBody SQL.CommandError where
|
||||
-- Special error raised with code PGRST, to allow full response control
|
||||
code (SQL.ResultError (SQL.ServerError "PGRST" m d _ _)) =
|
||||
@@ -586,8 +531,13 @@ instance ErrorBody SQL.CommandError where
|
||||
pgErrorStatus :: Bool -> SQL.UsageError -> HTTP.Status
|
||||
pgErrorStatus _ (SQL.ConnectionUsageError _) = HTTP.status503
|
||||
pgErrorStatus _ SQL.AcquisitionTimeoutUsageError = HTTP.status504
|
||||
pgErrorStatus _ (SQL.SessionUsageError (SQL.PipelineError (SQL.ClientError _))) = HTTP.status503
|
||||
pgErrorStatus _ (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) = HTTP.status503
|
||||
pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError rError))) =
|
||||
pgErrorStatus authed (SQL.SessionUsageError (SQL.PipelineError (SQL.ResultError rError))) = mapSQLtoHTTP authed rError
|
||||
pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError rError))) = mapSQLtoHTTP authed rError
|
||||
|
||||
mapSQLtoHTTP :: Bool -> SQL.ResultError -> HTTP.Status
|
||||
mapSQLtoHTTP authed rError =
|
||||
case rError of
|
||||
(SQL.ServerError c m d _ _) ->
|
||||
case BS.unpack c of
|
||||
@@ -641,86 +591,45 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError
|
||||
_ -> HTTP.status500
|
||||
|
||||
|
||||
data Error
|
||||
= ApiRequestError ApiRequestError
|
||||
| SchemaCacheErr SchemaCacheError
|
||||
| JwtErr JwtError
|
||||
| NoSchemaCacheError
|
||||
| PgErr PgError
|
||||
deriving Show
|
||||
instance ErrorHeaders Error where
|
||||
status (ApiRequestErr err) = status err
|
||||
status (SchemaCacheErr err) = status err
|
||||
status (JwtErr err) = status err
|
||||
status NoSchemaCacheError = HTTP.status503
|
||||
status (PgErr err) = status err
|
||||
|
||||
data JwtError
|
||||
= JwtDecodeErr JwtDecodeError
|
||||
| JwtSecretMissing
|
||||
| JwtTokenRequired
|
||||
| JwtClaimsErr JwtClaimsError
|
||||
deriving Show
|
||||
|
||||
data JwtDecodeError
|
||||
= EmptyAuthHeader
|
||||
| UnexpectedParts Int
|
||||
| KeyError Text
|
||||
| BadAlgorithm Text
|
||||
| BadCrypto
|
||||
| UnsupportedTokenType
|
||||
| UnreachableDecodeError
|
||||
deriving Show
|
||||
|
||||
data JwtClaimsError
|
||||
= JWTExpired
|
||||
| JWTNotYetValid
|
||||
| JWTIssuedAtFuture
|
||||
| JWTNotInAudience
|
||||
| ParsingClaimsFailed
|
||||
| ExpClaimNotNumber
|
||||
| NbfClaimNotNumber
|
||||
| IatClaimNotNumber
|
||||
| AudClaimNotStringOrArray
|
||||
deriving Show
|
||||
|
||||
instance PgrstError Error where
|
||||
status (ApiRequestError err) = status err
|
||||
status (SchemaCacheErr err) = status err
|
||||
status (JwtErr err) = status err
|
||||
status NoSchemaCacheError = HTTP.status503
|
||||
status (PgErr err) = status err
|
||||
|
||||
headers (ApiRequestError err) = proxyStatusHeader (code err) : headers err
|
||||
headers (SchemaCacheErr err) = proxyStatusHeader (code err) : headers err
|
||||
headers (JwtErr err) = proxyStatusHeader (code err) : headers err
|
||||
headers (PgErr err) = proxyStatusHeader (code err) : headers err
|
||||
headers err@NoSchemaCacheError = proxyStatusHeader (code err) : mempty
|
||||
|
||||
instance JSON.ToJSON Error where
|
||||
toJSON err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
headers (ApiRequestErr err) = headers err
|
||||
headers (SchemaCacheErr err) = headers err
|
||||
headers (JwtErr err) = headers err
|
||||
headers (PgErr err) = headers err
|
||||
headers NoSchemaCacheError = mempty
|
||||
|
||||
instance ErrorBody Error where
|
||||
code (ApiRequestError err) = code err
|
||||
code (SchemaCacheErr err) = code err
|
||||
code (JwtErr err) = code err
|
||||
code NoSchemaCacheError = "PGRST002"
|
||||
code (PgErr err) = code err
|
||||
code (ApiRequestErr err) = code err
|
||||
code (SchemaCacheErr err) = code err
|
||||
code (JwtErr err) = code err
|
||||
code NoSchemaCacheError = "PGRST002"
|
||||
code (PgErr err) = code err
|
||||
|
||||
message (ApiRequestError err) = message err
|
||||
message (ApiRequestErr err) = message err
|
||||
message (SchemaCacheErr err) = message err
|
||||
message (JwtErr err) = message err
|
||||
message NoSchemaCacheError = "Could not query the database for the schema cache. Retrying."
|
||||
message (PgErr err) = message err
|
||||
|
||||
details (ApiRequestError err) = details err
|
||||
details (SchemaCacheErr err) = details err
|
||||
details (JwtErr err) = details err
|
||||
details NoSchemaCacheError = Nothing
|
||||
details (PgErr err) = details err
|
||||
details (ApiRequestErr err) = details err
|
||||
details (SchemaCacheErr err) = details err
|
||||
details (JwtErr err) = details err
|
||||
details NoSchemaCacheError = Nothing
|
||||
details (PgErr err) = details err
|
||||
|
||||
hint (ApiRequestError err) = hint err
|
||||
hint (SchemaCacheErr err) = hint err
|
||||
hint (JwtErr err) = hint err
|
||||
hint NoSchemaCacheError = Nothing
|
||||
hint (PgErr err) = hint err
|
||||
hint (ApiRequestErr err) = hint err
|
||||
hint (SchemaCacheErr err) = hint err
|
||||
hint (JwtErr err) = hint err
|
||||
hint NoSchemaCacheError = Nothing
|
||||
hint (PgErr err) = hint err
|
||||
|
||||
instance PgrstError JwtError where
|
||||
instance ErrorHeaders JwtError where
|
||||
status JwtDecodeErr{} = HTTP.unauthorized401
|
||||
status JwtSecretMissing = HTTP.status500
|
||||
status JwtTokenRequired = HTTP.unauthorized401
|
||||
@@ -731,10 +640,6 @@ instance PgrstError JwtError where
|
||||
headers e@(JwtClaimsErr _) = [invalidTokenHeader $ message e]
|
||||
headers _ = mempty
|
||||
|
||||
instance JSON.ToJSON JwtError where
|
||||
toJSON err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
instance ErrorBody JwtError where
|
||||
code JwtSecretMissing = "PGRST300"
|
||||
code (JwtDecodeErr _) = "PGRST301"
|
||||
@@ -778,18 +683,6 @@ requiredTokenHeader :: Header
|
||||
requiredTokenHeader = ("WWW-Authenticate", "Bearer")
|
||||
|
||||
-- For parsing byteString to JSON Object, used for allowing full response control
|
||||
data PgRaiseErrMessage = PgRaiseErrMessage {
|
||||
getCode :: Text,
|
||||
getMessage :: Text,
|
||||
getDetails :: Maybe Text,
|
||||
getHint :: Maybe Text
|
||||
}
|
||||
|
||||
data PgRaiseErrDetails = PgRaiseErrDetails {
|
||||
getStatus :: Int,
|
||||
getStatusText :: Maybe Text,
|
||||
getHeaders :: Map Text Text
|
||||
}
|
||||
|
||||
instance JSON.FromJSON PgRaiseErrMessage where
|
||||
parseJSON (JSON.Object m) =
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
{-|
|
||||
Module : PostgREST.Error.Types
|
||||
Description : PostgREST Error Data Types
|
||||
-}
|
||||
module PostgREST.Error.Types
|
||||
( ApiRequestError(..)
|
||||
, QPError(..)
|
||||
, RangeError(..)
|
||||
, RaiseError(..)
|
||||
, SchemaCacheError(..)
|
||||
, PgError(..)
|
||||
, Error(..)
|
||||
, JwtError (..)
|
||||
, JwtDecodeError(..)
|
||||
, JwtClaimsError(..)
|
||||
, PgRaiseErrMessage(..)
|
||||
, PgRaiseErrDetails(..)
|
||||
) where
|
||||
|
||||
import qualified Hasql.Pool as SQL
|
||||
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import PostgREST.SchemaCache (SchemaCache (..))
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
||||
import PostgREST.SchemaCache.Relationship (Relationship (..),
|
||||
RelationshipsMap)
|
||||
import PostgREST.SchemaCache.Routine (Routine (..))
|
||||
import Protolude
|
||||
|
||||
data Error
|
||||
= ApiRequestErr ApiRequestError
|
||||
| SchemaCacheErr SchemaCacheError
|
||||
| JwtErr JwtError
|
||||
| NoSchemaCacheError
|
||||
| PgErr PgError
|
||||
deriving Show
|
||||
|
||||
-- API REQUEST ERRORS: PGRST1XX
|
||||
data ApiRequestError
|
||||
= AggregatesNotAllowed
|
||||
| MediaTypeError [ByteString]
|
||||
| InvalidBody ByteString
|
||||
| InvalidFilters
|
||||
| InvalidPreferences [ByteString]
|
||||
| InvalidRange RangeError
|
||||
| InvalidRpcMethod ByteString
|
||||
| NotEmbedded Text
|
||||
| NotImplemented Text
|
||||
| PutLimitNotAllowedError
|
||||
| QueryParamError QPError
|
||||
| RelatedOrderNotToOne Text Text
|
||||
| UnacceptableFilter Text
|
||||
| UnacceptableSchema Text [Text]
|
||||
| UnsupportedMethod ByteString
|
||||
| GucHeadersError
|
||||
| GucStatusError
|
||||
| PutMatchingPkError
|
||||
| SingularityError Integer
|
||||
| PGRSTParseError RaiseError
|
||||
| MaxAffectedViolationError Integer
|
||||
| InvalidResourcePath
|
||||
| OpenAPIDisabled
|
||||
| MaxAffectedRpcViolation
|
||||
deriving Show
|
||||
|
||||
data QPError = QPError Text Text
|
||||
deriving Show
|
||||
|
||||
data RaiseError
|
||||
= MsgParseError ByteString
|
||||
| DetParseError ByteString
|
||||
| NoDetail
|
||||
deriving Show
|
||||
|
||||
data RangeError
|
||||
= NegativeLimit
|
||||
| LowerGTUpper
|
||||
| OutOfBounds Text Text
|
||||
deriving Show
|
||||
|
||||
-- SCHEMA CACHE ERRORS: PGRST2XX
|
||||
data SchemaCacheError
|
||||
= AmbiguousRelBetween Text Text [Relationship]
|
||||
| AmbiguousRpc [Routine]
|
||||
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
||||
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
|
||||
| ColumnNotFound Text Text
|
||||
| TableNotFound Text Text SchemaCache
|
||||
deriving Show
|
||||
|
||||
-- JWT ERRORS: PGRST3XX
|
||||
data JwtError
|
||||
= JwtDecodeErr JwtDecodeError
|
||||
| JwtSecretMissing
|
||||
| JwtTokenRequired
|
||||
| JwtClaimsErr JwtClaimsError
|
||||
deriving Show
|
||||
|
||||
data JwtDecodeError
|
||||
= EmptyAuthHeader
|
||||
| UnexpectedParts Int
|
||||
| KeyError Text
|
||||
| BadAlgorithm Text
|
||||
| BadCrypto
|
||||
| UnsupportedTokenType
|
||||
| UnreachableDecodeError
|
||||
deriving Show
|
||||
|
||||
data JwtClaimsError
|
||||
= JWTExpired
|
||||
| JWTNotYetValid
|
||||
| JWTIssuedAtFuture
|
||||
| JWTNotInAudience
|
||||
| ParsingClaimsFailed
|
||||
| ExpClaimNotNumber
|
||||
| NbfClaimNotNumber
|
||||
| IatClaimNotNumber
|
||||
| AudClaimNotStringOrArray
|
||||
deriving Show
|
||||
|
||||
-- PG ERRORS
|
||||
type Authenticated = Bool
|
||||
data PgError = PgError Authenticated SQL.UsageError
|
||||
deriving Show
|
||||
|
||||
-- For parsing byteString to JSON Object, used for allowing full response control
|
||||
data PgRaiseErrMessage = PgRaiseErrMessage {
|
||||
getCode :: Text,
|
||||
getMessage :: Text,
|
||||
getDetails :: Maybe Text,
|
||||
getHint :: Maybe Text
|
||||
}
|
||||
|
||||
data PgRaiseErrDetails = PgRaiseErrDetails {
|
||||
getStatus :: Int,
|
||||
getStatusText :: Maybe Text,
|
||||
getHeaders :: Map Text Text
|
||||
}
|
||||
+53
-24
@@ -1,3 +1,4 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
@@ -10,29 +11,39 @@ import qualified Hasql.Notifications as SQL
|
||||
import PostgREST.AppState (AppState, getConfig)
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.Observation (Observation (..))
|
||||
import PostgREST.Version (prettyVersion)
|
||||
|
||||
import qualified PostgREST.AppState as AppState
|
||||
import qualified PostgREST.Config as Config
|
||||
|
||||
import Protolude
|
||||
import Control.Arrow ((&&&))
|
||||
import Data.Bitraversable (bisequence)
|
||||
import Data.Either.Combinators (whenRight)
|
||||
import qualified Data.Text as T
|
||||
import qualified Database.PostgreSQL.LibPQ as LibPQ
|
||||
import qualified Hasql.Session as SQL
|
||||
import PostgREST.Config.Database (queryPgVersion)
|
||||
import PostgREST.Config.PgVersion (pgvFullName)
|
||||
import Protolude
|
||||
|
||||
-- | Starts the Listener in a thread
|
||||
runListener :: AppState -> IO ()
|
||||
runListener appState = do
|
||||
AppConfig{..} <- getConfig appState
|
||||
when configDbChannelEnabled $
|
||||
void . forkIO $ retryingListen appState
|
||||
void . forkIO . void $ retryingListen appState
|
||||
|
||||
-- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost.
|
||||
retryingListen :: AppState -> IO ()
|
||||
-- | This function never returns (but can throw) and return type enforces that.
|
||||
retryingListen :: AppState -> IO Void
|
||||
retryingListen appState = do
|
||||
AppConfig{..} <- AppState.getConfig appState
|
||||
cfg@AppConfig{..} <- AppState.getConfig appState
|
||||
let
|
||||
dbChannel = toS configDbChannel
|
||||
handleFinally err = do
|
||||
onError err = do
|
||||
AppState.putIsListenerOn appState False
|
||||
observer $ DBListenFail dbChannel (Right err)
|
||||
when (isDbListenerBug err) $
|
||||
observer DBListenBugHint
|
||||
unless configDbPoolAutomaticRecovery $
|
||||
killThread mainThreadId
|
||||
|
||||
@@ -42,29 +53,43 @@ retryingListen appState = do
|
||||
threadDelay (delay * oneSecondInMicro)
|
||||
unless (delay == maxDelay) $
|
||||
AppState.putNextListenerDelay appState (delay * 2)
|
||||
-- loop running the listener
|
||||
retryingListen appState
|
||||
|
||||
-- forkFinally allows to detect if the thread dies
|
||||
void . flip forkFinally handleFinally $ do
|
||||
dbOrError <- SQL.acquire $ toUtf8 (Config.addTargetSessionAttrs $ Config.addFallbackAppName prettyVersion configDbUri)
|
||||
case dbOrError of
|
||||
Right db -> do
|
||||
SQL.listen db $ SQL.toPgIdentifier dbChannel
|
||||
AppState.putIsListenerOn appState True
|
||||
-- Execute the listener with with error handling
|
||||
handle onError $ do
|
||||
-- Make sure we don't leak connections on errors
|
||||
bracket
|
||||
-- acquire connection
|
||||
(SQL.acquire $
|
||||
Config.toConnectionSettings Config.addTargetSessionAttrs cfg)
|
||||
-- release connection
|
||||
(`whenRight` releaseConnection) $
|
||||
-- use connection
|
||||
\case
|
||||
Right db -> do
|
||||
SQL.listen db $ SQL.toPgIdentifier dbChannel
|
||||
(pqHost, pqPort) <- SQL.withLibPQConnection db $ bisequence . (LibPQ.host &&& LibPQ.port)
|
||||
pgFullName <- SQL.run queryPgVersion db >>= either throwIO (pure . pgvFullName)
|
||||
|
||||
delay <- AppState.getNextListenerDelay appState
|
||||
when (delay > 1) $ do -- if we did a retry
|
||||
-- assume we lost notifications, refresh the schema cache
|
||||
AppState.schemaCacheLoader appState
|
||||
-- reset the delay
|
||||
AppState.putNextListenerDelay appState 1
|
||||
AppState.putIsListenerOn appState True
|
||||
|
||||
observer $ DBListenStart dbChannel
|
||||
SQL.waitForNotifications handleNotification db
|
||||
delay <- AppState.getNextListenerDelay appState
|
||||
when (delay > 1) $ do -- if we did a retry
|
||||
-- assume we lost notifications, refresh the schema cache
|
||||
AppState.schemaCacheLoader appState
|
||||
-- reset the delay
|
||||
AppState.putNextListenerDelay appState 1
|
||||
|
||||
Left err -> do
|
||||
observer $ DBListenFail dbChannel (Left err)
|
||||
exitFailure
|
||||
observer $ DBListenStart pqHost pqPort pgFullName dbChannel
|
||||
|
||||
-- wait for notifications
|
||||
-- this will never return, in case of an error it will throw and be caught by onError
|
||||
forever $ SQL.waitForNotifications handleNotification db
|
||||
|
||||
Left err -> do
|
||||
observer $ DBListenFail dbChannel (Left err)
|
||||
exitFailure
|
||||
where
|
||||
observer = AppState.getObserver appState
|
||||
mainThreadId = AppState.getMainThreadId appState
|
||||
@@ -79,3 +104,7 @@ retryingListen appState = do
|
||||
|
||||
cacheReloader =
|
||||
AppState.schemaCacheLoader appState
|
||||
|
||||
releaseConnection = void . forkIO . handle (observer . DBListenerConnectionCleanupFail) . SQL.release
|
||||
|
||||
isDbListenerBug e = "could not access status of transaction" `T.isInfixOf` show e
|
||||
|
||||
+167
-48
@@ -1,4 +1,6 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE RecursiveDo #-}
|
||||
{-|
|
||||
Module : PostgREST.Logger
|
||||
Description : Logging based on the Observation.hs module. Access logs get sent to stdout and server diagnostic get sent to stderr.
|
||||
@@ -14,7 +16,6 @@ module PostgREST.Logger
|
||||
import Control.AutoUpdate (defaultUpdateSettings,
|
||||
mkAutoUpdate,
|
||||
updateAction)
|
||||
import Control.Debounce
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Hasql.Decoders as HD
|
||||
@@ -31,37 +32,36 @@ import qualified Network.Wai.Middleware.RequestLogger as Wai
|
||||
import Network.HTTP.Types.Status (Status, status400, status500)
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
|
||||
import PostgREST.Config (LogLevel (..))
|
||||
import PostgREST.Config (LogLevel (..), Verbosity (..))
|
||||
import PostgREST.Debounce (makeDebouncer)
|
||||
import PostgREST.Observation
|
||||
import PostgREST.Query (MainQuery (..))
|
||||
import PostgREST.SchemaCache (queryTimingsWLabels)
|
||||
|
||||
import Protolude
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import qualified Data.Text as T
|
||||
import qualified Hasql.Connection as SQL
|
||||
import qualified Hasql.Pool as SQL
|
||||
import qualified Hasql.Pool.Observation as SQL
|
||||
import Numeric (showFFloat)
|
||||
import PostgREST.Config.PgVersion (pgvName)
|
||||
import qualified PostgREST.Error as Error
|
||||
import Protolude
|
||||
|
||||
data LoggerState = LoggerState
|
||||
{ stateGetZTime :: IO ZonedTime -- ^ Time with time zone used for logs
|
||||
, stateLogDebouncePoolTimeout :: MVar (IO ()) -- ^ Logs with a debounce
|
||||
, stateLogDebouncePoolTimeout :: IO () -- ^ Logs with a debounce
|
||||
}
|
||||
|
||||
init :: IO LoggerState
|
||||
init = do
|
||||
init = mdo
|
||||
let
|
||||
oneSecond = 1000000
|
||||
loggerState = LoggerState zTime debouncePoolTimeout
|
||||
zTime <- mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
|
||||
LoggerState zTime <$> newEmptyMVar
|
||||
|
||||
logWithDebounce :: LoggerState -> IO () -> IO ()
|
||||
logWithDebounce loggerState action = do
|
||||
debouncer <- tryReadMVar $ stateLogDebouncePoolTimeout loggerState
|
||||
case debouncer of
|
||||
Just d -> d
|
||||
Nothing -> do
|
||||
newDebouncer <-
|
||||
let oneSecond = 1000000 in
|
||||
mkDebounce defaultDebounceSettings
|
||||
{ debounceAction = action
|
||||
, debounceFreq = 5*oneSecond
|
||||
, debounceEdge = leadingEdge -- logs at the start and the end
|
||||
}
|
||||
putMVar (stateLogDebouncePoolTimeout loggerState) newDebouncer
|
||||
newDebouncer
|
||||
debouncePoolTimeout <- makeDebouncer $
|
||||
logWithZTime loggerState (observationMessages PoolAcqTimeoutObs) *> threadDelay (5 * oneSecond)
|
||||
pure loggerState
|
||||
|
||||
-- TODO stop using this middleware to reuse the same "observer" pattern for all our logs
|
||||
middleware :: LogLevel -> (Wai.Request -> Maybe BS.ByteString) -> Wai.Middleware
|
||||
@@ -88,55 +88,174 @@ shouldLogResponse logLevel = case logLevel of
|
||||
-- All observations are logged except some that depend on the log-level
|
||||
observationLogger :: LoggerState -> LogLevel -> ObservationHandler
|
||||
observationLogger loggerState logLevel obs = case obs of
|
||||
o@(PoolAcqTimeoutObs _) -> do
|
||||
when (logLevel >= LogError) $ do
|
||||
logWithDebounce loggerState $
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
PoolAcqTimeoutObs -> do
|
||||
when (logLevel >= LogError) $
|
||||
stateLogDebouncePoolTimeout loggerState
|
||||
o@(QueryErrorCodeHighObs _) -> do
|
||||
when (logLevel >= LogError) $ do
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
o@SchemaCacheEmptyObs ->
|
||||
when (logLevel >= LogError) $ do
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
o@(HasqlPoolObs _) -> do
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
QueryObs gq status -> do
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
o@(QueryObs _ status) -> do
|
||||
when (shouldLogResponse logLevel status) $
|
||||
logMainQ loggerState gq
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
o@PoolRequest ->
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
o@PoolRequestFullfilled ->
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
o@PoolFlushed ->
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
o@JwtCacheEviction ->
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
o@(JwtCacheLookup _) ->
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
o@(WarpServerObs _) ->
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
o ->
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
|
||||
logWithZTime :: LoggerState -> Text -> IO ()
|
||||
logWithZTime loggerState txt = do
|
||||
logWithZTime :: LoggerState -> [Text] -> IO ()
|
||||
logWithZTime loggerState txts = do
|
||||
zTime <- stateGetZTime loggerState
|
||||
hPutStrLn stderr $ toS (formatTime defaultTimeLocale "%d/%b/%Y:%T %z: " zTime) <> txt
|
||||
|
||||
logMainQ :: LoggerState -> MainQuery -> IO ()
|
||||
logMainQ loggerState MainQuery{mqOpenAPI=(x, y, z),..} =
|
||||
let snipts = renderSnippet <$> [mqTxVars, fromMaybe mempty mqPreReq, mqMain, x, y, z, fromMaybe mempty mqExplain]
|
||||
-- Does not log SQL when it's empty (happens on OPTIONS requests and when the openapi queries are not generated)
|
||||
logQ q = when (q /= mempty) $ logWithZTime loggerState $ showOnSingleLine '\n' $ T.decodeUtf8 q in
|
||||
mapM_ logQ snipts
|
||||
traverse_ (hPutStrLn stderr . (toS (formatTime defaultTimeLocale "%d/%b/%Y:%T %z: " zTime) <>)) txts
|
||||
|
||||
-- TODO: maybe patch upstream hasql-dynamic-statements so we have a less hackish way to convert
|
||||
-- the SQL.Snippet or maybe don't use hasql-dynamic-statements and resort to plain strings for the queries and use regular hasql
|
||||
renderSnippet :: SQL.Snippet -> ByteString
|
||||
renderSnippet snippet =
|
||||
let SQL.Statement sql _ _ _ = SQL.dynamicallyParameterized snippet decoder prepared
|
||||
let SQL.Statement sql _ _ _ = SQL.dynamicallyParameterized snippet decoder False
|
||||
decoder = HD.noResult -- unused
|
||||
prepared = False -- unused
|
||||
in
|
||||
sql
|
||||
|
||||
observationMessages :: Observation -> [Text]
|
||||
observationMessages = \case
|
||||
AdminStartObs address ->
|
||||
pure $ "Admin server listening on " <> address
|
||||
AppStartObs ver ->
|
||||
pure $ "Starting PostgREST " <> T.decodeUtf8 ver <> "..."
|
||||
AppServerAddressObs address ->
|
||||
pure $ "API server listening on " <> address
|
||||
DBConnectedObs ver ->
|
||||
pure $ "Successfully connected to " <> ver
|
||||
ExitUnsupportedPgVersion pgVer minPgVer ->
|
||||
pure $ "Cannot run in this PostgreSQL version (" <> pgvName pgVer <> "), PostgREST needs at least " <> pgvName minPgVer
|
||||
ExitDBNoRecoveryObs ->
|
||||
pure "Automatic recovery disabled, exiting."
|
||||
ExitDBFatalError ServerAuthError usageErr ->
|
||||
pure $ "Failed to establish a connection. " <> jsonMessage usageErr
|
||||
ExitDBFatalError ServerPgrstBug usageErr ->
|
||||
pure $ "This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues. " <> jsonMessage usageErr
|
||||
ExitDBFatalError ServerError42P05 usageErr ->
|
||||
pure $ "If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. " <> jsonMessage usageErr
|
||||
ExitDBFatalError ServerError08P01 usageErr ->
|
||||
pure $ "Connection poolers in statement mode are not supported." <> jsonMessage usageErr
|
||||
SchemaCacheEmptyObs ->
|
||||
pure $ T.decodeUtf8 . LBS.toStrict . Error.errorPayload Verbose $ Error.NoSchemaCacheError
|
||||
SchemaCacheErrorObs dbSchemas extraPaths usageErr ->
|
||||
pure $ "Failed to load the schema cache using "
|
||||
<> "db-schemas=" <> T.intercalate "," (toList dbSchemas)
|
||||
<> " and "
|
||||
<> "db-extra-search-path=" <> T.intercalate "," extraPaths
|
||||
<> ". " <> jsonMessage usageErr
|
||||
SchemaCacheQueriedObs resultTime timings ->
|
||||
[ "Schema cache queried in " <> showMillis resultTime <> " milliseconds " ] <>
|
||||
let showTimings qt = [ T.intercalate ", " $ (\(l, v) -> T.decodeUtf8 l <> ": " <> v <> " ms") <$> queryTimingsWLabels qt ] in
|
||||
maybe mempty showTimings timings
|
||||
SchemaCacheLoadedObs resultTime summary ->
|
||||
[
|
||||
"Schema cache loaded " <> summary
|
||||
, "Schema cache loaded in " <> showMillis resultTime <> " milliseconds"
|
||||
]
|
||||
ConnectionRetryObs delay ->
|
||||
pure $ "Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
|
||||
QueryPgVersionError usageErr ->
|
||||
pure $ "Failed to query the PostgreSQL version. " <> jsonMessage usageErr
|
||||
DBListenStart host port fullName channel -> do
|
||||
pure $ "Listener connected to " <> fullName <> " on " <> show (fold $ host <> fmap (":" <>) port) <> " and listening for database notifications on the " <> show channel <> " channel"
|
||||
DBListenFail channel listenErr ->
|
||||
pure $ "Failed listening for database notifications on the " <> show channel <> " channel. " <>
|
||||
either showListenerConnError showListenerException listenErr
|
||||
DBListenRetry delay ->
|
||||
pure $ "Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
|
||||
DBListenBugHint ->
|
||||
pure "HINT: This is likely a bug in the notification queue, try executing the following to solve it: select pg_notification_queue_usage();"
|
||||
DBListenerGotSCacheMsg channel ->
|
||||
pure $ "Received a schema cache reload message on the " <> show channel <> " channel"
|
||||
DBListenerGotConfigMsg channel ->
|
||||
pure $ "Received a config reload message on the " <> show channel <> " channel"
|
||||
DBListenerConnectionCleanupFail ex ->
|
||||
pure $ "Failed during listener connection cleanup: " <> showOnSingleLine '\t' (show ex)
|
||||
(QueryObs MainQuery{mqOpenAPI=(x, y, z),..} _) ->
|
||||
let snipts = renderSnippet <$> [mqTxVars, fromMaybe mempty mqPreReq, mqMain, x, y, z, fromMaybe mempty mqExplain]
|
||||
in
|
||||
showOnSingleLine '\n' . T.decodeUtf8 <$> filter (/= mempty) snipts
|
||||
ConfigReadErrorObs usageErr ->
|
||||
pure $ "Failed to query database settings for the config parameters." <> jsonMessage usageErr
|
||||
QueryRoleSettingsErrorObs usageErr ->
|
||||
pure $ "Failed to query the role settings. " <> jsonMessage usageErr
|
||||
QueryErrorCodeHighObs usageErr ->
|
||||
pure $ jsonMessage usageErr
|
||||
ConfigInvalidObs err ->
|
||||
pure $ "Failed reloading config: " <> err
|
||||
ConfigSucceededObs ->
|
||||
pure "Config reloaded"
|
||||
PoolInit poolSize ->
|
||||
pure $ "Connection Pool initialized with a maximum size of " <> show poolSize <> " connections"
|
||||
PoolAcqTimeoutObs -> pure $ jsonMessage SQL.AcquisitionTimeoutUsageError
|
||||
HasqlPoolObs (SQL.ConnectionObservation uuid status) ->
|
||||
pure $ "Connection " <> show uuid <> (
|
||||
case status of
|
||||
SQL.ConnectingConnectionStatus -> " is being established"
|
||||
SQL.ReadyForUseConnectionStatus reason -> " is available due to " <> case reason of
|
||||
SQL.EstablishedConnectionReadyForUseReason -> "connection establishment"
|
||||
SQL.SessionFailedConnectionReadyForUseReason _ -> "session failure"
|
||||
SQL.SessionSucceededConnectionReadyForUseReason -> "session success"
|
||||
SQL.InUseConnectionStatus -> " is used"
|
||||
SQL.TerminatedConnectionStatus reason -> " is terminated due to " <> case reason of
|
||||
SQL.AgingConnectionTerminationReason -> "max lifetime"
|
||||
SQL.IdlenessConnectionTerminationReason -> "max idletime"
|
||||
SQL.ReleaseConnectionTerminationReason -> "release"
|
||||
SQL.NetworkErrorConnectionTerminationReason _ -> "network error" -- usage error is already logged, no need to repeat the same message.
|
||||
SQL.InitializationErrorTerminationReason _ -> "init failure"
|
||||
)
|
||||
PoolRequest ->
|
||||
pure "Trying to borrow a connection from pool"
|
||||
PoolRequestFullfilled ->
|
||||
pure "Borrowed a connection from the pool"
|
||||
PoolFlushed ->
|
||||
pure "Database connection pool flushed"
|
||||
JwtCacheLookup _ ->
|
||||
pure "Looked up a JWT in JWT cache"
|
||||
JwtCacheEviction ->
|
||||
pure "Evicted entry from JWT cache"
|
||||
TerminationUnixSignalObs signal ->
|
||||
pure $ "Received termination unix signal " <> signal
|
||||
WarpServerObs txt ->
|
||||
pure $ "Warp server: " <> txt
|
||||
where
|
||||
showMillis :: Double -> Text
|
||||
showMillis x = toS $ showFFloat (Just 1) x ""
|
||||
|
||||
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload Verbose $ Error.PgError False err
|
||||
|
||||
|
||||
showListenerConnError :: SQL.ConnectionError -> Text
|
||||
showListenerConnError = maybe "Connection error" (showOnSingleLine '\t' . T.decodeUtf8)
|
||||
|
||||
showListenerException :: SomeException -> Text
|
||||
showListenerException = showOnSingleLine '\t' . show
|
||||
|
||||
|
||||
showOnSingleLine :: Char -> Text -> Text
|
||||
showOnSingleLine split txt = T.intercalate " " $ T.filter (/= split) <$> T.lines txt -- the errors from hasql-notifications come intercalated with "\t\n"
|
||||
|
||||
@@ -96,9 +96,8 @@ data ResultSet
|
||||
mainTx :: MainQuery -> AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> MainTx
|
||||
mainTx _ _ _ _ (NoDb x) _ = NoDbTx $ NoDbResult x
|
||||
mainTx genQ@MainQuery{..} conf@AppConfig{..} AuthResult{..} apiReq (Db plan) sCache =
|
||||
DbTx isoLvl txMode dbHandler transaction
|
||||
DbTx isoLvl txMode dbHandler SQL.transactionNoRetry
|
||||
where
|
||||
transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction
|
||||
isoLvl = planIsoLvl conf authRole plan
|
||||
txMode = planTxMode plan
|
||||
dbHandler = do
|
||||
@@ -222,7 +221,7 @@ failPut :: ResultSet -> DbHandler ()
|
||||
failPut RSStandard{rsQueryTotal=queryTotal} =
|
||||
when (queryTotal /= 1) $ do
|
||||
lift SQL.condemn
|
||||
throwError $ Error.ApiRequestError Error.PutMatchingPkError
|
||||
throwError $ Error.ApiRequestErr Error.PutMatchingPkError
|
||||
|
||||
-- |
|
||||
-- Fail a response if a single JSON object was requested and not exactly one
|
||||
@@ -231,13 +230,13 @@ failNotSingular :: MediaType -> ResultSet -> DbHandler ()
|
||||
failNotSingular mediaType RSStandard{rsQueryTotal=queryTotal} =
|
||||
when (elem mediaType [MTVndSingularJSON True, MTVndSingularJSON False] && queryTotal /= 1) $ do
|
||||
lift SQL.condemn
|
||||
throwError $ Error.ApiRequestError . Error.SingularityError $ toInteger queryTotal
|
||||
throwError $ Error.ApiRequestErr . Error.SingularityError $ toInteger queryTotal
|
||||
|
||||
failExceedsMaxAffectedPref :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> ResultSet -> DbHandler ()
|
||||
failExceedsMaxAffectedPref (Nothing,_) _ = pure ()
|
||||
failExceedsMaxAffectedPref (Just (PreferMaxAffected n), handling) RSStandard{rsQueryTotal=queryTotal} = when ((queryTotal > n) && (handling == Just Strict)) $ do
|
||||
lift SQL.condemn
|
||||
throwError $ Error.ApiRequestError . Error.MaxAffectedViolationError $ toInteger queryTotal
|
||||
throwError $ Error.ApiRequestErr . Error.MaxAffectedViolationError $ toInteger queryTotal
|
||||
|
||||
-- | Set a transaction to roll back if requested
|
||||
optionalRollback :: AppConfig -> ApiRequest -> DbHandler ()
|
||||
|
||||
@@ -50,10 +50,10 @@ init configDbPoolSize = do
|
||||
-- Only some observations are used as metrics
|
||||
observationMetrics :: MetricsState -> ObservationHandler
|
||||
observationMetrics MetricsState{..} obs = case obs of
|
||||
(PoolAcqTimeoutObs _) -> do
|
||||
PoolAcqTimeoutObs -> do
|
||||
incCounter poolTimeouts
|
||||
(HasqlPoolObs (SQL.ConnectionObservation _ status)) -> case status of
|
||||
SQL.ReadyForUseConnectionStatus -> do
|
||||
SQL.ReadyForUseConnectionStatus _ -> do
|
||||
incGauge poolAvailable
|
||||
SQL.InUseConnectionStatus -> do
|
||||
decGauge poolAvailable
|
||||
@@ -64,7 +64,7 @@ observationMetrics MetricsState{..} obs = case obs of
|
||||
incGauge poolWaiting
|
||||
PoolRequestFullfilled ->
|
||||
decGauge poolWaiting
|
||||
SchemaCacheLoadedObs resTime -> do
|
||||
SchemaCacheLoadedObs resTime _ -> do
|
||||
withLabel schemaCacheLoads "SUCCESS" incCounter
|
||||
setGauge schemaCacheQueryTime resTime
|
||||
SchemaCacheErrorObs{} -> do
|
||||
|
||||
+13
-120
@@ -1,4 +1,4 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-|
|
||||
Module : PostgREST.Observation
|
||||
Description : This module holds an Observation type which is the core of Observability for PostgREST.
|
||||
@@ -9,23 +9,16 @@ Description : This module holds an Observation type which is the core of Observa
|
||||
module PostgREST.Observation
|
||||
( Observation(..)
|
||||
, ObsFatalError(..)
|
||||
, observationMessage
|
||||
, ObservationHandler
|
||||
, showOnSingleLine
|
||||
) where
|
||||
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import Data.List.NonEmpty (toList)
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Hasql.Connection as SQL
|
||||
import qualified Hasql.Pool as SQL
|
||||
import qualified Hasql.Pool.Observation as SQL
|
||||
import Network.HTTP.Types.Status (Status)
|
||||
import Numeric (showFFloat)
|
||||
import PostgREST.Config.PgVersion
|
||||
import qualified PostgREST.Error as Error
|
||||
import PostgREST.Query (MainQuery)
|
||||
import PostgREST.SchemaCache (QueryTimings)
|
||||
|
||||
import Protolude hiding (toList)
|
||||
|
||||
@@ -39,15 +32,16 @@ data Observation
|
||||
| DBConnectedObs Text
|
||||
| SchemaCacheEmptyObs
|
||||
| SchemaCacheErrorObs (NonEmpty Text) [Text] SQL.UsageError
|
||||
| SchemaCacheQueriedObs Double
|
||||
| SchemaCacheSummaryObs Text
|
||||
| SchemaCacheLoadedObs Double
|
||||
| SchemaCacheQueriedObs Double (Maybe QueryTimings)
|
||||
| SchemaCacheLoadedObs Double Text
|
||||
| ConnectionRetryObs Int
|
||||
| DBListenStart Text
|
||||
| DBListenFail Text (Either SQL.ConnectionError (Either SomeException ()))
|
||||
| DBListenStart (Maybe ByteString) (Maybe ByteString) Text Text -- host, port, version string, channel
|
||||
| DBListenFail Text (Either SQL.ConnectionError SomeException)
|
||||
| DBListenRetry Int
|
||||
| DBListenBugHint -- https://github.com/PostgREST/postgrest/issues/3147
|
||||
| DBListenerGotSCacheMsg ByteString
|
||||
| DBListenerGotConfigMsg ByteString
|
||||
| DBListenerConnectionCleanupFail SomeException
|
||||
| QueryObs MainQuery Status
|
||||
| ConfigReadErrorObs SQL.UsageError
|
||||
| ConfigInvalidObs Text
|
||||
@@ -56,118 +50,17 @@ data Observation
|
||||
| QueryErrorCodeHighObs SQL.UsageError
|
||||
| QueryPgVersionError SQL.UsageError
|
||||
| PoolInit Int
|
||||
| PoolAcqTimeoutObs SQL.UsageError
|
||||
| PoolAcqTimeoutObs
|
||||
| HasqlPoolObs SQL.Observation
|
||||
| PoolRequest
|
||||
| PoolRequestFullfilled
|
||||
| PoolFlushed
|
||||
| JwtCacheLookup Bool
|
||||
| JwtCacheEviction
|
||||
| TerminationUnixSignalObs Text
|
||||
| WarpServerObs Text
|
||||
deriving (Generic)
|
||||
|
||||
data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01
|
||||
|
||||
type ObservationHandler = Observation -> IO ()
|
||||
|
||||
observationMessage :: Observation -> Text
|
||||
observationMessage = \case
|
||||
AdminStartObs address ->
|
||||
"Admin server listening on " <> address
|
||||
AppStartObs ver ->
|
||||
"Starting PostgREST " <> T.decodeUtf8 ver <> "..."
|
||||
AppServerAddressObs address ->
|
||||
"API server listening on " <> address
|
||||
DBConnectedObs ver ->
|
||||
"Successfully connected to " <> ver
|
||||
ExitUnsupportedPgVersion pgVer minPgVer ->
|
||||
"Cannot run in this PostgreSQL version (" <> pgvName pgVer <> "), PostgREST needs at least " <> pgvName minPgVer
|
||||
ExitDBNoRecoveryObs ->
|
||||
"Automatic recovery disabled, exiting."
|
||||
ExitDBFatalError ServerAuthError usageErr ->
|
||||
"Failed to establish a connection. " <> jsonMessage usageErr
|
||||
ExitDBFatalError ServerPgrstBug usageErr ->
|
||||
"This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues. " <> jsonMessage usageErr
|
||||
ExitDBFatalError ServerError42P05 usageErr ->
|
||||
"If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. " <> jsonMessage usageErr
|
||||
ExitDBFatalError ServerError08P01 usageErr ->
|
||||
"Connection poolers in statement mode are not supported." <> jsonMessage usageErr
|
||||
SchemaCacheEmptyObs ->
|
||||
T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.NoSchemaCacheError
|
||||
SchemaCacheErrorObs dbSchemas extraPaths usageErr ->
|
||||
"Failed to load the schema cache using "
|
||||
<> "db-schemas=" <> T.intercalate "," (toList dbSchemas)
|
||||
<> " and "
|
||||
<> "db-extra-search-path=" <> T.intercalate "," extraPaths
|
||||
<> ". " <> jsonMessage usageErr
|
||||
SchemaCacheQueriedObs resultTime ->
|
||||
"Schema cache queried in " <> showMillis resultTime <> " milliseconds"
|
||||
SchemaCacheSummaryObs summary ->
|
||||
"Schema cache loaded " <> summary
|
||||
SchemaCacheLoadedObs resultTime ->
|
||||
"Schema cache loaded in " <> showMillis resultTime <> " milliseconds"
|
||||
ConnectionRetryObs delay ->
|
||||
"Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
|
||||
QueryPgVersionError usageErr ->
|
||||
"Failed to query the PostgreSQL version. " <> jsonMessage usageErr
|
||||
DBListenStart channel -> do
|
||||
"Listening for database notifications on the " <> show channel <> " channel"
|
||||
DBListenFail channel listenErr ->
|
||||
"Failed listening for database notifications on the " <> show channel <> " channel. " <>
|
||||
either showListenerConnError showListenerException listenErr
|
||||
DBListenRetry delay ->
|
||||
"Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
|
||||
DBListenerGotSCacheMsg channel ->
|
||||
"Received a schema cache reload message on the " <> show channel <> " channel"
|
||||
DBListenerGotConfigMsg channel ->
|
||||
"Received a config reload message on the " <> show channel <> " channel"
|
||||
QueryObs{} ->
|
||||
mempty -- TODO pending refactor: The logic for printing the query cannot be done here. Join the observationMessage function into observationLogger to avoid this mempty.
|
||||
ConfigReadErrorObs usageErr ->
|
||||
"Failed to query database settings for the config parameters." <> jsonMessage usageErr
|
||||
QueryRoleSettingsErrorObs usageErr ->
|
||||
"Failed to query the role settings. " <> jsonMessage usageErr
|
||||
QueryErrorCodeHighObs usageErr ->
|
||||
jsonMessage usageErr
|
||||
ConfigInvalidObs err ->
|
||||
"Failed reloading config: " <> err
|
||||
ConfigSucceededObs ->
|
||||
"Config reloaded"
|
||||
PoolInit poolSize ->
|
||||
"Connection Pool initialized with a maximum size of " <> show poolSize <> " connections"
|
||||
PoolAcqTimeoutObs usageErr ->
|
||||
jsonMessage usageErr
|
||||
HasqlPoolObs (SQL.ConnectionObservation uuid status) ->
|
||||
"Connection " <> show uuid <> (
|
||||
case status of
|
||||
SQL.ConnectingConnectionStatus -> " is being established"
|
||||
SQL.ReadyForUseConnectionStatus -> " is available"
|
||||
SQL.InUseConnectionStatus -> " is used"
|
||||
SQL.TerminatedConnectionStatus reason -> " is terminated due to " <> case reason of
|
||||
SQL.AgingConnectionTerminationReason -> "max lifetime"
|
||||
SQL.IdlenessConnectionTerminationReason -> "max idletime"
|
||||
SQL.ReleaseConnectionTerminationReason -> "release"
|
||||
SQL.NetworkErrorConnectionTerminationReason _ -> "network error" -- usage error is already logged, no need to repeat the same message.
|
||||
)
|
||||
PoolRequest ->
|
||||
"Trying to borrow a connection from pool"
|
||||
PoolRequestFullfilled ->
|
||||
"Borrowed a connection from the pool"
|
||||
JwtCacheLookup _ ->
|
||||
"Looked up a JWT in JWT cache"
|
||||
JwtCacheEviction ->
|
||||
"Evicted entry from JWT cache"
|
||||
where
|
||||
showMillis :: Double -> Text
|
||||
showMillis x = toS $ showFFloat (Just 1) (x * 1000) ""
|
||||
|
||||
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
|
||||
|
||||
|
||||
showListenerConnError :: SQL.ConnectionError -> Text
|
||||
showListenerConnError = maybe "Connection error" (showOnSingleLine '\t' . T.decodeUtf8)
|
||||
|
||||
showListenerException :: Either SomeException () -> Text
|
||||
showListenerException (Right _) = "Failed getting notifications" -- should not happen as the listener will never finish (hasql-notifications uses `forever` internally) with a Right result
|
||||
showListenerException (Left e) = showOnSingleLine '\t' $ show e
|
||||
|
||||
|
||||
showOnSingleLine :: Char -> Text -> Text
|
||||
showOnSingleLine split txt = T.intercalate " " $ T.filter (/= split) <$> T.lines txt -- the errors from hasql-notifications come intercalated with "\t\n"
|
||||
|
||||
+41
-66
@@ -43,6 +43,7 @@ import PostgREST.Error (ApiRequestError (..),
|
||||
Error (..),
|
||||
SchemaCacheError (..))
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import PostgREST.Plan.Negotiate (negotiateContent)
|
||||
import PostgREST.Query.SqlFragment (sourceCTEName)
|
||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
convertToLimitZeroRange,
|
||||
@@ -50,7 +51,6 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
import PostgREST.SchemaCache (SchemaCache (..))
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||
QualifiedIdentifier (..),
|
||||
RelIdentifier (..),
|
||||
Schema)
|
||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
Junction (..),
|
||||
@@ -60,8 +60,6 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
import PostgREST.SchemaCache.Representations (DataRepresentation (..),
|
||||
RepresentationsMap)
|
||||
import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
||||
MediaHandlerMap,
|
||||
ResolvedHandler,
|
||||
Routine (..),
|
||||
RoutineMap,
|
||||
RoutineParam (..),
|
||||
@@ -172,19 +170,19 @@ dbActionPlan dbAct conf apiReq sCache = case dbAct of
|
||||
|
||||
wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Bool -> Either Error CrudPlan
|
||||
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} headersOnly = do
|
||||
qi <- findTable identifier (dbTables sCache)
|
||||
qi <- findTable identifier sCache
|
||||
rPlan <- readPlan qi conf sCache apiRequest
|
||||
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||
(handler, mediaType) <- mapLeft ApiRequestErr $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestErr $ InvalidPreferences invalidPrefs else Right ()
|
||||
return $ WrappedReadPlan rPlan SQL.Read handler mediaType headersOnly qi
|
||||
|
||||
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error CrudPlan
|
||||
mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do
|
||||
qi <- findTable identifier (dbTables sCache)
|
||||
qi <- findTable identifier sCache
|
||||
rPlan <- readPlan qi conf sCache apiRequest
|
||||
mPlan <- mutatePlan mutation qi apiRequest sCache rPlan
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestErr $ InvalidPreferences invalidPrefs else Right ()
|
||||
(handler, mediaType) <- mapLeft ApiRequestErr $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation qi
|
||||
|
||||
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CrudPlan
|
||||
@@ -206,15 +204,15 @@ callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferenc
|
||||
(Inv, Routine.Immutable) -> SQL.Read
|
||||
(Inv, Routine.Volatile) -> SQL.Write
|
||||
cPlan = callPlan proc apiRequest paramKeys args rPlan
|
||||
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||
(handler, mediaType) <- mapLeft ApiRequestErr $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestErr $ InvalidPreferences invalidPrefs else Right ()
|
||||
failMaxAffectedRpcReturnsSingle (preferMaxAffected, preferHandling) proc
|
||||
return $ CallReadPlan rPlan cPlan txMode proc handler mediaType invMethod identifier
|
||||
where
|
||||
qsParams' = QueryParams.qsParams iQueryParams
|
||||
|
||||
failMaxAffectedRpcReturnsSingle :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> Routine -> Either Error ()
|
||||
failMaxAffectedRpcReturnsSingle (Just (PreferMaxAffected _), Just Strict) rout = if funcReturnsSingle rout then Left $ ApiRequestError MaxAffectedRpcViolation else Right ()
|
||||
failMaxAffectedRpcReturnsSingle (Just (PreferMaxAffected _), Just Strict) rout = if funcReturnsSingle rout then Left $ ApiRequestErr MaxAffectedRpcViolation else Right ()
|
||||
failMaxAffectedRpcReturnsSingle _ _ = Right ()
|
||||
|
||||
hasDefaultSelect :: ReadPlanTree -> Bool
|
||||
@@ -227,7 +225,7 @@ inspectPlan apiRequest headersOnly schema = do
|
||||
accepts = iAcceptMediaType apiRequest
|
||||
mediaType <- if not . null $ L.intersect accepts producedMTs
|
||||
then Right MTOpenAPI
|
||||
else Left . ApiRequestError . MediaTypeError $ MediaType.toMime <$> accepts
|
||||
else Left . ApiRequestErr . MediaTypeError $ MediaType.toMime <$> accepts
|
||||
return $ InspectPlan mediaType SQL.Read headersOnly schema
|
||||
|
||||
{-|
|
||||
@@ -256,14 +254,16 @@ findProc qi argumentsKeys allProcs contentMediaType isInvPost =
|
||||
| hasSingleUnnamedParam proc = (ts,proc:fs)
|
||||
| otherwise = (ts,fs)
|
||||
-- If the function is called with post and has a single unnamed parameter
|
||||
-- it can be called depending on content type and the parameter type
|
||||
hasSingleUnnamedParam Function{pdParams=[RoutineParam{ppType}]} = isInvPost && case (contentMediaType, ppType) of
|
||||
(MTApplicationJSON, "json") -> True
|
||||
(MTApplicationJSON, "jsonb") -> True
|
||||
(MTTextPlain, "text") -> True
|
||||
(MTTextXML, "xml") -> True
|
||||
(MTOctetStream, "bytea") -> True
|
||||
_ -> False
|
||||
-- it can be called depending on content type and the parameter type.
|
||||
-- The parameter must have no declared name (ppName == mempty).
|
||||
hasSingleUnnamedParam Function{pdParams=[RoutineParam{ppName, ppType}]} =
|
||||
isInvPost && ppName == mempty && case (contentMediaType, ppType) of
|
||||
(MTApplicationJSON, "json") -> True
|
||||
(MTApplicationJSON, "jsonb") -> True
|
||||
(MTTextPlain, "text") -> True
|
||||
(MTTextXML, "xml") -> True
|
||||
(MTOctetStream, "bytea") -> True
|
||||
_ -> False
|
||||
hasSingleUnnamedParam _ = False
|
||||
matchesParams proc =
|
||||
let
|
||||
@@ -784,7 +784,7 @@ hoistIntoRelSelectFields _ r = r
|
||||
-- to order once it's aggregated if it's not selected in the inner query beforehand.
|
||||
addToManyOrderSelects :: ReadPlanTree -> Either Error ReadPlanTree
|
||||
addToManyOrderSelects (Node rp@ReadPlan{order, select, relAggAlias, relSelect, relSpread = Just ToManySpread {}} forest)
|
||||
| anyAggSel || anyAggRelSel = Left $ ApiRequestError $ NotImplemented "Aggregates are not implemented for one-to-many or many-to-many spreads."
|
||||
| anyAggSel || anyAggRelSel = Left $ ApiRequestErr $ NotImplemented "Aggregates are not implemented for one-to-many or many-to-many spreads."
|
||||
| otherwise = Node rp { order = [], relSpread = newRelSpread } <$> addToManyOrderSelects `traverse` forest
|
||||
where
|
||||
newRelSpread = Just ToManySpread { stExtraSelect = addSprExtraSelects, stOrder = addSprOrder}
|
||||
@@ -806,14 +806,14 @@ addToManyOrderSelects (Node rp forest) = Node rp <$> addToManyOrderSelects `trav
|
||||
|
||||
validateAggFunctions :: Bool -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
validateAggFunctions aggFunctionsAllowed (Node rp@ReadPlan {select} forest)
|
||||
| not aggFunctionsAllowed && any (isJust . csAggFunction) select = Left $ ApiRequestError AggregatesNotAllowed
|
||||
| not aggFunctionsAllowed && any (isJust . csAggFunction) select = Left $ ApiRequestErr AggregatesNotAllowed
|
||||
| otherwise = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest
|
||||
|
||||
-- | Lookup table in the schema cache before creating read plan
|
||||
findTable :: QualifiedIdentifier -> TablesMap -> Either Error QualifiedIdentifier
|
||||
findTable qi@QualifiedIdentifier{..} tableMap =
|
||||
case HM.lookup qi tableMap of
|
||||
Nothing -> Left $ SchemaCacheErr $ TableNotFound qiSchema qiName (HM.elems tableMap)
|
||||
findTable :: QualifiedIdentifier -> SchemaCache -> Either Error QualifiedIdentifier
|
||||
findTable qi@QualifiedIdentifier{..} sc@SchemaCache{dbTables} =
|
||||
case HM.lookup qi dbTables of
|
||||
Nothing -> Left $ SchemaCacheErr $ TableNotFound qiSchema qiName sc
|
||||
Just _ -> Right qi
|
||||
|
||||
addFilters :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
@@ -860,9 +860,9 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||
name = fromMaybe relName relAlias in
|
||||
if isToOne == Just True
|
||||
then Right $ cot{coRelation=relAggAlias}
|
||||
else Left $ ApiRequestError $ RelatedOrderNotToOne (qiName from) name
|
||||
else Left $ ApiRequestErr $ RelatedOrderNotToOne (qiName from) name
|
||||
Nothing ->
|
||||
Left $ ApiRequestError $ NotEmbedded coRelation
|
||||
Left $ ApiRequestErr $ NotEmbedded coRelation
|
||||
|
||||
-- | Searches for null filters on embeds, e.g. `projects=not.is.null` on `GET /clients?select=*,projects(*)&projects=not.is.null`
|
||||
--
|
||||
@@ -956,17 +956,24 @@ addRanges ApiRequest{..} rReq =
|
||||
_ -> foldr addRangeToNode (Right rReq) =<< ranges
|
||||
where
|
||||
ranges :: Either Error [(EmbedPath, NonnegRange)]
|
||||
ranges = first (ApiRequestError . QueryParamError) $ QueryParams.pRequestRange `traverse` HM.toList iRange
|
||||
ranges = first (ApiRequestErr . QueryParamError) $ QueryParams.pRequestRange `traverse` HM.toList iRange
|
||||
|
||||
addRangeToNode :: (EmbedPath, NonnegRange) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
||||
addRangeToNode = updateNode (\r (Node q f) -> Node q{range_=r} f)
|
||||
|
||||
addLogicTrees :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
addLogicTrees ctx ApiRequest{..} rReq =
|
||||
foldr addLogicTreeToNode (Right rReq) qsLogic
|
||||
foldr addLogicTreeToNode (Right rReq) logic
|
||||
where
|
||||
QueryParams.QueryParams{..} = iQueryParams
|
||||
|
||||
logic =
|
||||
case iAction of
|
||||
ActDb (ActRelationRead _ _) -> qsLogic
|
||||
ActDb (ActRoutine _ _) -> qsLogic
|
||||
-- For mutations, take the non-root logic filters. These will only affect the embeddings and not the top level of the returned representation.
|
||||
_ -> filter (not . null . fst) qsLogic
|
||||
|
||||
addLogicTreeToNode :: (EmbedPath, LogicTree) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
||||
addLogicTreeToNode = updateNode (\t (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=resolveLogicTree ctx{qi=fromTable} t:lf} f)
|
||||
|
||||
@@ -983,13 +990,13 @@ updateNode f ([], a) rr = f a <$> rr
|
||||
updateNode _ _ (Left e) = Left e
|
||||
updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
|
||||
case findNode of
|
||||
Nothing -> Left $ ApiRequestError $ NotEmbedded targetNodeName
|
||||
Nothing -> Left $ ApiRequestErr $ NotEmbedded targetNodeName
|
||||
Just target ->
|
||||
(\node -> Node rootNode $ node : delete target forest) <$>
|
||||
updateNode f (remainingPath, a) (Right target)
|
||||
where
|
||||
findNode :: Maybe ReadPlanTree
|
||||
findNode = find (\(Node ReadPlan{relName, relAlias} _) -> relName == targetNodeName || relAlias == Just targetNodeName) forest
|
||||
findNode = find (\(Node ReadPlan{relName, relAlias} _) -> fromMaybe relName relAlias == targetNodeName) forest
|
||||
|
||||
mutatePlan :: Mutation -> QualifiedIdentifier -> ApiRequest -> SchemaCache -> ReadPlanTree -> Either Error MutatePlan
|
||||
mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{dbTables, dbRepresentations} readReq =
|
||||
@@ -1007,7 +1014,7 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
|
||||
_ -> False) qsFiltersRoot
|
||||
then mapRight (\typedColumns -> Insert qi typedColumns body (Just (MergeDuplicates, pkCols)) combinedLogic returnings mempty False) typedColumnsOrError
|
||||
else
|
||||
Left $ ApiRequestError InvalidFilters
|
||||
Left $ ApiRequestErr InvalidFilters
|
||||
MutationDelete -> Right $ Delete qi combinedLogic returnings
|
||||
where
|
||||
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
||||
@@ -1114,35 +1121,3 @@ inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
|
||||
-- they are later concatenated with AND in the QueryBuilder
|
||||
addFilterToLogicForest :: CoercibleFilter -> [CoercibleLogicTree] -> [CoercibleLogicTree]
|
||||
addFilterToLogicForest flt lf = CoercibleStmnt flt : lf
|
||||
|
||||
-- | Do content negotiation. i.e. choose a media type based on the intersection of accepted/produced media types.
|
||||
negotiateContent :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] -> MediaHandlerMap -> Bool -> Either ApiRequestError ResolvedHandler
|
||||
negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} identifier accepts produces defaultSelect =
|
||||
case (act, firstAcceptedPick) of
|
||||
(_, Nothing) -> Left . MediaTypeError $ map MediaType.toMime accepts
|
||||
(ActDb (ActRelationMut _ _), Just (x, mt)) -> Right (if rep == Just Full then x else NoAgg, mt)
|
||||
-- no need for an aggregate on HEAD https://github.com/PostgREST/postgrest/issues/2849
|
||||
-- TODO: despite no aggregate, these are responding with a Content-Type, which is not correct.
|
||||
(ActDb (ActRelationRead _ True), Just (_, mt)) -> Right (NoAgg, mt)
|
||||
(ActDb (ActRoutine _ (InvRead True)), Just (_, mt)) -> Right (NoAgg, mt)
|
||||
(_, Just (x, mt)) -> Right (x, mt)
|
||||
where
|
||||
firstAcceptedPick = listToMaybe $ mapMaybe matchMT accepts -- If there are multiple accepted media types, pick the first. This is usual in content negotiation.
|
||||
matchMT mt = case mt of
|
||||
-- all the vendored media types have special handling as they have media type parameters, they cannot be overridden
|
||||
m@(MTVndSingularJSON strip) -> Just (BuiltinAggSingleJson strip, m)
|
||||
m@MTVndArrayJSONStrip -> Just (BuiltinAggArrayJsonStrip, m)
|
||||
m@(MTVndPlan (MTVndSingularJSON strip) _ _) -> mtPlanToNothing $ Just (BuiltinAggSingleJson strip, m)
|
||||
m@(MTVndPlan MTVndArrayJSONStrip _ _) -> mtPlanToNothing $ Just (BuiltinAggArrayJsonStrip, m)
|
||||
-- TODO the plan should have its own MediaHandler instead of relying on MediaType
|
||||
m@(MTVndPlan mType _ _) -> mtPlanToNothing $ ((,) . fst <$> lookupHandler mType) <*> pure m
|
||||
-- all the other media types can be overridden
|
||||
x -> lookupHandler x
|
||||
mtPlanToNothing x = if configDbPlanEnabled conf then x else Nothing -- don't find anything if the plan media type is not allowed
|
||||
lookupHandler mt =
|
||||
when' defaultSelect (HM.lookup (RelId identifier, MTAny) produces) <|> -- lookup for identifier and `*/*`
|
||||
when' defaultSelect (HM.lookup (RelId identifier, mt) produces) <|> -- lookup for identifier and a particular media type
|
||||
HM.lookup (RelAnyElement, mt) produces -- lookup for anyelement and a particular media type
|
||||
when' :: Bool -> Maybe a -> Maybe a
|
||||
when' True (Just a) = Just a
|
||||
when' _ _ = Nothing
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
{-|
|
||||
Module : PostgREST.Plan.Negotiate
|
||||
Description : PostgREST Content Negotiation
|
||||
|
||||
This module contains logic for content negotiation.
|
||||
RFC: https://datatracker.ietf.org/doc/html/rfc7231#section-3.4
|
||||
-}
|
||||
|
||||
module PostgREST.Plan.Negotiate
|
||||
( negotiateContent
|
||||
) where
|
||||
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
|
||||
import PostgREST.ApiRequest (ApiRequest (..))
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.Error (ApiRequestError (..))
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||
RelIdentifier (..))
|
||||
import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
||||
MediaHandlerMap,
|
||||
ResolvedHandler)
|
||||
|
||||
import PostgREST.ApiRequest.Preferences
|
||||
import PostgREST.ApiRequest.Types
|
||||
import qualified PostgREST.MediaType as MediaType
|
||||
|
||||
import Protolude hiding (from)
|
||||
|
||||
-- We have two general cases of return values from database objects
|
||||
-- (tables/views/functions):
|
||||
--
|
||||
-- 1. "un-mime-typed" values, in most of the cases this is a composite/row
|
||||
-- value, for example for tables or views, but also often for functions.
|
||||
-- It can be simple integer values or text or bytea as well.
|
||||
--
|
||||
-- For this, we need handlers to transform the "non-mime-typed" values
|
||||
-- into "mimetypes". We have a default builtin handler that does
|
||||
-- "application/json". We can add more handlers via aggregates.
|
||||
--
|
||||
-- 2. "mime-typed" values, which specifically return a domain type that is
|
||||
-- associated to a certain mimetype. e.g, a function returning only
|
||||
-- "image/png".
|
||||
--
|
||||
-- FIXME:
|
||||
-- If the function returns a domain type - let's say image/png, we should
|
||||
-- accept */*, image/*, and image/png.
|
||||
-- Related issue: https://github.com/PostgREST/postgrest/issues/3391
|
||||
|
||||
-- | Do content negotiation. i.e. choose a media type based on the
|
||||
-- intersection of accepted/produced media types.
|
||||
negotiateContent :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] -> MediaHandlerMap -> Bool -> Either ApiRequestError ResolvedHandler
|
||||
negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} identifier accepts produces defaultSelect =
|
||||
case (act, firstAcceptedPick) of
|
||||
(_, Nothing) -> Left . MediaTypeError $ map MediaType.toMime accepts
|
||||
(ActDb (ActRelationMut _ _), Just (x, mt)) -> Right (if rep == Just Full then x else NoAgg, mt)
|
||||
-- no need for an aggregate on HEAD https://github.com/PostgREST/postgrest/issues/2849
|
||||
-- TODO: despite no aggregate, these are responding with a Content-Type, which is not correct.
|
||||
(ActDb (ActRelationRead _ True), Just (_, mt)) -> Right (NoAgg, mt)
|
||||
(ActDb (ActRoutine _ (InvRead True)), Just (_, mt)) -> Right (NoAgg, mt)
|
||||
(_, Just (x, mt)) -> Right (x, mt)
|
||||
where
|
||||
firstAcceptedPick = listToMaybe $ mapMaybe matchMT accepts -- If there are multiple accepted media types, pick the first. This is usual in content negotiation.
|
||||
matchMT mt = case mt of
|
||||
-- all the vendored media types have special handling as they have media type parameters, they cannot be overridden
|
||||
m@(MTVndSingularJSON strip) -> Just (BuiltinAggSingleJson strip, m)
|
||||
m@MTVndArrayJSONStrip -> Just (BuiltinAggArrayJsonStrip, m)
|
||||
m@(MTVndPlan (MTVndSingularJSON strip) _ _) -> mtPlanToNothing $ Just (BuiltinAggSingleJson strip, m)
|
||||
m@(MTVndPlan MTVndArrayJSONStrip _ _) -> mtPlanToNothing $ Just (BuiltinAggArrayJsonStrip, m)
|
||||
-- TODO the plan should have its own MediaHandler instead of relying on MediaType
|
||||
m@(MTVndPlan mType _ _) -> mtPlanToNothing $ ((,) . fst <$> lookupHandler mType) <*> pure m
|
||||
-- all the other media types can be overridden
|
||||
x -> lookupHandler x
|
||||
mtPlanToNothing x = if configDbPlanEnabled conf then x else Nothing -- don't find anything if the plan media type is not allowed
|
||||
lookupHandler mt =
|
||||
when' defaultSelect (HM.lookup (RelId identifier, MTAny) produces) <|> -- lookup for identifier and `*/*`
|
||||
when' defaultSelect (HM.lookup (RelId identifier, mt) produces) <|> -- lookup for identifier and a particular media type
|
||||
HM.lookup (RelAnyElement, mt) produces -- lookup for anyelement and a particular media type
|
||||
when' :: Bool -> Maybe a -> Maybe a
|
||||
when' True (Just a) = Just a
|
||||
when' _ _ = Nothing
|
||||
@@ -47,7 +47,7 @@ data CoercibleField = CoercibleField
|
||||
, cfBaseType :: Text -- ^ The base type of the field in case of domains, or just the type otherwise (without modifiers in case of pg_catalog types)
|
||||
, cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
|
||||
, cfDefault :: Maybe Text
|
||||
, cfFullRow :: Bool -- ^ True if the field represents the whole selected row. Used in spread rels: instead of COUNT(*), it does a COUNT(<row>) in order to not mix with other spreaded resources.
|
||||
, cfFullRow :: Bool -- ^ True if the field represents the whole selected row. Used in spread rels: instead of COUNT(*), it does a COUNT(<row>) in order to not mix with other spread resources.
|
||||
} deriving (Eq, Show)
|
||||
|
||||
unknownField :: FieldName -> JsonPath -> CoercibleField
|
||||
|
||||
@@ -43,16 +43,16 @@ data MainQuery = MainQuery
|
||||
|
||||
mainQuery :: ActionPlan -> AppConfig -> ApiRequest -> AuthResult -> Maybe QualifiedIdentifier -> MainQuery
|
||||
mainQuery (NoDb _) _ _ _ _ = MainQuery mempty Nothing mempty (mempty, mempty, mempty) mempty
|
||||
mainQuery (Db plan) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} authRes preReq =
|
||||
mainQuery (Db plan) conf@AppConfig{..} apiReq@ApiRequest{iTopLevelRange=range, iPreferences=Preferences{..}} authRes preReq =
|
||||
let genQ = MainQuery (PreQuery.txVarQuery plan conf authRes apiReq) (PreQuery.preReqQuery <$> preReq) in
|
||||
case plan of
|
||||
DbCrud _ WrappedReadPlan{..} ->
|
||||
let countQuery = QueryBuilder.readPlanToCountQuery wrReadPlan in
|
||||
genQ (Statements.mainRead wrReadPlan countQuery preferCount configDbMaxRows pMedia wrHandler) (mempty, mempty, mempty)
|
||||
genQ (Statements.mainRead wrReadPlan countQuery preferCount configDbMaxRows range pMedia wrHandler) (mempty, mempty, mempty)
|
||||
(if shouldExplainCount preferCount then Just (Statements.postExplain countQuery) else Nothing)
|
||||
DbCrud _ MutateReadPlan{..} ->
|
||||
genQ (Statements.mainWrite mrReadPlan mrMutatePlan pMedia mrHandler preferRepresentation preferResolution) (mempty, mempty, mempty) mempty
|
||||
DbCrud _ CallReadPlan{..} ->
|
||||
genQ (Statements.mainCall crProc crCallPlan crReadPlan preferCount pMedia crHandler) (mempty, mempty, mempty) mempty
|
||||
genQ (Statements.mainCall crProc crCallPlan crReadPlan preferCount configDbMaxRows range pMedia crHandler) (mempty, mempty, mempty) mempty
|
||||
MayUseDb InspectPlan{ipSchema=tSchema} ->
|
||||
genQ mempty (SqlFragment.accessibleTables tSchema, SqlFragment.accessibleFuncs tSchema, SqlFragment.schemaDescription tSchema) mempty
|
||||
|
||||
@@ -10,6 +10,7 @@ module PostgREST.Query.PreQuery
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Hasql.DynamicStatements.Snippet as SQL hiding (sql)
|
||||
@@ -46,7 +47,10 @@ txVarQuery dbActPlan AppConfig{..} AuthResult{..} ApiRequest{..} =
|
||||
pathSql = setConfigWithConstantName ("request.path", iPath)
|
||||
headersSql = setConfigWithConstantNameJSON "request.headers" iHeaders
|
||||
cookiesSql = setConfigWithConstantNameJSON "request.cookies" iCookies
|
||||
claimsSql = [setConfigWithConstantName ("request.jwt.claims", LBS.toStrict $ JSON.encode authClaims)]
|
||||
claimsSql = [setConfigWithConstantName ("request.jwt.claims", LBS.toStrict $ JSON.encode claims)]
|
||||
where
|
||||
claims = authClaims & KM.insert "role" (JSON.String $ decodeUtf8 authRole) -- insert "role" to claims as well
|
||||
|
||||
roleSql = [setConfigWithConstantName ("role", authRole)]
|
||||
roleSettingsSql = setConfigWithDynamicName <$> HM.toList (fromMaybe mempty $ HM.lookup authRole configRoleSettings)
|
||||
appSettingsSql = setConfigWithDynamicName . join bimap toUtf8 <$> configAppSettings
|
||||
|
||||
@@ -264,7 +264,7 @@ readPlanToCountQuery (Node ReadPlan{from=mainQi, fromAlias=tblAlias, where_=logi
|
||||
limitedQuery :: SQL.Snippet -> Maybe Integer -> SQL.Snippet
|
||||
limitedQuery query maxRows = query <> SQL.sql (maybe mempty (\x -> " LIMIT " <> BS.pack (show x)) maxRows)
|
||||
|
||||
-- TODO refactor so this function is uneeded and ComputedRelationship QualifiedIdentifier comes from the ReadPlan type
|
||||
-- TODO refactor so this function is unneeded and ComputedRelationship QualifiedIdentifier comes from the ReadPlan type
|
||||
getQualifiedIdentifier :: Maybe Relationship -> QualifiedIdentifier -> Maybe Alias -> QualifiedIdentifier
|
||||
getQualifiedIdentifier rel mainQi tblAlias = case rel of
|
||||
Just ComputedRelationship{relFunction} -> QualifiedIdentifier mempty $ fromMaybe (qiName relFunction) tblAlias
|
||||
|
||||
@@ -23,6 +23,7 @@ module PostgREST.Query.SqlFragment
|
||||
, locationF
|
||||
, noLocationF
|
||||
, orderF
|
||||
, pageCountSelectF
|
||||
, pgFmtColumn
|
||||
, pgFmtFilter
|
||||
, pgFmtIdent
|
||||
@@ -96,6 +97,7 @@ import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
||||
Routine (..),
|
||||
funcReturnsScalar,
|
||||
funcReturnsSetOfScalar,
|
||||
funcReturnsSingle,
|
||||
funcReturnsSingleComposite)
|
||||
|
||||
import Protolude hiding (Sum, cast)
|
||||
@@ -485,15 +487,21 @@ pgFmtGroup _ CoercibleSelectField{csAggFunction=Just _} = Nothing
|
||||
pgFmtGroup _ CoercibleSelectField{csAlias=Just alias, csAggFunction=Nothing} = Just $ pgFmtIdent alias
|
||||
pgFmtGroup qi CoercibleSelectField{csField=fld, csAlias=Nothing, csAggFunction=Nothing} = Just $ pgFmtField qi fld
|
||||
|
||||
countF :: SQL.Snippet -> Bool -> (SQL.Snippet, SQL.Snippet)
|
||||
countF countQuery shouldCount =
|
||||
if shouldCount
|
||||
then (
|
||||
", pgrst_source_count AS (" <> countQuery <> ")"
|
||||
, "(SELECT pg_catalog.count(*) FROM pgrst_source_count)" )
|
||||
else (
|
||||
mempty
|
||||
, "null::bigint")
|
||||
countF :: SQL.Snippet -> SQL.Snippet -> Bool -> Maybe Integer -> NonnegRange -> (SQL.Snippet, SQL.Snippet)
|
||||
countF countQuery pageCountSelect shouldCount maxRows range
|
||||
| shouldCount = if isJust maxRows || range /= allRange
|
||||
then ( ", pgrst_source_count AS (" <> countQuery <> ")"
|
||||
, "(SELECT pg_catalog.count(*) FROM pgrst_source_count)" )
|
||||
-- When there are no db-max-rows and limits/offsets, the total count will be the same as the page count,
|
||||
-- so we use the same page count here to avoid doing a separate aggregated count.
|
||||
else ( mempty, pageCountSelect )
|
||||
| otherwise = ( mempty, "null::bigint" )
|
||||
|
||||
pageCountSelectF :: Maybe Routine -> SQL.Snippet
|
||||
pageCountSelectF rout =
|
||||
if maybe False funcReturnsSingle rout
|
||||
then "1"
|
||||
else "pg_catalog.count(_postgrest_t)"
|
||||
|
||||
returningF :: QualifiedIdentifier -> [FieldName] -> SQL.Snippet
|
||||
returningF qi returnings =
|
||||
|
||||
@@ -20,8 +20,8 @@ import PostgREST.Plan.MutatePlan as MTPlan
|
||||
import PostgREST.Plan.ReadPlan
|
||||
import PostgREST.Query.QueryBuilder
|
||||
import PostgREST.Query.SqlFragment
|
||||
import PostgREST.SchemaCache.Routine (MediaHandler (..), Routine,
|
||||
funcReturnsSingle)
|
||||
import PostgREST.RangeQuery (NonnegRange)
|
||||
import PostgREST.SchemaCache.Routine (MediaHandler (..), Routine)
|
||||
|
||||
import Protolude
|
||||
|
||||
@@ -64,23 +64,24 @@ mainWrite rPlan mtplan mt handler rep resolution = mtSnippet mt snippet
|
||||
_ -> (False,False, mempty);
|
||||
|
||||
mainRead :: ReadPlanTree -> SQL.Snippet -> Maybe PreferCount -> Maybe Integer ->
|
||||
MediaType -> MediaHandler -> SQL.Snippet
|
||||
mainRead rPlan countQuery pCount maxRows mt handler = mtSnippet mt snippet
|
||||
NonnegRange -> MediaType -> MediaHandler -> SQL.Snippet
|
||||
mainRead rPlan countQuery pCount maxRows range mt handler = mtSnippet mt snippet
|
||||
where
|
||||
snippet =
|
||||
"WITH " <> sourceCTE <> " AS ( " <> selectQuery <> " ) " <>
|
||||
countCTEF <> " " <>
|
||||
"SELECT " <>
|
||||
countResultF <> " AS total_result_set, " <>
|
||||
"pg_catalog.count(_postgrest_t) AS page_total, " <>
|
||||
pageCountSelect <> " AS page_total, " <>
|
||||
handlerF Nothing handler <> " AS body, " <>
|
||||
responseHeadersF <> " AS response_headers, " <>
|
||||
responseStatusF <> " AS response_status, " <>
|
||||
"''" <> " AS response_inserted " <>
|
||||
"FROM ( SELECT * FROM " <> sourceCTE <> " ) _postgrest_t"
|
||||
|
||||
(countCTEF, countResultF) = countF countQ $ shouldCount pCount
|
||||
(countCTEF, countResultF) = countF countQ pageCountSelect (shouldCount pCount) maxRows range
|
||||
selectQuery = readPlanToQuery rPlan
|
||||
pageCountSelect = pageCountSelectF Nothing
|
||||
countQ =
|
||||
if pCount == Just EstimatedCount then
|
||||
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
|
||||
@@ -88,28 +89,27 @@ mainRead rPlan countQuery pCount maxRows mt handler = mtSnippet mt snippet
|
||||
else
|
||||
countQuery
|
||||
|
||||
mainCall :: Routine -> CallPlan -> ReadPlanTree -> Maybe PreferCount ->
|
||||
MediaType -> MediaHandler -> SQL.Snippet
|
||||
mainCall rout cPlan rPlan pCount mt handler = mtSnippet mt snippet
|
||||
mainCall :: Routine -> CallPlan -> ReadPlanTree -> Maybe PreferCount -> Maybe Integer ->
|
||||
NonnegRange-> MediaType -> MediaHandler -> SQL.Snippet
|
||||
mainCall rout cPlan rPlan pCount maxRows range mt handler = mtSnippet mt snippet
|
||||
where
|
||||
snippet =
|
||||
"WITH " <> sourceCTE <> " AS (" <> callProcQuery <> ") " <>
|
||||
countCTEF <>
|
||||
"SELECT " <>
|
||||
countResultF <> " AS total_result_set, " <>
|
||||
(if funcReturnsSingle rout
|
||||
then "1"
|
||||
else "pg_catalog.count(_postgrest_t)") <> " AS page_total, " <>
|
||||
pageCountSelect <> " AS page_total, " <>
|
||||
handlerF (Just rout) handler <> " AS body, " <>
|
||||
responseHeadersF <> " AS response_headers, " <>
|
||||
responseStatusF <> " AS response_status, " <>
|
||||
"''" <> " AS response_inserted " <>
|
||||
"FROM (" <> selectQuery <> ") _postgrest_t"
|
||||
|
||||
(countCTEF, countResultF) = countF countQuery $ shouldCount pCount
|
||||
(countCTEF, countResultF) = countF countQuery pageCountSelect (shouldCount pCount) maxRows range
|
||||
selectQuery = readPlanToQuery rPlan
|
||||
callProcQuery = callPlanToQuery cPlan
|
||||
countQuery = readPlanToCountQuery rPlan
|
||||
pageCountSelect = pageCountSelectF (Just rout)
|
||||
|
||||
-- This occurs after the main query runs, that's why it's prefixed with "post"
|
||||
postExplain :: SQL.Snippet -> SQL.Snippet
|
||||
|
||||
+80
-54
@@ -60,13 +60,14 @@ data PgrstResponse = PgrstResponse {
|
||||
, pgrstBody :: LBS.ByteString
|
||||
}
|
||||
|
||||
actionResponse :: DbResult -> ApiRequest -> (Text, Text) -> AppConfig -> SchemaCache -> Schema -> Bool -> Either Error.Error PgrstResponse
|
||||
actionResponse :: DbResult -> ApiRequest -> (Text, Text) -> AppConfig -> SchemaCache -> Either Error.Error PgrstResponse
|
||||
|
||||
actionResponse (DbCrudResult WrappedReadPlan{pMedia, wrHdrsOnly=headersOnly, crudQi=identifier} RSStandard{..}) ctxApiRequest@ApiRequest{iPreferences=Preferences{..},..} _ _ _ _ _ = do
|
||||
actionResponse (DbCrudResult plan@WrappedReadPlan{pMedia, wrHdrsOnly=headersOnly, crudQi=identifier} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ AppConfig{..} _ = do
|
||||
let
|
||||
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
|
||||
cLHeader = if headersOnly then mempty else [contentLengthHeaderStrict rsBody]
|
||||
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
|
||||
cLHeader = if headersOnly then mempty else [ contentLengthHeader bod ]
|
||||
prefHeader = maybeToList . prefAppliedHeader $ responsePreferences plan ctxApiRequest
|
||||
|
||||
headers =
|
||||
[ contentRange
|
||||
, ( "Content-Location"
|
||||
@@ -78,7 +79,7 @@ actionResponse (DbCrudResult WrappedReadPlan{pMedia, wrHdrsOnly=headersOnly, cru
|
||||
++ cLHeader
|
||||
++ contentTypeHeaders pMedia ctxApiRequest
|
||||
++ prefHeader
|
||||
bod | status == HTTP.status416 = Error.errorPayload $ Error.ApiRequestError $ Error.InvalidRange $
|
||||
bod | status == HTTP.status416 = Error.errorPayload configClientErrorVerbosity $ Error.ApiRequestErr $ Error.InvalidRange $
|
||||
Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
|
||||
| headersOnly = mempty
|
||||
| otherwise = LBS.fromStrict rsBody
|
||||
@@ -87,12 +88,10 @@ actionResponse (DbCrudResult WrappedReadPlan{pMedia, wrHdrsOnly=headersOnly, cru
|
||||
|
||||
Right $ PgrstResponse ovStatus ovHeaders bod
|
||||
|
||||
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutatePlan, pMedia, crudQi=QualifiedIdentifier{..}} RSStandard{..}) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}, ..} _ _ _ _ _ = do
|
||||
actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationCreate, pMedia, crudQi=QualifiedIdentifier{..}} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ = do
|
||||
let
|
||||
pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols; _ -> mempty;}
|
||||
prefHeader = prefAppliedHeader $
|
||||
Preferences (if null pkCols && isNothing (qsOnConflict iQueryParams) then Nothing else preferResolution)
|
||||
preferRepresentation preferCount preferTransaction preferMissing preferHandling preferTimezone Nothing []
|
||||
prefHeader = prefAppliedHeader $ responsePreferences plan ctxApiRequest
|
||||
|
||||
headers =
|
||||
catMaybes
|
||||
[ if null rsLocation then
|
||||
@@ -105,37 +104,39 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutateP
|
||||
<> HTTP.renderSimpleQuery True rsLocation
|
||||
)
|
||||
, Just . RangeQuery.contentRangeH 1 0 $
|
||||
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
||||
, Just $ contentLengthHeaderStrict rsBody
|
||||
if shouldCount (preferCount iPreferences) then Just rsQueryTotal else Nothing
|
||||
, prefHeader ]
|
||||
|
||||
isInsertIfGTZero i =
|
||||
if i <= 0 && preferResolution == Just MergeDuplicates then
|
||||
if i <= 0 && preferResolution iPreferences == Just MergeDuplicates then
|
||||
HTTP.status200
|
||||
else
|
||||
HTTP.status201
|
||||
status = maybe HTTP.status200 isInsertIfGTZero rsInserted
|
||||
(headers', bod) = case preferRepresentation of
|
||||
(headers', bod) = case preferRepresentation iPreferences of
|
||||
Just Full -> (headers ++ contentTypeHeaders pMedia ctxApiRequest, LBS.fromStrict rsBody)
|
||||
Just None -> (headers, mempty)
|
||||
Just HeadersOnly -> (headers, mempty)
|
||||
Nothing -> (headers, mempty)
|
||||
|
||||
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers'
|
||||
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status $ contentLengthHeader bod:headers'
|
||||
|
||||
Right $ PgrstResponse ovStatus ovHeaders bod
|
||||
|
||||
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, pMedia} RSStandard{..}) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = do
|
||||
actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationUpdate, pMedia} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ = do
|
||||
let
|
||||
contentRangeHeader =
|
||||
Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $
|
||||
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
||||
prefHeader = prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction preferMissing preferHandling preferTimezone preferMaxAffected []
|
||||
if shouldCount (preferCount iPreferences) then Just rsQueryTotal else Nothing
|
||||
|
||||
prefHeader = prefAppliedHeader $ responsePreferences plan ctxApiRequest
|
||||
|
||||
headers = catMaybes [contentRangeHeader, prefHeader]
|
||||
lbsBody = LBS.fromStrict rsBody
|
||||
|
||||
let (status, headers', body) =
|
||||
case preferRepresentation of
|
||||
Just Full -> (HTTP.status200, headers ++ [contentLengthHeaderStrict rsBody] ++ contentTypeHeaders pMedia ctxApiRequest, LBS.fromStrict rsBody)
|
||||
case preferRepresentation iPreferences of
|
||||
Just Full -> (HTTP.status200, headers ++ [contentLengthHeader lbsBody] ++ contentTypeHeaders pMedia ctxApiRequest, lbsBody)
|
||||
Just None -> (HTTP.status204, headers, mempty)
|
||||
_ -> (HTTP.status204, headers, mempty)
|
||||
|
||||
@@ -143,31 +144,33 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, pMedia} R
|
||||
|
||||
Right $ PgrstResponse ovStatus ovHeaders body
|
||||
|
||||
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationSingleUpsert, pMedia} RSStandard{..}) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = do
|
||||
actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationSingleUpsert, pMedia} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ = do
|
||||
let
|
||||
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
|
||||
cLHeader = [contentLengthHeaderStrict rsBody]
|
||||
prefHeader = maybeToList . prefAppliedHeader $ responsePreferences plan ctxApiRequest
|
||||
lbsBody = LBS.fromStrict rsBody
|
||||
cLHeader = [contentLengthHeader lbsBody]
|
||||
cTHeader = contentTypeHeaders pMedia ctxApiRequest
|
||||
|
||||
let isInsertIfGTZero i = if i > 0 then HTTP.status201 else HTTP.status200
|
||||
upsertStatus = isInsertIfGTZero $ fromJust rsInserted
|
||||
(status, headers, body) =
|
||||
case preferRepresentation of
|
||||
Just Full -> (upsertStatus, cLHeader ++ cTHeader ++ prefHeader, LBS.fromStrict rsBody)
|
||||
case preferRepresentation iPreferences of
|
||||
Just Full -> (upsertStatus, cLHeader ++ cTHeader ++ prefHeader, lbsBody)
|
||||
Just None -> (HTTP.status204, prefHeader, mempty)
|
||||
_ -> (HTTP.status204, prefHeader, mempty)
|
||||
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers
|
||||
|
||||
Right $ PgrstResponse ovStatus ovHeaders body
|
||||
|
||||
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, pMedia} RSStandard{..}) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = do
|
||||
actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationDelete, pMedia} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ = do
|
||||
let
|
||||
contentRangeHeader = RangeQuery.contentRangeH 1 0 $ if shouldCount preferCount then Just rsQueryTotal else Nothing
|
||||
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
|
||||
contentRangeHeader = RangeQuery.contentRangeH 1 0 $ if shouldCount (preferCount iPreferences) then Just rsQueryTotal else Nothing
|
||||
prefHeader = maybeToList . prefAppliedHeader $ responsePreferences plan ctxApiRequest
|
||||
headers = contentRangeHeader : prefHeader
|
||||
lbsBody = LBS.fromStrict rsBody
|
||||
(status, headers', body) =
|
||||
case preferRepresentation of
|
||||
Just Full -> (HTTP.status200, headers ++ [contentLengthHeaderStrict rsBody] ++ contentTypeHeaders pMedia ctxApiRequest, LBS.fromStrict rsBody)
|
||||
case preferRepresentation iPreferences of
|
||||
Just Full -> (HTTP.status200, headers ++ [contentLengthHeader lbsBody] ++ contentTypeHeaders pMedia ctxApiRequest, lbsBody)
|
||||
Just None -> (HTTP.status204, headers, mempty)
|
||||
_ -> (HTTP.status204, headers, mempty)
|
||||
|
||||
@@ -175,17 +178,17 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, pMedia} R
|
||||
|
||||
Right $ PgrstResponse ovStatus ovHeaders body
|
||||
|
||||
actionResponse (DbCrudResult CallReadPlan{pMedia, crInvMthd=invMethod, crProc=proc} RSStandard {..}) ctxApiRequest@ApiRequest{iPreferences=Preferences{..},..} _ _ _ _ _ = do
|
||||
actionResponse (DbCrudResult plan@CallReadPlan{pMedia, crInvMthd=invMethod, crProc=proc} RSStandard {..}) ctxApiRequest@ApiRequest{..} _ AppConfig{..} _ = do
|
||||
let
|
||||
(status, contentRange) =
|
||||
RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
|
||||
rsOrErrBody = if status == HTTP.status416
|
||||
then Error.errorPayload $ Error.ApiRequestError $ Error.InvalidRange
|
||||
then Error.errorPayload configClientErrorVerbosity $ Error.ApiRequestErr $ Error.InvalidRange
|
||||
$ Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
|
||||
else LBS.fromStrict rsBody
|
||||
isHeadMethod = invMethod == InvRead True
|
||||
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
|
||||
cLHeader = if isHeadMethod then mempty else [contentLengthHeaderLazy rsOrErrBody]
|
||||
prefHeader = maybeToList . prefAppliedHeader $ responsePreferences plan ctxApiRequest
|
||||
cLHeader = if isHeadMethod then mempty else [contentLengthHeader rsOrErrBody]
|
||||
headers = contentRange : prefHeader
|
||||
(status', headers', body) =
|
||||
if Routine.funcReturnsVoid proc then
|
||||
@@ -199,20 +202,21 @@ actionResponse (DbCrudResult CallReadPlan{pMedia, crInvMthd=invMethod, crProc=pr
|
||||
|
||||
Right $ PgrstResponse ovStatus ovHeaders body
|
||||
|
||||
actionResponse (DbPlanResult media plan) ctxApiRequest _ _ _ _ _ =
|
||||
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders media ctxApiRequest) $ LBS.fromStrict plan
|
||||
actionResponse (DbPlanResult media plan) ctxApiRequest _ _ _ =
|
||||
let body = LBS.fromStrict plan in
|
||||
Right $ PgrstResponse HTTP.status200 (contentLengthHeader body : contentTypeHeaders media ctxApiRequest) body
|
||||
|
||||
actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) _ versions conf sCache schema negotiatedByProfile =
|
||||
actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) ApiRequest{..} versions conf sCache =
|
||||
let
|
||||
rsBody = maybe mempty (\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) body
|
||||
cLHeader = if headersOnly then mempty else [contentLengthHeaderLazy rsBody]
|
||||
cLHeader = if headersOnly then mempty else [contentLengthHeader rsBody]
|
||||
in
|
||||
Right $ PgrstResponse HTTP.status200 (MediaType.toContentType MTOpenAPI : cLHeader ++ maybeToList (profileHeader schema negotiatedByProfile)) rsBody
|
||||
Right $ PgrstResponse HTTP.status200 (MediaType.toContentType MTOpenAPI : cLHeader ++ maybeToList (profileHeader iSchema iNegotiatedByProfile)) rsBody
|
||||
|
||||
actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ SchemaCache{dbTables} _ _ =
|
||||
actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ sc@SchemaCache{dbTables} =
|
||||
case HM.lookup qi dbTables of
|
||||
Just tbl -> respondInfo $ allowH tbl
|
||||
Nothing -> Left $ Error.SchemaCacheErr $ Error.TableNotFound qiSchema qiName (HM.elems dbTables)
|
||||
Nothing -> Left $ Error.SchemaCacheErr $ Error.TableNotFound qiSchema qiName sc
|
||||
where
|
||||
allowH table =
|
||||
let hasPK = not . null $ tablePKCols table in
|
||||
@@ -223,16 +227,16 @@ actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ Schem
|
||||
["PATCH" | tableUpdatable table] ++
|
||||
["DELETE" | tableDeletable table]
|
||||
|
||||
actionResponse (NoDbResult (RoutineInfoPlan proc)) _ _ _ _ _ _
|
||||
actionResponse (NoDbResult (RoutineInfoPlan proc)) _ _ _ _
|
||||
| pdVolatility proc == Volatile = respondInfo "OPTIONS,POST"
|
||||
| otherwise = respondInfo "OPTIONS,GET,HEAD,POST"
|
||||
|
||||
actionResponse (NoDbResult SchemaInfoPlan) _ _ _ _ _ _ = respondInfo "OPTIONS,GET,HEAD"
|
||||
actionResponse (NoDbResult SchemaInfoPlan) _ _ _ _ = respondInfo "OPTIONS,GET,HEAD"
|
||||
|
||||
respondInfo :: ByteString -> Either Error.Error PgrstResponse
|
||||
respondInfo allowHeader =
|
||||
let allOrigins = ("Access-Control-Allow-Origin", "*") in
|
||||
Right $ PgrstResponse HTTP.status200 [contentLengthHeaderStrict mempty, allOrigins, (HTTP.hAllow, allowHeader)] mempty
|
||||
Right $ PgrstResponse HTTP.status200 [contentLengthHeader mempty, allOrigins, (HTTP.hAllow, allowHeader)] mempty
|
||||
|
||||
-- Status and headers can be overridden as per https://postgrest.org/en/stable/references/transactions.html#response-headers
|
||||
overrideStatusHeaders :: Maybe Text -> Maybe BS.ByteString -> HTTP.Status -> [HTTP.Header]-> Either Error.Error (HTTP.Status, [HTTP.Header])
|
||||
@@ -243,20 +247,14 @@ overrideStatusHeaders rsGucStatus rsGucHeaders pgrstStatus pgrstHeaders = do
|
||||
|
||||
decodeGucHeaders :: Maybe BS.ByteString -> Either Error.Error [GucHeader]
|
||||
decodeGucHeaders =
|
||||
maybe (Right []) $ first (const . Error.ApiRequestError $ Error.GucHeadersError) . JSON.eitherDecode . LBS.fromStrict
|
||||
maybe (Right []) $ first (const . Error.ApiRequestErr $ Error.GucHeadersError) . JSON.eitherDecode . LBS.fromStrict
|
||||
|
||||
decodeGucStatus :: Maybe Text -> Either Error.Error (Maybe HTTP.Status)
|
||||
decodeGucStatus =
|
||||
maybe (Right Nothing) $ first (const . Error.ApiRequestError $ Error.GucStatusError) . fmap (Just . toEnum . fst) . decimal
|
||||
maybe (Right Nothing) $ first (const . Error.ApiRequestErr $ Error.GucStatusError) . fmap (Just . toEnum . fst) . decimal
|
||||
|
||||
contentLengthHeader :: Show b => (a -> b) -> a -> HTTP.Header
|
||||
contentLengthHeader lenFn body = ("Content-Length", show (lenFn body))
|
||||
|
||||
contentLengthHeaderStrict :: BS.ByteString -> HTTP.Header
|
||||
contentLengthHeaderStrict = contentLengthHeader BS.length
|
||||
|
||||
contentLengthHeaderLazy :: LBS.ByteString -> HTTP.Header
|
||||
contentLengthHeaderLazy = contentLengthHeader LBS.length
|
||||
contentLengthHeader :: LBS.ByteString -> HTTP.Header
|
||||
contentLengthHeader body = ("Content-Length", show (LBS.length body))
|
||||
|
||||
contentTypeHeaders :: MediaType -> ApiRequest -> [HTTP.Header]
|
||||
contentTypeHeaders mediaType ApiRequest{..} =
|
||||
@@ -274,3 +272,31 @@ addHeadersIfNotIncluded :: [HTTP.Header] -> [HTTP.Header] -> [HTTP.Header]
|
||||
addHeadersIfNotIncluded newHeaders initialHeaders =
|
||||
filter (\(nk, _) -> isNothing $ find (\(ik, _) -> ik == nk) initialHeaders) newHeaders ++
|
||||
initialHeaders
|
||||
|
||||
-- | Get Preferences for Preference-Applied header per plan
|
||||
responsePreferences :: CrudPlan -> ApiRequest -> Preferences
|
||||
responsePreferences plan ApiRequest{iPreferences=Preferences{..}, iQueryParams=QueryParams{..}} =
|
||||
let
|
||||
-- Only returned on Inserts
|
||||
preferResolution' = case plan of
|
||||
MutateReadPlan{mrMutation=MutationCreate, mrMutatePlan} ->
|
||||
let pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols ; _ -> mempty; }
|
||||
in (if null pkCols && isNothing qsOnConflict then Nothing else preferResolution)
|
||||
_ -> Nothing
|
||||
|
||||
preferRepresentation' = case plan of
|
||||
MutateReadPlan{} -> preferRepresentation
|
||||
_ -> Nothing
|
||||
|
||||
preferMissing' = case plan of
|
||||
MutateReadPlan{mrMutation=MutationCreate} -> preferMissing
|
||||
MutateReadPlan{mrMutation=MutationUpdate} -> preferMissing
|
||||
_ -> Nothing
|
||||
|
||||
preferMaxAffected' = case plan of
|
||||
MutateReadPlan{mrMutation=MutationUpdate} -> preferMaxAffected
|
||||
MutateReadPlan{mrMutation=MutationDelete} -> preferMaxAffected
|
||||
CallReadPlan{} -> preferMaxAffected
|
||||
_ -> Nothing
|
||||
|
||||
in Preferences preferResolution' preferRepresentation' preferCount preferTransaction preferMissing' preferHandling preferTimezone preferMaxAffected' []
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user