mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
551
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0ec28f02a | ||
|
|
18f3280bad | ||
|
|
bae328efa1 | ||
|
|
03575e68c4 | ||
|
|
2762e84974 | ||
|
|
7b2f6ac692 | ||
|
|
542aac7fda | ||
|
|
7430de293e | ||
|
|
8ffe48d948 | ||
|
|
4050b9ded1 | ||
|
|
2f2a124a06 | ||
|
|
6088fd55c8 | ||
|
|
1aeee8c407 | ||
|
|
36486ccc93 | ||
|
|
794d8eac5b | ||
|
|
32ebfc6cb8 | ||
|
|
e1b8f5bf98 | ||
|
|
4d86e67006 | ||
|
|
b3e3b2a330 | ||
|
|
e010bf25d5 | ||
|
|
aaf91f3816 | ||
|
|
bae61e8a05 | ||
|
|
dfbf45f4cc | ||
|
|
c264db6146 | ||
|
|
cccafeff0c | ||
|
|
360d08f087 | ||
|
|
d8e97ae47d | ||
|
|
d13e6d75f0 | ||
|
|
c2a249c48b | ||
|
|
4447396859 | ||
|
|
4d1f229ec7 | ||
|
|
4f733f5984 | ||
|
|
701f557755 | ||
|
|
8ab26aab44 | ||
|
|
ec3aa2b54f | ||
|
|
2d636ff7ec | ||
|
|
fd42b8fa64 | ||
|
|
9d7df34590 | ||
|
|
b2b9ac5b4d | ||
|
|
e75fc5b859 | ||
|
|
57b8305144 | ||
|
|
a429fb5860 | ||
|
|
aca11ed073 | ||
|
|
86123c9e93 | ||
|
|
a58ea1123b | ||
|
|
18a67e00d3 | ||
|
|
e634145612 | ||
|
|
ce15eeb722 | ||
|
|
dae9cbcf45 | ||
|
|
b15457bcca | ||
|
|
fce2abfd4c | ||
|
|
da5382d58a | ||
|
|
380cc3eb27 | ||
|
|
b29f9f4a45 | ||
|
|
05f2d2b9cf | ||
|
|
c1f61b22aa | ||
|
|
980762fc3e | ||
|
|
c96836b1c0 | ||
|
|
15917ac7ba | ||
|
|
cd851bff24 | ||
|
|
3f5dcc81fd | ||
|
|
f5a732d231 | ||
|
|
72dfd8a7bc | ||
|
|
11f80fc2ed | ||
|
|
547709a1d8 | ||
|
|
ccb0ddd578 | ||
|
|
b2ce95a0cd | ||
|
|
ed9b47f6ce | ||
|
|
2a357511ea | ||
|
|
67d24d3ef1 | ||
|
|
87ed7c3817 | ||
|
|
8f176166c9 | ||
|
|
29a0d6c2bf | ||
|
|
a0ef145b92 | ||
|
|
3d3f5934c6 | ||
|
|
9e1f8af103 | ||
|
|
b055ff1066 | ||
|
|
f0cbfbbbe4 | ||
|
|
cb4cff120b | ||
|
|
49e6a38e7f | ||
|
|
752df21007 | ||
|
|
dfa3ae5274 | ||
|
|
543765a22a | ||
|
|
e2bb9a9161 | ||
|
|
bda823ac0e | ||
|
|
3dd5a51ba4 | ||
|
|
5afc8f462d | ||
|
|
ed0c7b4e12 | ||
|
|
d25707346f | ||
|
|
a3f6cf4645 | ||
|
|
9a175c5098 | ||
|
|
5465be6301 | ||
|
|
46a457bfa3 | ||
|
|
4c989f99c3 | ||
|
|
8f1d73a6cb | ||
|
|
01b5fa8ecb | ||
|
|
d73b7a70d8 | ||
|
|
f71a2dfddc | ||
|
|
cefd063c55 | ||
|
|
c924cbcc59 | ||
|
|
81321e82d1 | ||
|
|
30a511692b | ||
|
|
9637409831 | ||
|
|
c4f6b11706 | ||
|
|
ad4178aa42 | ||
|
|
92b5f0dcd6 | ||
|
|
77982f4b90 | ||
|
|
784babcf45 | ||
|
|
1f16b0d9e7 | ||
|
|
015aee087a | ||
|
|
50d86c5b5d | ||
|
|
3d43530225 | ||
|
|
d0d536a2dd | ||
|
|
4b620bb31a | ||
|
|
ca7f36a9b8 | ||
|
|
ec3c7f1ad6 | ||
|
|
ecba11eba5 | ||
|
|
7a96b024b3 | ||
|
|
4ba413fcc0 | ||
|
|
af95e71244 | ||
|
|
ee1eea5c0c | ||
|
|
b28b851bb2 | ||
|
|
2f2da98cca | ||
|
|
54997638e8 | ||
|
|
70b9c60e2c | ||
|
|
49d2db65e6 | ||
|
|
1fdcca9592 | ||
|
|
cb8640b56f | ||
|
|
a781325679 | ||
|
|
3f12062259 | ||
|
|
2e57500d7e | ||
|
|
a1a88ae57e | ||
|
|
9553f8a9f8 | ||
|
|
15f94d99ca | ||
|
|
ca105ff9bc | ||
|
|
7e3114d733 | ||
|
|
ef4b9c4934 | ||
|
|
2e960b90df | ||
|
|
1928605bd9 | ||
|
|
340ce9d56b | ||
|
|
e6d5c216eb | ||
|
|
d25ccb784d | ||
|
|
8ce7aa2abe | ||
|
|
78715e4a1a | ||
|
|
df57ee2a32 | ||
|
|
6d4a66fbb3 | ||
|
|
3aee54747c | ||
|
|
d2423a5bb5 | ||
|
|
c0c053ff20 | ||
|
|
fb0d43daf3 | ||
|
|
e4bfb38a57 | ||
|
|
0a7b7de197 | ||
|
|
51c0767be2 | ||
|
|
d25dac7d60 | ||
|
|
1f18e16310 | ||
|
|
d0f6daa67a | ||
|
|
184399023c | ||
|
|
a588a62277 | ||
|
|
ffd0dbd406 | ||
|
|
8200d079c6 | ||
|
|
d9d87f813f | ||
|
|
db7acd4b8b | ||
|
|
a62ca8612d | ||
|
|
975da667f5 | ||
|
|
6cfb12c48b | ||
|
|
8cdcd379c8 | ||
|
|
1b8123bf62 | ||
|
|
1ca75c73df | ||
|
|
fe08978851 | ||
|
|
0633662695 | ||
|
|
89819db775 | ||
|
|
faf09ed7c5 | ||
|
|
c097d9fcdd | ||
|
|
dac184b281 | ||
|
|
335b1c2f6c | ||
|
|
5fbe6cba79 | ||
|
|
36435d940d | ||
|
|
93f7a0d902 | ||
|
|
7fbfb6266b | ||
|
|
29b700d1ec | ||
|
|
e3231393b9 | ||
|
|
5beeca5a99 | ||
|
|
4de3009f67 | ||
|
|
4b3963512c | ||
|
|
8c634a95e4 | ||
|
|
7a3c001452 | ||
|
|
a48fd13ced | ||
|
|
9c6ed9565e | ||
|
|
bdbc5b92df | ||
|
|
c246fb0f83 | ||
|
|
a0a3849b16 | ||
|
|
b99139661e | ||
|
|
6d86b8fd0d | ||
|
|
cef7a5197f | ||
|
|
c1dfeae323 | ||
|
|
51fa0770a6 | ||
|
|
d46403507c | ||
|
|
83724ce96f | ||
|
|
34930e6d67 | ||
|
|
096f8041fd | ||
|
|
9dad7cef9e | ||
|
|
9e23867f0a | ||
|
|
48353faa9b | ||
|
|
c36b8fbabf | ||
|
|
5a5cb1e74e | ||
|
|
a8e6b1b683 | ||
|
|
b0939b92c3 | ||
|
|
3d66078fe9 | ||
|
|
9a6a597710 | ||
|
|
c15e8d0126 | ||
|
|
7b25d778d4 | ||
|
|
3283b0bec0 | ||
|
|
49f8d6e89c | ||
|
|
a0274a7b6f | ||
|
|
277a09847e | ||
|
|
cdc5654c26 | ||
|
|
bb4fdefee7 | ||
|
|
c2e5bbe0f7 | ||
|
|
b2d24080b9 | ||
|
|
b006858d0c | ||
|
|
a0f30f608d | ||
|
|
a85c5e582c | ||
|
|
6d1def7716 | ||
|
|
f0f1e1c412 | ||
|
|
11f85e2477 | ||
|
|
dbb1d1139e | ||
|
|
f0e2a8b2c4 | ||
|
|
46e778ea90 | ||
|
|
fd9fe5d9ce | ||
|
|
59e0383264 | ||
|
|
0f81bc2da9 | ||
|
|
17e3d03b40 | ||
|
|
c97435d95c | ||
|
|
b2ad7237ab | ||
|
|
57d777f80a | ||
|
|
d1962dbc62 | ||
|
|
6648a1987b | ||
|
|
c60e85ca84 | ||
|
|
ca1cf1982f | ||
|
|
f50ca0281b | ||
|
|
2f12717f30 | ||
|
|
f934259c31 | ||
|
|
838dbc52e3 | ||
|
|
e7d9ef74c4 | ||
|
|
d106e1d5bb | ||
|
|
d1ade3872e | ||
|
|
0f64f54221 | ||
|
|
00512df4ca | ||
|
|
9065d9ada4 | ||
|
|
e0fec8f550 | ||
|
|
205c283935 | ||
|
|
038be90969 | ||
|
|
355d4323fc | ||
|
|
7578b5644c | ||
|
|
240cdf43e5 | ||
|
|
481eaf7530 | ||
|
|
2390d40731 | ||
|
|
f75a7269c9 | ||
|
|
4f74394e8f | ||
|
|
f770277ea5 | ||
|
|
eeadba3b59 | ||
|
|
2e42633e12 | ||
|
|
affd5876ab | ||
|
|
dfd39d3946 | ||
|
|
7d7fcea8c0 | ||
|
|
33d3f82a58 | ||
|
|
af531ee4f9 | ||
|
|
73a77b8dcc | ||
|
|
4b1b2972e9 | ||
|
|
72bb6378c2 | ||
|
|
bfa1960d8a | ||
|
|
e530bf41f7 | ||
|
|
0d27c59cb8 | ||
|
|
97e611029e | ||
|
|
efc37c68ec | ||
|
|
b3f305f9a8 | ||
|
|
32508e7251 | ||
|
|
a98b9f2319 | ||
|
|
a0ef5968fb | ||
|
|
bc637ad25f | ||
|
|
5b6dd5f75a | ||
|
|
2c973387c5 | ||
|
|
3b9c604eb8 | ||
|
|
8a841a6971 | ||
|
|
c973ca0a5d | ||
|
|
7c486ddcef | ||
|
|
cb7f2ec693 | ||
|
|
3cc7b833db | ||
|
|
9ca84a56c9 | ||
|
|
af05ad5784 | ||
|
|
39af9c544d | ||
|
|
10b6955e98 | ||
|
|
7e963e8439 | ||
|
|
d9d5a81337 | ||
|
|
193891a7c9 | ||
|
|
70cb5ca68a | ||
|
|
c8a1746119 | ||
|
|
06eb550e98 | ||
|
|
6c89a05a60 | ||
|
|
1b993ede0f | ||
|
|
648ffcdcfa | ||
|
|
0be94a7aff | ||
|
|
eba145ee2d | ||
|
|
99d230c70d | ||
|
|
4ac1b68bbc | ||
|
|
211ac32080 | ||
|
|
30bf9deae0 | ||
|
|
196b04a39c | ||
|
|
6adabae620 | ||
|
|
5f9dc0fcd8 | ||
|
|
b9d01617a4 | ||
|
|
9c65609d73 | ||
|
|
da4dcd8505 | ||
|
|
d0f853d4cd | ||
|
|
63b2a1fb7f | ||
|
|
30cfbb2d99 | ||
|
|
2f272f0480 | ||
|
|
4317ee5617 | ||
|
|
ef08d5a04c | ||
|
|
566eebfaec | ||
|
|
51bc3dc1d5 | ||
|
|
83f182b444 | ||
|
|
dcd01a8fb0 | ||
|
|
6aeb2d2be0 | ||
|
|
2cfe40e98e | ||
|
|
15e45df81e | ||
|
|
e4f74a3fb1 | ||
|
|
774a0d7022 | ||
|
|
1dc9217c65 | ||
|
|
50bd2877f4 | ||
|
|
f465e2267a | ||
|
|
b4f1652286 | ||
|
|
4e4860e7f8 | ||
|
|
cce04b3bd1 | ||
|
|
7568964b7c | ||
|
|
82af29c0cf | ||
|
|
c6672e93cf | ||
|
|
ac905a80b5 | ||
|
|
5df65d5417 | ||
|
|
abd6f0add2 | ||
|
|
25854c84f5 | ||
|
|
5724d19426 | ||
|
|
7c84337c5f | ||
|
|
b1d37bc4be | ||
|
|
4808cad081 | ||
|
|
72cd7671c4 | ||
|
|
c3e411fcaa | ||
|
|
8c31e9f206 | ||
|
|
a4ad1b6df3 | ||
|
|
9e94f15f02 | ||
|
|
e27719108b | ||
|
|
8bf2f54a51 | ||
|
|
8c03db70e9 | ||
|
|
8ea69e0321 | ||
|
|
dfec2b3cb7 | ||
|
|
69d778ab05 | ||
|
|
848f09956e | ||
|
|
51f50b161c | ||
|
|
53d3713bfe | ||
|
|
b8385fe927 | ||
|
|
52e3319a86 | ||
|
|
8b41aed919 | ||
|
|
b2233193d5 | ||
|
|
ba4430801d | ||
|
|
1c1ec43150 | ||
|
|
2f62cced0a | ||
|
|
afb8f70316 | ||
|
|
78eeac6d14 | ||
|
|
9f65177649 | ||
|
|
e9ea39a822 | ||
|
|
2097b0a0b0 | ||
|
|
78ef52cc9e | ||
|
|
f040a4dc31 | ||
|
|
56385ce167 | ||
|
|
bc592903db | ||
|
|
06a2d48deb | ||
|
|
c4d9c3dc4f | ||
|
|
b36f1edeb0 | ||
|
|
eef443f07e | ||
|
|
62f7f25cb5 | ||
|
|
4b94d14f1e | ||
|
|
12f76d74b1 | ||
|
|
9f504cd065 | ||
|
|
9dc27e9ca5 | ||
|
|
2bff4192ee | ||
|
|
a9fb17b067 | ||
|
|
db3d00b1da | ||
|
|
5ab42b8d32 | ||
|
|
8de7b41929 | ||
|
|
a5c0a920ba | ||
|
|
37a2b1cbd9 | ||
|
|
6b66923f18 | ||
|
|
adb4e4ef36 | ||
|
|
56165ee3d9 | ||
|
|
d86258a6f6 | ||
|
|
d2230f8fe7 | ||
|
|
69d53cb2d4 | ||
|
|
447375dcbf | ||
|
|
afab349a77 | ||
|
|
2a95cfd5a3 | ||
|
|
8a1f8c71b2 | ||
|
|
2793bcac38 | ||
|
|
13f48797d4 | ||
|
|
0a71efc244 | ||
|
|
fcc7a56f4a | ||
|
|
0aab3e9a43 | ||
|
|
be5eeb1aba | ||
|
|
08b43dce50 | ||
|
|
ad3bdee6c1 | ||
|
|
e00efaea68 | ||
|
|
be214f7968 | ||
|
|
64d182ac6a | ||
|
|
bb38bf4248 | ||
|
|
956285d522 | ||
|
|
dc704fdb51 | ||
|
|
e388d2f6a3 | ||
|
|
fc62ef2fcc | ||
|
|
b01ec03723 | ||
|
|
489b2c4e21 | ||
|
|
1a46fc2006 | ||
|
|
c1b7aff2d9 | ||
|
|
7df1814203 | ||
|
|
8f39783d7a | ||
|
|
4043153df3 | ||
|
|
9765c7f0e3 | ||
|
|
eddd159af8 | ||
|
|
17de886296 | ||
|
|
27b0e48296 | ||
|
|
6b2054d0b5 | ||
|
|
df0f7ba581 | ||
|
|
c484c487a9 | ||
|
|
47f3ab930b | ||
|
|
c8c3c68b0e | ||
|
|
facae65f08 | ||
|
|
ef6711e317 | ||
|
|
e06b06a21b | ||
|
|
c97dd78335 | ||
|
|
bf9bb22ac5 | ||
|
|
1b826df8f9 | ||
|
|
11f23fdcfa | ||
|
|
b5ec92964c | ||
|
|
71b74e21a3 | ||
|
|
1e797b3f01 | ||
|
|
f1b2dd7335 | ||
|
|
50c379c5c3 | ||
|
|
463e5acf50 | ||
|
|
807e8bec8b | ||
|
|
f167049b51 | ||
|
|
93b9500a95 | ||
|
|
935c1e40eb | ||
|
|
e472d98fe3 | ||
|
|
7fe65eec57 | ||
|
|
4cf375de6d | ||
|
|
19d0eb66c0 | ||
|
|
05d70f8826 | ||
|
|
98ec9e45c4 | ||
|
|
6dc17c91bc | ||
|
|
1493b7b8a6 | ||
|
|
965836c8b4 | ||
|
|
4545989c91 | ||
|
|
b71d42d226 | ||
|
|
945cdb3a99 | ||
|
|
a0db023860 | ||
|
|
6a7280e7dd | ||
|
|
680cb04de7 | ||
|
|
6e50ed050d | ||
|
|
f78b5c6596 | ||
|
|
55001a7daa | ||
|
|
6d2876ab26 | ||
|
|
b510194f00 | ||
|
|
8fd1c5d620 | ||
|
|
590f063694 | ||
|
|
90e1433878 | ||
|
|
2b42487198 | ||
|
|
53ed9462bd | ||
|
|
0da185e689 | ||
|
|
dc39af50a1 | ||
|
|
6da132db46 | ||
|
|
b2a02e3807 | ||
|
|
e7f78b3e01 | ||
|
|
682a56c03c | ||
|
|
7376c5b517 | ||
|
|
730c62bda6 | ||
|
|
db84c79802 | ||
|
|
6ba810a4da | ||
|
|
6d9b787816 | ||
|
|
7b8fb409b1 | ||
|
|
1fb78115e8 | ||
|
|
9567f1f4e8 | ||
|
|
49c93c635d | ||
|
|
5010cdedbb | ||
|
|
13413c05c3 | ||
|
|
dc185be836 | ||
|
|
29be9b6019 | ||
|
|
1247b1c719 | ||
|
|
23233d0dcb | ||
|
|
37540aa4f5 | ||
|
|
db0a971384 | ||
|
|
445e86361c | ||
|
|
80197b566c | ||
|
|
8671533878 | ||
|
|
f51f02375a | ||
|
|
d40df613de | ||
|
|
e77bfae091 | ||
|
|
f9efd803c1 | ||
|
|
54d6470163 | ||
|
|
50640b5bb9 | ||
|
|
4d4ba508ef | ||
|
|
4f2f9fedfa | ||
|
|
732538504d | ||
|
|
d3dcbfe567 | ||
|
|
905d2a8eaa | ||
|
|
b5e46c1a60 | ||
|
|
a3a8ad88fe | ||
|
|
9d6428d5d5 | ||
|
|
d7f8ca3f01 | ||
|
|
56bcc0d09f | ||
|
|
8b0c60d2f9 | ||
|
|
ffdfe4bbf8 | ||
|
|
556ca10ed9 | ||
|
|
193beb76fe | ||
|
|
b2884ddb88 | ||
|
|
82798fa5a3 | ||
|
|
fd8fedca18 | ||
|
|
c52ca65905 | ||
|
|
5d53685976 | ||
|
|
d869c39783 | ||
|
|
fe02964ebd | ||
|
|
86cfea0edb | ||
|
|
a1342b4b9d | ||
|
|
d40a355d9d | ||
|
|
f4f5494b85 | ||
|
|
fc0fd2f6ac | ||
|
|
70a75c2e7b | ||
|
|
6bc2168e29 | ||
|
|
18b1c344e6 | ||
|
|
c6e32940f4 | ||
|
|
a31cb67ec5 | ||
|
|
5ecd15a5f5 | ||
|
|
ec5472f1ed | ||
|
|
9ae804f8ae | ||
|
|
0de26be6e6 | ||
|
|
dc0a5c88b7 | ||
|
|
d6f51a96a5 | ||
|
|
8442a1e612 | ||
|
|
cc82b1a5cf | ||
|
|
ba32012744 | ||
|
|
8f6530d1e7 | ||
|
|
4297c70f58 | ||
|
|
16ed60a5c3 | ||
|
|
d831b93934 |
@@ -0,0 +1,174 @@
|
||||
# Parallel Chunk Upload Support for utopia-php/storage
|
||||
|
||||
## Context
|
||||
|
||||
The Appwrite API now supports out-of-order chunked uploads (chunks can arrive in any sequence). The next step is **parallel uploads** — multiple chunks uploaded simultaneously via separate HTTP requests. The SDK guarantees the first chunk is sent before any parallel chunks, so the document creation race is handled at the API layer. However, the storage device layer has a race condition that must be fixed.
|
||||
|
||||
## Problem: `Local::joinChunks()` Race
|
||||
|
||||
When two requests upload the final missing chunks in parallel, both can observe `countChunks() == $chunks` and call `joinChunks()` simultaneously.
|
||||
|
||||
### Current behavior (loser throws)
|
||||
|
||||
```php
|
||||
// Local::joinChunks()
|
||||
$dest = \fopen($tmpAssemble, 'wb');
|
||||
// ... stream all parts into $tmpAssemble ...
|
||||
|
||||
if (! \rename($tmpAssemble, $path)) {
|
||||
\unlink($tmpAssemble);
|
||||
throw new Exception('Failed to finalize assembled file '.$path);
|
||||
}
|
||||
```
|
||||
|
||||
The winner succeeds with `rename()`. The loser gets `false` from `rename()` (file already exists at `$path`) and throws a 500-error exception. The client that lost the race receives an error even though the file is fully assembled.
|
||||
|
||||
### Required behavior
|
||||
|
||||
If `$path` already exists, another request already assembled the file. The loser should **silently succeed** — the file is complete, nothing more to do.
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
### 1. `Local::joinChunks()` — Handle assembly race
|
||||
|
||||
Before opening `$tmpAssemble`, check if the final file already exists. If it does, skip assembly entirely.
|
||||
|
||||
```php
|
||||
private function joinChunks(string $path, int $chunks): void
|
||||
{
|
||||
// Race winner already assembled the file
|
||||
if (\file_exists($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tmp = \dirname($path).DIRECTORY_SEPARATOR.'tmp_'.asename($path);
|
||||
$tmpAssemble = \dirname($path).DIRECTORY_SEPARATOR.'tmp_assemble_'.asename($path);
|
||||
|
||||
// ... rest of assembly logic ...
|
||||
|
||||
if (! \rename($tmpAssemble, $path)) {
|
||||
// Another request may have won the race between fclose and rename
|
||||
if (\file_exists($path)) {
|
||||
\unlink($tmpAssemble);
|
||||
return;
|
||||
}
|
||||
\unlink($tmpAssemble);
|
||||
throw new Exception('Failed to finalize assembled file '.$path);
|
||||
}
|
||||
|
||||
// ... cleanup ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. `Local::countChunks()` — Reliability under concurrent writes
|
||||
|
||||
`countChunks()` uses `glob()` on the temp directory. Under heavy parallel load, `glob()` might miss files or return inconsistent counts. The current implementation is already fairly robust (it validates `.part.\d+` suffix), but we should document that the return value is a best-effort snapshot.
|
||||
|
||||
No code change needed here unless tests reveal issues.
|
||||
|
||||
### 3. Tests — Concurrent chunk uploads
|
||||
|
||||
Add a test that simulates two parallel requests completing a multi-chunk upload:
|
||||
|
||||
```php
|
||||
public function testParallelChunkUpload(): void
|
||||
{
|
||||
$storage = $this->makeJoinTestStorage();
|
||||
$dest = $storage->getRoot().DIRECTORY_SEPARATOR.'parallel.dat';
|
||||
|
||||
// Upload chunk 1 (creates temp directory)
|
||||
$storage->uploadData('AAAA', $dest, 'application/octet-stream', 1, 2);
|
||||
|
||||
// Simulate two parallel requests uploading the last chunk
|
||||
// In a real test, use pcntl_fork() or pthreads for true concurrency
|
||||
// For the test suite, sequential calls are sufficient if we verify
|
||||
// the second call doesn't throw after the first completed assembly
|
||||
$storage->uploadData('BBBB', $dest, 'application/octet-stream', 2, 2);
|
||||
|
||||
// Verify file exists and is correct
|
||||
$this->assertTrue(\file_exists($dest));
|
||||
$this->assertSame('AAAABBBB', \file_get_contents($dest));
|
||||
|
||||
// Verify second assembly attempt doesn't throw
|
||||
// (This simulates the race where another request already assembled)
|
||||
try {
|
||||
$storage->uploadData('BBBB', $dest, 'application/octet-stream', 2, 2);
|
||||
} catch (\Exception $e) {
|
||||
$this->fail('Duplicate assembly should not throw: '.$e->getMessage());
|
||||
}
|
||||
|
||||
$storage->delete($storage->getRoot(), true);
|
||||
}
|
||||
```
|
||||
|
||||
A more realistic concurrent test using `pcntl_fork()`:
|
||||
|
||||
```php
|
||||
public function testParallelChunkUploadWithFork(): void
|
||||
{
|
||||
if (!\function_exists('pcntl_fork')) {
|
||||
$this->markTestSkipped('pcntl extension required for fork-based concurrency test');
|
||||
}
|
||||
|
||||
$storage = $this->makeJoinTestStorage();
|
||||
$dest = $storage->getRoot().DIRECTORY_SEPARATOR.'parallel-fork.dat';
|
||||
|
||||
// Pre-upload chunk 1
|
||||
$storage->uploadData('AAAA', $dest, 'application/octet-stream', 1, 2);
|
||||
|
||||
$pid = pcntl_fork();
|
||||
if ($pid === -1) {
|
||||
$this->fail('Failed to fork');
|
||||
} elseif ($pid === 0) {
|
||||
// Child process: upload chunk 2
|
||||
try {
|
||||
$storage->uploadData('BBBB', $dest, 'application/octet-stream', 2, 2);
|
||||
exit(0);
|
||||
} catch (\Exception $e) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Parent process: also upload chunk 2 (race condition)
|
||||
$parentSuccess = true;
|
||||
try {
|
||||
$storage->uploadData('BBBB', $dest, 'application/octet-stream', 2, 2);
|
||||
} catch (\Exception $e) {
|
||||
$parentSuccess = false;
|
||||
}
|
||||
|
||||
pcntl_waitpid($pid, $status);
|
||||
$childSuccess = pcntl_wexitstatus($status) === 0;
|
||||
|
||||
// At least one should succeed
|
||||
$this->assertTrue($parentSuccess || $childSuccess, 'At least one parallel upload should succeed');
|
||||
|
||||
// File should be correctly assembled
|
||||
$this->assertTrue(\file_exists($dest));
|
||||
$this->assertSame('AAAABBBB', \file_get_contents($dest));
|
||||
|
||||
$storage->delete($storage->getRoot(), true);
|
||||
}
|
||||
```
|
||||
|
||||
## S3 Device
|
||||
|
||||
S3 already handles out-of-order multipart uploads natively. The `completeMultipartUpload` call with `ksort()` sorts parts by number regardless of upload order. However, parallel `completeMultipartUpload` calls for the same `uploadId` would still be problematic.
|
||||
|
||||
This is an **API-layer concern** — the Appwrite API should ensure only one request calls `completeMultipartUpload` per upload. The S3 device itself does not need changes.
|
||||
|
||||
## Files to Change
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/Storage/Device/Local.php` | Add `file_exists($path)` guard at start of `joinChunks()` and in `rename()` failure handler |
|
||||
| `tests/Storage/Device/LocalTest.php` | Add `testParallelChunkUpload` and `testParallelChunkUploadWithFork` |
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
Fully backwards compatible. The change only affects the error path when `rename()` fails due to an existing file. Previously it threw; now it returns silently. No public API signatures change.
|
||||
|
||||
## Related PRs
|
||||
|
||||
- Appwrite server PR: https://github.com/appwrite/appwrite/pull/12138 (out-of-order upload support)
|
||||
- This storage PR is a prerequisite for the follow-up Appwrite PR that enables parallel chunk uploads at the API level.
|
||||
@@ -146,3 +146,5 @@ _APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main
|
||||
_APP_TRUSTED_HEADERS=x-forwarded-for
|
||||
_APP_POOL_ADAPTER=stack
|
||||
_APP_WORKER_SCREENSHOTS_ROUTER=http://appwrite
|
||||
_TESTS_OAUTH2_GITHUB_CLIENT_ID=
|
||||
_TESTS_OAUTH2_GITHUB_CLIENT_SECRET=
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const marker = '<!-- appwrite-benchmark-results -->';
|
||||
const serviceLabels = ['Account', 'TablesDB', 'Storage', 'Functions'];
|
||||
|
||||
module.exports = async ({ github, context, core }) => {
|
||||
const body = buildComment(core);
|
||||
fs.writeFileSync('benchmark-comment.txt', body);
|
||||
|
||||
const pullRequest = context.payload.pull_request;
|
||||
if (!pullRequest || pullRequest.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) {
|
||||
return;
|
||||
}
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const existing = comments.find((comment) => {
|
||||
return comment.user?.type === 'Bot' && comment.body?.includes(marker);
|
||||
}) || comments.find((comment) => {
|
||||
return comment.user?.type === 'Bot' && comment.body?.includes('Benchmark results');
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
body,
|
||||
});
|
||||
};
|
||||
|
||||
function buildComment(core) {
|
||||
const before = readSummary('benchmark-before-summary.json', core);
|
||||
const after = readSummary('benchmark-after-summary.json', core);
|
||||
const beforeSamples = readSamples('benchmark-before-samples.json', core);
|
||||
const afterSamples = readSamples('benchmark-after-samples.json', core);
|
||||
const baseRef = markdownText(process.env.BENCHMARK_BASE_REF || 'base');
|
||||
const headRef = markdownText(process.env.BENCHMARK_HEAD_REF || 'head');
|
||||
const rows = benchmarkRows(before, after, beforeSamples, afterSamples);
|
||||
const topWaits = topSamples(afterSamples, 'appwrite_api_waiting', 3);
|
||||
const lines = [
|
||||
marker,
|
||||
'## :sparkles: Benchmark results',
|
||||
'',
|
||||
`Comparing ${baseRef} (before) to ${headRef} (after).`,
|
||||
'',
|
||||
];
|
||||
|
||||
if (before === null) {
|
||||
lines.push('> Before benchmark did not complete; showing current branch metrics only.', '');
|
||||
}
|
||||
if (after === null) {
|
||||
lines.push('> Current branch benchmark did not complete; showing available metrics only.', '');
|
||||
}
|
||||
|
||||
lines.push(
|
||||
'**Before**',
|
||||
'',
|
||||
metricTable(rows, 'before'),
|
||||
'',
|
||||
'**After**',
|
||||
'',
|
||||
metricTable(rows, 'after'),
|
||||
'',
|
||||
'**Delta**',
|
||||
'',
|
||||
'| Scenario | P95 delta (ms) |',
|
||||
'| --- | ---: |',
|
||||
...rows.map(deltaRow),
|
||||
'',
|
||||
'<details>',
|
||||
'<summary><strong>Top API waits</strong></summary>',
|
||||
'',
|
||||
'<br>',
|
||||
'',
|
||||
'| API request | Max wait (ms) |',
|
||||
'| --- | ---: |',
|
||||
...topWaitRows(topWaits),
|
||||
'',
|
||||
'</details>',
|
||||
);
|
||||
|
||||
return `${lines.join('\n')}\n`;
|
||||
}
|
||||
|
||||
function readSummary(path, core) {
|
||||
if (!fs.existsSync(path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(path, 'utf8'));
|
||||
} catch (error) {
|
||||
core?.warning(`Invalid benchmark summary ${path}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readSamples(path, core) {
|
||||
if (!fs.existsSync(path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const contents = fs.readFileSync(path, 'utf8').trim();
|
||||
if (contents === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return contents
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.flatMap((line) => {
|
||||
try {
|
||||
return [JSON.parse(line)];
|
||||
} catch (error) {
|
||||
core?.warning(`Invalid benchmark sample in ${path}: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function benchmarkRows(before, after, beforeSamples, afterSamples) {
|
||||
const beforeServices = serviceStats(beforeSamples);
|
||||
const afterServices = serviceStats(afterSamples);
|
||||
return [
|
||||
{
|
||||
label: 'API total',
|
||||
before: apiSampleStats(beforeSamples) || summaryStats(before, 'appwrite_api_duration'),
|
||||
after: apiSampleStats(afterSamples) || summaryStats(after, 'appwrite_api_duration'),
|
||||
},
|
||||
...serviceLabels.map((label) => ({
|
||||
label,
|
||||
before: beforeServices.get(label) || null,
|
||||
after: afterServices.get(label) || null,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
function summaryStats(summary, durationMetric, iterationsMetric = null, rpsMetric = null) {
|
||||
const values = metricValues(summary, durationMetric);
|
||||
if (!values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
p50: values.med ?? null,
|
||||
p95: values['p(95)'] ?? null,
|
||||
iterations: iterationsMetric ? metricValue(summary, iterationsMetric, 'count') : values.count ?? null,
|
||||
rps: rpsMetric ? metricValue(summary, rpsMetric, 'rate') : null,
|
||||
};
|
||||
}
|
||||
|
||||
function serviceStats(samples) {
|
||||
const apiSamples = samples.filter((sample) => {
|
||||
return sample.metric === 'appwrite_api_duration' && typeof sample.data?.value === 'number';
|
||||
});
|
||||
const groups = new Map();
|
||||
|
||||
for (const sample of apiSamples) {
|
||||
const service = serviceFromName(sample.data.tags?.name || '');
|
||||
if (!service) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const serviceSamples = groups.get(service) || [];
|
||||
serviceSamples.push(sample);
|
||||
groups.set(service, serviceSamples);
|
||||
}
|
||||
|
||||
return new Map([...groups.entries()].map(([service, serviceSamples]) => {
|
||||
const values = serviceSamples.map((sample) => sample.data.value);
|
||||
const durationSeconds = sampleWindowSeconds(serviceSamples);
|
||||
return [service, {
|
||||
p50: percentile(values, 50),
|
||||
p95: percentile(values, 95),
|
||||
iterations: values.length,
|
||||
rps: durationSeconds ? values.length / durationSeconds : null,
|
||||
}];
|
||||
}));
|
||||
}
|
||||
|
||||
function apiSampleStats(samples) {
|
||||
const apiSamples = samples.filter((sample) => {
|
||||
return sample.metric === 'appwrite_api_duration' && typeof sample.data?.value === 'number';
|
||||
});
|
||||
const values = apiSamples.map((sample) => sample.data.value);
|
||||
if (values.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const durationSeconds = sampleWindowSeconds(apiSamples);
|
||||
return {
|
||||
p50: percentile(values, 50),
|
||||
p95: percentile(values, 95),
|
||||
iterations: values.length,
|
||||
rps: durationSeconds ? values.length / durationSeconds : null,
|
||||
};
|
||||
}
|
||||
|
||||
function serviceFromName(name) {
|
||||
if (name.startsWith('account.')) {
|
||||
return 'Account';
|
||||
}
|
||||
if (name.startsWith('tablesdb.')) {
|
||||
return 'TablesDB';
|
||||
}
|
||||
if (name.startsWith('storage.') || name.startsWith('tokens.')) {
|
||||
return 'Storage';
|
||||
}
|
||||
if (name.startsWith('functions.')) {
|
||||
return 'Functions';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function sampleWindowSeconds(samples) {
|
||||
const times = samples
|
||||
.map((sample) => Date.parse(sample.data?.time))
|
||||
.filter((value) => !Number.isNaN(value));
|
||||
if (times.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.max((Math.max(...times) - Math.min(...times)) / 1000, 1);
|
||||
}
|
||||
|
||||
function percentile(values, percentileValue) {
|
||||
if (values.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sorted = [...values].sort((left, right) => left - right);
|
||||
const index = Math.ceil((percentileValue / 100) * sorted.length) - 1;
|
||||
return sorted[Math.max(0, Math.min(index, sorted.length - 1))];
|
||||
}
|
||||
|
||||
function metricValues(data, metric) {
|
||||
return data?.metrics?.[metric]?.values ?? null;
|
||||
}
|
||||
|
||||
function metricValue(data, metric, stat) {
|
||||
return metricValues(data, metric)?.[stat] ?? null;
|
||||
}
|
||||
|
||||
function metricTable(rows, side) {
|
||||
return [
|
||||
'| Scenario | P50 (ms) | P95 (ms) | Requests | RPS |',
|
||||
'| --- | ---: | ---: | ---: | ---: |',
|
||||
...rows.map((row) => metricRow(row, side)),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function metricRow(row, side) {
|
||||
const values = row[side];
|
||||
return `| ${row.label} | ${formatMs(values?.p50)} | ${formatMs(values?.p95)} | ${formatCount(values?.iterations)} | ${formatRate(values?.rps)} |`;
|
||||
}
|
||||
|
||||
function deltaRow(row) {
|
||||
return `| ${row.label} | ${formatDelta(row.before?.p95, row.after?.p95)} |`;
|
||||
}
|
||||
|
||||
function topSamples(samples, metric, limit) {
|
||||
const byName = samples.reduce((result, sample) => {
|
||||
if (sample.metric !== metric || typeof sample.data?.value !== 'number') {
|
||||
return result;
|
||||
}
|
||||
|
||||
const name = sample.data.tags?.name || 'unknown';
|
||||
const current = result.get(name);
|
||||
if (!current || sample.data.value > current.value) {
|
||||
result.set(name, { name, value: sample.data.value });
|
||||
}
|
||||
|
||||
return result;
|
||||
}, new Map());
|
||||
|
||||
return [...byName.values()]
|
||||
.sort((left, right) => right.value - left.value)
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
function topWaitRows(samples) {
|
||||
if (samples.length === 0) {
|
||||
return ['| n/a | n/a |'];
|
||||
}
|
||||
|
||||
return samples.map((sample) => {
|
||||
return `| ${markdownText(sample.name).replace(/\|/g, '\\|')} | ${formatMs(sample.value)} |`;
|
||||
});
|
||||
}
|
||||
|
||||
function markdownText(value) {
|
||||
return String(value || '').replace(/[\r\n]/g, ' ').replace(/[&<>"']/g, (char) => {
|
||||
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char];
|
||||
});
|
||||
}
|
||||
|
||||
function formatMs(value) {
|
||||
return formatNumber(value, 2);
|
||||
}
|
||||
|
||||
function formatRate(value) {
|
||||
return formatNumber(value, 2);
|
||||
}
|
||||
|
||||
function formatCount(value) {
|
||||
if (value === null || value === undefined || Number.isNaN(value)) {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
return `${Math.round(value)}`;
|
||||
}
|
||||
|
||||
function formatDelta(before, after) {
|
||||
if (before === null || before === undefined || after === null || after === undefined || Number.isNaN(before) || Number.isNaN(after)) {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
const difference = Number((after - before).toFixed(2));
|
||||
return `${difference > 0 ? '+' : ''}${trimNumber(difference)}`;
|
||||
}
|
||||
|
||||
function formatNumber(value, decimals) {
|
||||
if (value === null || value === undefined || Number.isNaN(value)) {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
return trimNumber(Number(value).toFixed(decimals));
|
||||
}
|
||||
|
||||
function trimNumber(value) {
|
||||
const text = String(value);
|
||||
const trimmed = text.includes('.') ? text.replace(/\.?0+$/, '') : text;
|
||||
return trimmed === '' ? '0' : trimmed;
|
||||
}
|
||||
+243
-126
@@ -7,6 +7,8 @@ concurrency:
|
||||
env:
|
||||
COMPOSE_FILE: docker-compose.yml
|
||||
IMAGE: appwrite-dev
|
||||
REGISTRY_IMAGE: ghcr.io/${{ github.repository }}/appwrite-dev
|
||||
K6_VERSION: '0.53.0'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -18,6 +20,10 @@ on:
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
dependencies:
|
||||
name: Checks / Dependencies
|
||||
@@ -210,7 +216,7 @@ jobs:
|
||||
with:
|
||||
script: |
|
||||
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
|
||||
const allModes = ['dedicated', 'shared_v1', 'shared_v2'];
|
||||
const allModes = ['dedicated', 'shared'];
|
||||
|
||||
const defaultDatabases = ['MongoDB'];
|
||||
const defaultModes = ['dedicated'];
|
||||
@@ -257,32 +263,30 @@ jobs:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build Appwrite
|
||||
- name: Build and push Appwrite
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: ${{ env.IMAGE }}
|
||||
load: true
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
outputs: type=docker,dest=/tmp/${{ env.IMAGE }}.tar
|
||||
target: development
|
||||
build-args: |
|
||||
DEBUG=false
|
||||
TESTING=true
|
||||
VERSION=dev
|
||||
|
||||
- name: Upload Docker Image
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
retention-days: 1
|
||||
|
||||
unit:
|
||||
name: Tests / Unit
|
||||
runs-on: ubuntu-latest
|
||||
@@ -290,26 +294,32 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
packages: read
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull Docker Image
|
||||
run: |
|
||||
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
|
||||
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
@@ -337,26 +347,32 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
packages: read
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull Docker Image
|
||||
run: |
|
||||
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
|
||||
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
@@ -395,6 +411,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
packages: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -429,6 +446,8 @@ jobs:
|
||||
include:
|
||||
- service: Databases
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
paratest_processes: 3
|
||||
timeout_minutes: 30
|
||||
- service: Sites
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Functions
|
||||
@@ -439,18 +458,18 @@ jobs:
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: TablesDB
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
paratest_processes: 3
|
||||
timeout_minutes: 30
|
||||
- service: Migrations
|
||||
paratest_processes: 1
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Set database environment
|
||||
- name: Set environment
|
||||
run: |
|
||||
echo "_APP_OPTIONS_ROUTER_PROTECTION=enabled" >> $GITHUB_ENV
|
||||
|
||||
if [ "${{ matrix.database }}" = "MariaDB" ]; then
|
||||
echo "COMPOSE_PROFILES=mariadb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_ADAPTER=mariadb" >> $GITHUB_ENV
|
||||
@@ -474,18 +493,26 @@ jobs:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull Docker Image
|
||||
run: |
|
||||
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
|
||||
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
_APP_BROWSER_HOST: http://invalid-browser/v1
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
@@ -502,7 +529,7 @@ jobs:
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
timeout_minutes: 20
|
||||
timeout_minutes: ${{ matrix.timeout_minutes || 20 }}
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/Services/${{ matrix.service }}
|
||||
@@ -512,12 +539,19 @@ jobs:
|
||||
# Services that rely on sequential test method execution (shared static state)
|
||||
FUNCTIONAL_FLAG="--functional"
|
||||
case "${{ matrix.service }}" in
|
||||
Databases|TablesDB|Functions|Realtime|GraphQL) FUNCTIONAL_FLAG="" ;;
|
||||
Databases|TablesDB|Functions|Realtime|GraphQL|ProjectWebhooks) FUNCTIONAL_FLAG="" ;;
|
||||
esac
|
||||
|
||||
PARATEST_PROCESSES="${{ matrix.paratest_processes }}"
|
||||
if [ -z "$PARATEST_PROCESSES" ]; then
|
||||
PARATEST_PROCESSES="$(nproc)"
|
||||
fi
|
||||
|
||||
docker compose exec -T \
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
|
||||
appwrite vendor/bin/paratest --processes $(nproc) $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
|
||||
-e _TESTS_OAUTH2_GITHUB_CLIENT_ID="${{ secrets.TESTS_OAUTH2_GITHUB_CLIENT_ID }}" \
|
||||
-e _TESTS_OAUTH2_GITHUB_CLIENT_SECRET="${{ secrets.TESTS_OAUTH2_GITHUB_CLIENT_SECRET }}" \
|
||||
appwrite vendor/bin/paratest --processes "$PARATEST_PROCESSES" $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
@@ -532,6 +566,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
packages: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -539,12 +574,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
@@ -552,18 +583,26 @@ jobs:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull Docker Image
|
||||
run: |
|
||||
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
|
||||
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
_APP_OPTIONS_ABUSE: enabled
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
@@ -594,6 +633,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
packages: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -602,29 +642,31 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull Docker Image
|
||||
run: |
|
||||
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
|
||||
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
@@ -658,19 +700,20 @@ jobs:
|
||||
|
||||
benchmark:
|
||||
name: Benchmark
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
packages: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
@@ -678,79 +721,153 @@ jobs:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull Appwrite image
|
||||
run: |
|
||||
sed -i 's/traefik/localhost/g' .env
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose up -d
|
||||
sleep 10
|
||||
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
|
||||
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
|
||||
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}:after
|
||||
|
||||
- name: Install Oha
|
||||
- name: Setup k6
|
||||
uses: grafana/setup-k6-action@ffe7d7290dfa715e48c2ccc924d068444c94bde2
|
||||
with:
|
||||
k6-version: ${{ env.K6_VERSION }}
|
||||
|
||||
- name: Prepare benchmark before
|
||||
id: benchmark_before_prepare
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "deb [signed-by=/usr/share/keyrings/azlux-archive-keyring.gpg] http://packages.azlux.fr/debian/ stable main" | sudo tee /etc/apt/sources.list.d/azlux.list
|
||||
sudo wget -O /usr/share/keyrings/azlux-archive-keyring.gpg https://azlux.fr/repo.gpg
|
||||
sudo apt update
|
||||
sudo apt install oha
|
||||
oha --version
|
||||
git fetch --depth=1 origin ${{ github.event.pull_request.base.sha }}
|
||||
git worktree add --detach /tmp/appwrite-benchmark-before ${{ github.event.pull_request.base.sha }}
|
||||
docker build \
|
||||
--cache-from ${{ env.IMAGE }}:after \
|
||||
--target development \
|
||||
--build-arg DEBUG=false \
|
||||
--build-arg TESTING=true \
|
||||
--build-arg VERSION=dev \
|
||||
--tag ${{ env.IMAGE }}:before \
|
||||
/tmp/appwrite-benchmark-before
|
||||
|
||||
- name: Benchmark PR
|
||||
run: 'oha -z 180s http://localhost/v1/health/version --output-format json > benchmark.json'
|
||||
|
||||
- name: Cleaning
|
||||
run: docker compose down -v
|
||||
|
||||
- name: Installing latest version
|
||||
- name: Start before Appwrite
|
||||
id: benchmark_before_start
|
||||
if: steps.benchmark_before_prepare.outcome == 'success'
|
||||
continue-on-error: true
|
||||
working-directory: /tmp/appwrite-benchmark-before
|
||||
env:
|
||||
_APP_DOMAIN: localhost
|
||||
_APP_CONSOLE_DOMAIN: localhost
|
||||
_APP_DOMAIN_FUNCTIONS: functions.localhost
|
||||
_APP_OPTIONS_ABUSE: disabled
|
||||
run: |
|
||||
rm docker-compose.yml
|
||||
rm .env
|
||||
curl https://appwrite.io/install/compose -o docker-compose.yml
|
||||
curl https://appwrite.io/install/env -o .env
|
||||
sed -i 's/_APP_OPTIONS_ABUSE=enabled/_APP_OPTIONS_ABUSE=disabled/g' .env
|
||||
docker compose up -d
|
||||
sleep 10
|
||||
docker tag ${{ env.IMAGE }}:before ${{ env.IMAGE }}
|
||||
docker compose up -d --wait --no-build
|
||||
|
||||
- name: Benchmark Latest
|
||||
run: oha -z 180s http://localhost/v1/health/version --output-format json > benchmark-latest.json
|
||||
- name: Prepare benchmark files
|
||||
run: rm -f benchmark-before-summary.json benchmark-after-summary.json benchmark-before-samples.json benchmark-after-samples.json
|
||||
|
||||
- name: Prepare comment
|
||||
- name: Benchmark before
|
||||
if: steps.benchmark_before_start.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: grafana/run-k6-action@a15e2072ede004e8d46141e33d7f7dad8ad08d9d
|
||||
env:
|
||||
APPWRITE_ENDPOINT: 'http://localhost/v1'
|
||||
APPWRITE_BENCHMARK_ITERATIONS: '5'
|
||||
APPWRITE_BENCHMARK_VUS: '1'
|
||||
APPWRITE_WORKER_TIMEOUT_MS: '120000'
|
||||
APPWRITE_BENCHMARK_SUMMARY_PATH: 'benchmark-before-summary.json'
|
||||
with:
|
||||
path: tests/benchmarks/http.js
|
||||
flags: --quiet --out json=benchmark-before-samples.json
|
||||
cloud-comment-on-pr: false
|
||||
debug: true
|
||||
|
||||
- name: Stop before Appwrite
|
||||
if: always()
|
||||
run: |
|
||||
echo '## :sparkles: Benchmark results' > benchmark.txt
|
||||
echo ' ' >> benchmark.txt
|
||||
echo "- Requests per second: $(jq -r '.summary.requestsPerSec|tonumber?|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json)" >> benchmark.txt
|
||||
echo "- Requests with 200 status code: $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json)" >> benchmark.txt
|
||||
echo "- P99 latency: $(jq -r '.latencyPercentiles.p99' benchmark.json )" >> benchmark.txt
|
||||
echo " " >> benchmark.txt
|
||||
echo " " >> benchmark.txt
|
||||
echo "## :zap: Benchmark Comparison" >> benchmark.txt
|
||||
echo " " >> benchmark.txt
|
||||
echo "| Metric | This PR | Latest version | " >> benchmark.txt
|
||||
echo "| --- | --- | --- | " >> benchmark.txt
|
||||
echo "| RPS | $(jq -r '.summary.requestsPerSec|tonumber?|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json) | $(jq -r '.summary.requestsPerSec|tonumber|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark-latest.json) | " >> benchmark.txt
|
||||
echo "| 200 | $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json) | $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark-latest.json) | " >> benchmark.txt
|
||||
echo "| P99 | $(jq -r '.latencyPercentiles.p99' benchmark.json ) | $(jq -r '.latencyPercentiles.p99' benchmark-latest.json ) | " >> benchmark.txt
|
||||
if [ -d /tmp/appwrite-benchmark-before ]; then
|
||||
cd /tmp/appwrite-benchmark-before
|
||||
docker compose down -v || true
|
||||
fi
|
||||
|
||||
- name: Wait for benchmark ports
|
||||
if: always()
|
||||
run: |
|
||||
for port in 80 443 8080 9503; do
|
||||
for attempt in $(seq 1 30); do
|
||||
if ! ss -ltn | awk '{print $4}' | grep -Eq "[:.]${port}$"; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if ss -ltn | awk '{print $4}' | grep -Eq "[:.]${port}$"; then
|
||||
echo "Port ${port} is still in use after stopping the before stack"
|
||||
ss -ltn
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Start after Appwrite
|
||||
env:
|
||||
_APP_DOMAIN: localhost
|
||||
_APP_CONSOLE_DOMAIN: localhost
|
||||
_APP_DOMAIN_FUNCTIONS: functions.localhost
|
||||
_APP_OPTIONS_ABUSE: disabled
|
||||
run: |
|
||||
docker tag ${{ env.IMAGE }}:after ${{ env.IMAGE }}
|
||||
docker compose up -d --wait --no-build
|
||||
|
||||
- name: Benchmark after
|
||||
id: benchmark_after
|
||||
continue-on-error: true
|
||||
uses: grafana/run-k6-action@a15e2072ede004e8d46141e33d7f7dad8ad08d9d
|
||||
env:
|
||||
APPWRITE_ENDPOINT: 'http://localhost/v1'
|
||||
APPWRITE_BENCHMARK_ITERATIONS: '5'
|
||||
APPWRITE_BENCHMARK_VUS: '1'
|
||||
APPWRITE_WORKER_TIMEOUT_MS: '120000'
|
||||
APPWRITE_BENCHMARK_PREVIOUS_SUMMARY_PATH: '../../benchmark-before-summary.json'
|
||||
APPWRITE_BENCHMARK_SUMMARY_PATH: 'benchmark-after-summary.json'
|
||||
with:
|
||||
path: tests/benchmarks/http.js
|
||||
flags: --quiet --out json=benchmark-after-samples.json
|
||||
cloud-comment-on-pr: false
|
||||
debug: true
|
||||
|
||||
- name: Stop after Appwrite
|
||||
if: always()
|
||||
run: docker compose down -v || true
|
||||
|
||||
- name: Comment on PR
|
||||
if: always()
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
BENCHMARK_BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||
BENCHMARK_HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
with:
|
||||
script: |
|
||||
const comment = require('./.github/workflows/benchmark-comment.js');
|
||||
await comment({ github, context, core });
|
||||
|
||||
- name: Save results
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: benchmark.json
|
||||
path: benchmark.json
|
||||
name: benchmark-results
|
||||
path: |
|
||||
benchmark-comment.txt
|
||||
benchmark-before-summary.json
|
||||
benchmark-after-summary.json
|
||||
benchmark-before-samples.json
|
||||
benchmark-after-samples.json
|
||||
retention-days: 7
|
||||
|
||||
- name: Find Comment
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: Benchmark results
|
||||
|
||||
- name: Comment on PR
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-path: benchmark.txt
|
||||
edit-mode: replace
|
||||
- name: Fail benchmark
|
||||
if: always() && steps.benchmark_after.outcome != 'success'
|
||||
run: exit 1
|
||||
|
||||
@@ -5,6 +5,11 @@ on:
|
||||
types:
|
||||
- closed
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -36,4 +41,29 @@ jobs:
|
||||
done
|
||||
done
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Cleanup GHCR image
|
||||
continue-on-error: true
|
||||
run: |
|
||||
package_path="${GITHUB_REPOSITORY#*/}/appwrite-dev"
|
||||
encoded_path="$(printf '%s' "$package_path" | jq -Rr @uri)"
|
||||
|
||||
gh api --paginate "/repos/${GITHUB_REPOSITORY}/pulls/${{ github.event.pull_request.number }}/commits" --jq '.[].sha' | while read -r sha; do
|
||||
version_ids=$(gh api --paginate -H "Accept: application/vnd.github+json" \
|
||||
"/orgs/${GITHUB_REPOSITORY_OWNER}/packages/container/${encoded_path}/versions" \
|
||||
--jq ".[] | select(.metadata.container.tags | index(\"${sha}\")) | .id")
|
||||
|
||||
if [ -z "$version_ids" ]; then
|
||||
echo "No GHCR version found for SHA ${sha}"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "$version_ids" | while read -r version_id; do
|
||||
gh api --method DELETE -H "Accept: application/vnd.github+json" \
|
||||
"/orgs/${GITHUB_REPOSITORY_OWNER}/packages/container/${encoded_path}/versions/${version_id}"
|
||||
echo "Deleted ${package_path}:${sha} (version ${version_id})"
|
||||
done
|
||||
done
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -24,9 +24,11 @@ jobs:
|
||||
ignore-unfixed: 'false'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
- name: Upload Docker Image Scan Results
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
if: always() && hashFiles('trivy-image-results.sarif') != ''
|
||||
with:
|
||||
sarif_file: 'trivy-image-results.sarif'
|
||||
category: 'trivy-image'
|
||||
|
||||
scan-code:
|
||||
name: Scan Code
|
||||
@@ -42,6 +44,8 @@ jobs:
|
||||
output: 'trivy-fs-results.sarif'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
- name: Upload Code Scan Results
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
if: always() && hashFiles('trivy-fs-results.sarif') != ''
|
||||
with:
|
||||
sarif_file: 'trivy-fs-results.sarif'
|
||||
category: 'trivy-source'
|
||||
|
||||
+1
-1
@@ -892,7 +892,7 @@
|
||||
* Unset index length by @fogelito in https://github.com/appwrite/appwrite/pull/8978
|
||||
* Update base to 0.9.5 by @basert in https://github.com/appwrite/appwrite/pull/9005
|
||||
* Sync main into 1.6.x by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/9011
|
||||
* Improved shared tables V2 by @abnegate in https://github.com/appwrite/appwrite/pull/9013
|
||||
* Improved shared tables by @abnegate in https://github.com/appwrite/appwrite/pull/9013
|
||||
* Ensure backwards compatibility for 1.6.x by @christyjacob4 in https://github.com/appwrite/appwrite/pull/9018
|
||||
|
||||
# Version 1.6.0
|
||||
|
||||
+7
-2
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
|
||||
--no-plugins --no-scripts --prefer-dist \
|
||||
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
|
||||
|
||||
FROM appwrite/base:1.0.1 AS base
|
||||
FROM appwrite/base:1.2.1 AS base
|
||||
|
||||
LABEL maintainer="team@appwrite.io"
|
||||
|
||||
@@ -24,6 +24,10 @@ ENV _APP_VERSION=$VERSION \
|
||||
_APP_HOME=https://appwrite.io
|
||||
|
||||
RUN \
|
||||
if [ "$DEBUG" != "true" ]; then \
|
||||
rm -f /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && \
|
||||
rm -f /usr/local/lib/php/extensions/no-debug-non-zts-*/xdebug.so; \
|
||||
fi && \
|
||||
if [ "$DEBUG" == "true" ]; then \
|
||||
apk add boost boost-dev; \
|
||||
fi
|
||||
@@ -100,7 +104,8 @@ RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/
|
||||
FROM base AS production
|
||||
|
||||
RUN rm -rf /usr/src/code/app/config/specs && \
|
||||
rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20240924/xdebug.so && \
|
||||
rm -f /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini && \
|
||||
rm -f /usr/local/lib/php/extensions/no-debug-non-zts-*/xdebug.so && \
|
||||
find /usr -name '*.a' -delete 2>/dev/null || true && \
|
||||
find /usr -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true && \
|
||||
find /usr -name '*.pyc' -delete 2>/dev/null || true
|
||||
|
||||
+3
-3
@@ -72,7 +72,7 @@ docker run -it --rm \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
appwrite/appwrite:1.9.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
### Windows
|
||||
@@ -84,7 +84,7 @@ docker run -it --rm ^
|
||||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
appwrite/appwrite:1.9.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
@@ -94,7 +94,7 @@ docker run -it --rm `
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
appwrite/appwrite:1.9.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
|
||||
|
||||
@@ -75,7 +75,7 @@ docker run -it --rm \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
appwrite/appwrite:1.9.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
### Windows
|
||||
@@ -88,7 +88,7 @@ docker run -it --rm ^
|
||||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
appwrite/appwrite:1.9.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
@@ -99,7 +99,7 @@ docker run -it --rm `
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
appwrite/appwrite:1.9.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
|
||||
|
||||
+5
-4
@@ -2,10 +2,10 @@
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
|
||||
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
@@ -253,6 +253,10 @@ $container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePubli
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('publisherForStatsResources', fn (Publisher $publisher) => new StatsResourcesPublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
|
||||
@@ -263,9 +267,6 @@ $container->set('queueForFunctions', function (Publisher $publisher) {
|
||||
$container->set('queueForDeletes', function (Publisher $publisher) {
|
||||
return new Delete($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('queueForCertificates', function (Publisher $publisher) {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('logError', function (Registry $register) {
|
||||
return function (Throwable $error, string $namespace, string $action) use ($register) {
|
||||
Console::error('[Error] Timestamp: ' . date('c', time()));
|
||||
|
||||
@@ -34,6 +34,11 @@ $console = [
|
||||
'legalAddress' => '',
|
||||
'legalTaxId' => '',
|
||||
'auths' => [
|
||||
'membershipsUserName' => true,
|
||||
'membershipsUserEmail' => true,
|
||||
'membershipsMfa' => true,
|
||||
'membershipsUserId' => true,
|
||||
'membershipsUserPhone' => true,
|
||||
'mockNumbers' => [],
|
||||
'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled',
|
||||
'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user
|
||||
|
||||
+21
-1
@@ -54,6 +54,11 @@ return [
|
||||
'description' => 'Rate limit for the current endpoint has been exceeded. Please try again after some time.',
|
||||
'code' => 429,
|
||||
],
|
||||
Exception::GENERAL_RESOURCE_LOCKED => [
|
||||
'name' => Exception::GENERAL_RESOURCE_LOCKED,
|
||||
'description' => 'The requested resource is currently being modified by another request. Please retry after a brief delay.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::GENERAL_SMTP_DISABLED => [
|
||||
'name' => Exception::GENERAL_SMTP_DISABLED,
|
||||
'description' => 'SMTP is disabled on your Appwrite instance. You can <a href="/docs/email-delivery">learn more about setting up SMTP</a> in our docs.',
|
||||
@@ -384,7 +389,7 @@ return [
|
||||
],
|
||||
Exception::API_KEY_EXPIRED => [
|
||||
'name' => Exception::API_KEY_EXPIRED,
|
||||
'description' => 'The dynamic API key has expired. Please don\'t use dynamic API keys for more than duration of the execution.',
|
||||
'description' => 'The ephemeral API key has expired. Please don\'t use ephemeral API keys for more than duration of the execution.',
|
||||
'code' => 401,
|
||||
],
|
||||
|
||||
@@ -1408,4 +1413,19 @@ return [
|
||||
'description' => 'When using project API key, make sure to pass x-appwrite-project header with your project ID.',
|
||||
'code' => 403,
|
||||
],
|
||||
Exception::MOCK_NUMBER_ALREADY_EXISTS => [
|
||||
'name' => Exception::MOCK_NUMBER_ALREADY_EXISTS,
|
||||
'description' => 'Mock number with the requested number already exists. Try again with a different number. or update OTP of existing mock number.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::MOCK_NUMBER_NOT_FOUND => [
|
||||
'name' => Exception::MOCK_NUMBER_NOT_FOUND,
|
||||
'description' => 'Mock number with the requested number could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::MOCK_NUMBER_LIMIT_EXCEEDED => [
|
||||
'name' => Exception::MOCK_NUMBER_LIMIT_EXCEEDED,
|
||||
'description' => 'The maximum number of mock phones for this project has been reached.',
|
||||
'code' => 400,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -9,11 +9,5 @@ return [
|
||||
'mfaChallenge',
|
||||
'sessionAlert',
|
||||
'otpSession'
|
||||
],
|
||||
'sms' => [
|
||||
'verification',
|
||||
'login',
|
||||
'invitation',
|
||||
'mfaChallenge'
|
||||
]
|
||||
];
|
||||
|
||||
@@ -167,6 +167,17 @@ return [
|
||||
'mock' => false,
|
||||
'class' => 'Appwrite\\Auth\\OAuth2\\Figma',
|
||||
],
|
||||
'fusionauth' => [
|
||||
'name' => 'FusionAuth',
|
||||
'developers' => 'https://fusionauth.io/docs/',
|
||||
'icon' => 'icon-fusionauth',
|
||||
'enabled' => true,
|
||||
'sandbox' => false,
|
||||
'form' => 'fusionauth.phtml',
|
||||
'beta' => false,
|
||||
'mock' => false,
|
||||
'class' => 'Appwrite\\Auth\\OAuth2\\FusionAuth',
|
||||
],
|
||||
'github' => [
|
||||
'name' => 'GitHub',
|
||||
'developers' => 'https://developer.github.com/',
|
||||
@@ -200,6 +211,28 @@ return [
|
||||
'mock' => false,
|
||||
'class' => 'Appwrite\\Auth\\OAuth2\\Google',
|
||||
],
|
||||
'keycloak' => [
|
||||
'name' => 'Keycloak',
|
||||
'developers' => 'https://www.keycloak.org/documentation',
|
||||
'icon' => 'icon-keycloak',
|
||||
'enabled' => true,
|
||||
'sandbox' => false,
|
||||
'form' => 'keycloak.phtml',
|
||||
'beta' => false,
|
||||
'mock' => false,
|
||||
'class' => 'Appwrite\\Auth\\OAuth2\\Keycloak',
|
||||
],
|
||||
'kick' => [
|
||||
'name' => 'Kick',
|
||||
'developers' => 'https://docs.kick.com/',
|
||||
'icon' => 'icon-kick',
|
||||
'enabled' => true,
|
||||
'sandbox' => false,
|
||||
'form' => false,
|
||||
'beta' => false,
|
||||
'mock' => false,
|
||||
'class' => 'Appwrite\\Auth\\OAuth2\\Kick',
|
||||
],
|
||||
'linkedin' => [
|
||||
'name' => 'LinkedIn',
|
||||
'developers' => 'https://developer.linkedin.com/',
|
||||
|
||||
+13
-5
@@ -21,8 +21,8 @@ $member = [
|
||||
'projects.read',
|
||||
'locale.read',
|
||||
'avatars.read',
|
||||
'execution.read',
|
||||
'execution.write',
|
||||
'executions.read',
|
||||
'executions.write',
|
||||
'targets.read',
|
||||
'targets.write',
|
||||
'subscribers.write',
|
||||
@@ -55,6 +55,14 @@ $admins = [
|
||||
'tables.write',
|
||||
'platforms.read',
|
||||
'platforms.write',
|
||||
'oauth2.read',
|
||||
'oauth2.write',
|
||||
'mocks.read',
|
||||
'mocks.write',
|
||||
'policies.read',
|
||||
'policies.write',
|
||||
'templates.read',
|
||||
'templates.write',
|
||||
'projects.write',
|
||||
'keys.read',
|
||||
'keys.write',
|
||||
@@ -73,8 +81,8 @@ $admins = [
|
||||
'sites.write',
|
||||
'log.read',
|
||||
'log.write',
|
||||
'execution.read',
|
||||
'execution.write',
|
||||
'executions.read',
|
||||
'executions.write',
|
||||
'rules.read',
|
||||
'rules.write',
|
||||
'migrations.read',
|
||||
@@ -115,7 +123,7 @@ return [
|
||||
'files.write',
|
||||
'locale.read',
|
||||
'avatars.read',
|
||||
'execution.write',
|
||||
'executions.write',
|
||||
],
|
||||
],
|
||||
User::ROLE_USERS => [
|
||||
|
||||
+313
-180
@@ -1,207 +1,340 @@
|
||||
<?php
|
||||
|
||||
return [ // List of publicly visible scopes
|
||||
'sessions.write' => [
|
||||
'description' => 'Access to create, update, and delete user sessions',
|
||||
],
|
||||
'users.read' => [
|
||||
'description' => 'Access to read your project\'s users',
|
||||
],
|
||||
'users.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s users',
|
||||
],
|
||||
'teams.read' => [
|
||||
'description' => 'Access to read your project\'s teams',
|
||||
],
|
||||
'teams.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s teams',
|
||||
],
|
||||
'databases.read' => [
|
||||
'description' => 'Access to read your project\'s databases',
|
||||
],
|
||||
'databases.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s databases',
|
||||
],
|
||||
'collections.read' => [
|
||||
'description' => 'Access to read your project\'s database collections',
|
||||
],
|
||||
'collections.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s database collections',
|
||||
],
|
||||
'tables.read' => [
|
||||
'description' => 'Access to read your project\'s database tables',
|
||||
],
|
||||
'tables.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s database tables',
|
||||
],
|
||||
'attributes.read' => [
|
||||
'description' => 'Access to read your project\'s database collection\'s attributes',
|
||||
],
|
||||
'attributes.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s database collection\'s attributes',
|
||||
],
|
||||
'columns.read' => [
|
||||
'description' => 'Access to read your project\'s database table\'s columns',
|
||||
],
|
||||
'columns.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s database table\'s columns',
|
||||
],
|
||||
'indexes.read' => [
|
||||
'description' => 'Access to read your project\'s database table\'s indexes',
|
||||
],
|
||||
'indexes.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s database table\'s indexes',
|
||||
],
|
||||
'documents.read' => [
|
||||
'description' => 'Access to read your project\'s database documents',
|
||||
],
|
||||
'documents.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s database documents',
|
||||
],
|
||||
'rows.read' => [
|
||||
'description' => 'Access to read your project\'s database rows',
|
||||
],
|
||||
'rows.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s database rows',
|
||||
],
|
||||
'files.read' => [
|
||||
'description' => 'Access to read your project\'s storage files and preview images',
|
||||
],
|
||||
'files.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s storage files',
|
||||
],
|
||||
'buckets.read' => [
|
||||
'description' => 'Access to read your project\'s storage buckets',
|
||||
],
|
||||
'buckets.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s storage buckets',
|
||||
],
|
||||
'functions.read' => [
|
||||
'description' => 'Access to read your project\'s functions and code deployments',
|
||||
],
|
||||
'functions.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s functions and code deployments',
|
||||
],
|
||||
'sites.read' => [
|
||||
'description' => 'Access to read your project\'s sites and deployments',
|
||||
],
|
||||
'sites.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s sites and deployments',
|
||||
],
|
||||
'log.read' => [
|
||||
'description' => 'Access to read your site\'s logs',
|
||||
],
|
||||
'log.write' => [
|
||||
'description' => 'Access to update, and delete your site\'s logs',
|
||||
],
|
||||
'execution.read' => [
|
||||
'description' => 'Access to read your project\'s execution logs',
|
||||
],
|
||||
'execution.write' => [
|
||||
'description' => 'Access to execute your project\'s functions',
|
||||
],
|
||||
'locale.read' => [
|
||||
'description' => 'Access to access your project\'s Locale service',
|
||||
],
|
||||
'avatars.read' => [
|
||||
'description' => 'Access to access your project\'s Avatars service',
|
||||
],
|
||||
'health.read' => [
|
||||
'description' => 'Access to read your project\'s health status',
|
||||
],
|
||||
'providers.read' => [
|
||||
'description' => 'Access to read your project\'s providers',
|
||||
],
|
||||
'providers.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s providers',
|
||||
],
|
||||
'messages.read' => [
|
||||
'description' => 'Access to read your project\'s messages',
|
||||
],
|
||||
'messages.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s messages',
|
||||
],
|
||||
'topics.read' => [
|
||||
'description' => 'Access to read your project\'s topics',
|
||||
],
|
||||
'topics.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s topics',
|
||||
],
|
||||
'subscribers.read' => [
|
||||
'description' => 'Access to read your project\'s subscribers',
|
||||
],
|
||||
'subscribers.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s subscribers',
|
||||
],
|
||||
'targets.read' => [
|
||||
'description' => 'Access to read your project\'s targets',
|
||||
],
|
||||
'targets.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s targets',
|
||||
],
|
||||
'rules.read' => [
|
||||
'description' => 'Access to read your project\'s proxy rules',
|
||||
],
|
||||
'rules.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s proxy rules',
|
||||
],
|
||||
'schedules.read' => [
|
||||
'description' => 'Access to read your project\'s schedules',
|
||||
],
|
||||
'schedules.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s schedules',
|
||||
],
|
||||
'migrations.read' => [
|
||||
'description' => 'Access to read your project\'s migrations',
|
||||
],
|
||||
'migrations.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s migrations.',
|
||||
],
|
||||
'vcs.read' => [
|
||||
'description' => 'Access to read your project\'s VCS repositories',
|
||||
],
|
||||
'vcs.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s VCS repositories',
|
||||
],
|
||||
'assistant.read' => [
|
||||
'description' => 'Access to read the Assistant service',
|
||||
],
|
||||
'tokens.read' => [
|
||||
'description' => 'Access to read your project\'s tokens',
|
||||
],
|
||||
'tokens.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s tokens',
|
||||
],
|
||||
"webhooks.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s webhooks",
|
||||
],
|
||||
"webhooks.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s webhooks",
|
||||
],
|
||||
// List of publicly visible scopes
|
||||
return [
|
||||
// Project
|
||||
"project.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s information",
|
||||
"category" => "Project",
|
||||
],
|
||||
"project.write" => [
|
||||
"description" =>
|
||||
"Access to update project\'s information",
|
||||
"category" => "Project",
|
||||
],
|
||||
"keys.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s keys",
|
||||
"category" => "Project",
|
||||
],
|
||||
"keys.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s keys",
|
||||
"category" => "Project",
|
||||
],
|
||||
"platforms.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s platforms",
|
||||
"category" => "Project",
|
||||
],
|
||||
"platforms.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s platforms",
|
||||
"category" => "Project",
|
||||
],
|
||||
"mocks.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s mocks",
|
||||
"category" => "Project",
|
||||
],
|
||||
"mocks.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s mocks",
|
||||
"category" => "Project",
|
||||
],
|
||||
"policies.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s policies",
|
||||
"category" => "Project",
|
||||
],
|
||||
"policies.write" => [
|
||||
"description" =>
|
||||
"Access to update project\'s policies",
|
||||
"category" => "Project",
|
||||
],
|
||||
"templates.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s templates",
|
||||
"category" => "Project",
|
||||
],
|
||||
"templates.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s templates",
|
||||
"category" => "Project",
|
||||
],
|
||||
"oauth2.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s OAuth2 configuration",
|
||||
"category" => "Project",
|
||||
],
|
||||
"oauth2.write" => [
|
||||
"description" =>
|
||||
"Access to update project\'s OAuth2 configuration",
|
||||
"category" => "Project",
|
||||
],
|
||||
|
||||
// Auth
|
||||
'users.read' => [
|
||||
'description' => 'Access to read users',
|
||||
'category' => 'Auth',
|
||||
],
|
||||
'users.write' => [
|
||||
'description' => 'Access to create, update, and delete users',
|
||||
'category' => 'Auth',
|
||||
],
|
||||
'sessions.read' => [
|
||||
'description' => 'Access to read user sessions',
|
||||
'category' => 'Auth',
|
||||
],
|
||||
'sessions.write' => [
|
||||
'description' => 'Access to create, update, and delete user sessions',
|
||||
'category' => 'Auth',
|
||||
],
|
||||
'teams.read' => [
|
||||
'description' => 'Access to read teams',
|
||||
'category' => 'Auth',
|
||||
],
|
||||
'teams.write' => [
|
||||
'description' => 'Access to create, update, and delete teams',
|
||||
'category' => 'Auth',
|
||||
],
|
||||
|
||||
// Databases
|
||||
'databases.read' => [
|
||||
'description' => 'Access to read databases',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'databases.write' => [
|
||||
'description' => 'Access to create, update, and delete databases',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'tables.read' => [
|
||||
'description' => 'Access to read database tables',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'tables.write' => [
|
||||
'description' => 'Access to create, update, and delete database tables',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'columns.read' => [
|
||||
'description' => 'Access to read database table columns',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'columns.write' => [
|
||||
'description' => 'Access to create, update, and delete database table columns',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'indexes.read' => [
|
||||
'description' => 'Access to read database table indexes',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'indexes.write' => [
|
||||
'description' => 'Access to create, update, and delete database table indexes',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'rows.read' => [
|
||||
'description' => 'Access to read database table rows',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'rows.write' => [
|
||||
'description' => 'Access to create, update, and delete database table rows',
|
||||
'category' => 'Databases',
|
||||
],
|
||||
'collections.read' => [
|
||||
'description' => 'Access to read database collections',
|
||||
'category' => 'Databases',
|
||||
'deprecated' => true,
|
||||
],
|
||||
'collections.write' => [
|
||||
'description' => 'Access to create, update, and delete database collections',
|
||||
'category' => 'Databases',
|
||||
'deprecated' => true,
|
||||
],
|
||||
'attributes.read' => [
|
||||
'description' => 'Access to read database collection attributes',
|
||||
'category' => 'Databases',
|
||||
'deprecated' => true,
|
||||
],
|
||||
'attributes.write' => [
|
||||
'description' => 'Access to create, update, and delete database collection attributes',
|
||||
'category' => 'Databases',
|
||||
'deprecated' => true,
|
||||
],
|
||||
'documents.read' => [
|
||||
'description' => 'Access to read database collection documents',
|
||||
'category' => 'Databases',
|
||||
'deprecated' => true,
|
||||
],
|
||||
'documents.write' => [
|
||||
'description' => 'Access to create, update, and delete database collection documents',
|
||||
'category' => 'Databases',
|
||||
'deprecated' => true,
|
||||
],
|
||||
|
||||
// Storage
|
||||
'buckets.read' => [
|
||||
'description' => 'Access to read storage buckets',
|
||||
'category' => 'Storage',
|
||||
],
|
||||
'buckets.write' => [
|
||||
'description' => 'Access to create, update, and delete storage buckets',
|
||||
'category' => 'Storage',
|
||||
],
|
||||
'files.read' => [
|
||||
'description' => 'Access to read storage files and preview images',
|
||||
'category' => 'Storage',
|
||||
],
|
||||
'files.write' => [
|
||||
'description' => 'Access to create, update, and delete storage files',
|
||||
'category' => 'Storage',
|
||||
],
|
||||
'tokens.read' => [
|
||||
'description' => 'Access to read storage file tokens',
|
||||
'category' => 'Storage',
|
||||
],
|
||||
'tokens.write' => [
|
||||
'description' => 'Access to create, update, and delete storage file tokens',
|
||||
'category' => 'Storage',
|
||||
],
|
||||
|
||||
// Functions
|
||||
'functions.read' => [
|
||||
'description' => 'Access to read functions and deployments',
|
||||
'category' => 'Functions',
|
||||
],
|
||||
'functions.write' => [
|
||||
'description' => 'Access to create, update, and delete functions and deployments',
|
||||
'category' => 'Functions',
|
||||
],
|
||||
'executions.read' => [
|
||||
'description' => 'Access to read function executions',
|
||||
'category' => 'Functions',
|
||||
],
|
||||
'executions.write' => [
|
||||
'description' => 'Access to create function executions',
|
||||
'category' => 'Functions',
|
||||
],
|
||||
|
||||
// Sites
|
||||
'sites.read' => [
|
||||
'description' => 'Access to read sites and deployments',
|
||||
'category' => 'Sites',
|
||||
],
|
||||
'sites.write' => [
|
||||
'description' => 'Access to create, update, and delete sites and deployments',
|
||||
'category' => 'Sites',
|
||||
],
|
||||
'log.read' => [
|
||||
'description' => 'Access to read site logs',
|
||||
'category' => 'Sites',
|
||||
],
|
||||
'log.write' => [
|
||||
'description' => 'Access to update, and delete site logs',
|
||||
'category' => 'Sites',
|
||||
],
|
||||
|
||||
// Messaging
|
||||
'providers.read' => [
|
||||
'description' => 'Access to read messaging providers',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'providers.write' => [
|
||||
'description' => 'Access to create, update, and delete messaging providers',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'topics.read' => [
|
||||
'description' => 'Access to read messaging topics',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'topics.write' => [
|
||||
'description' => 'Access to create, update, and delete messaging topics',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'subscribers.read' => [
|
||||
'description' => 'Access to read messaging subscribers',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'subscribers.write' => [
|
||||
'description' => 'Access to create, update, and delete messaging subscribers',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'targets.read' => [
|
||||
'description' => 'Access to read messaging targets',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'targets.write' => [
|
||||
'description' => 'Access to create, update, and delete messaging targets',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'messages.read' => [
|
||||
'description' => 'Access to read messaging messages',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
'messages.write' => [
|
||||
'description' => 'Access to create, update, and delete messaging messages',
|
||||
'category' => 'Messaging',
|
||||
],
|
||||
|
||||
// Other
|
||||
"webhooks.read" => [
|
||||
"description" =>
|
||||
"Access to read webhooks",
|
||||
'category' => 'Other',
|
||||
],
|
||||
"webhooks.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete webhooks",
|
||||
'category' => 'Other',
|
||||
],
|
||||
'locale.read' => [
|
||||
'description' => 'Access to use Locale service',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'avatars.read' => [
|
||||
'description' => 'Access to use Avatars service',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'health.read' => [
|
||||
'description' => 'Access to use Health service',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'assistant.read' => [
|
||||
'description' => 'Access to use Assistant service',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'migrations.read' => [
|
||||
'description' => 'Access to read migrations',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'migrations.write' => [
|
||||
'description' => 'Access to create, update, and delete migrations.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
|
||||
// TODO: Figure out where to move those
|
||||
'schedules.read' => [
|
||||
'description' => 'Access to read schedules.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'schedules.write' => [
|
||||
'description' => 'Access to create, update, and delete schedules.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'vcs.read' => [
|
||||
'description' => 'Access to read resources under VCS service.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'vcs.write' => [
|
||||
'description' => 'Access to create, update, and delete resources under VCS service.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'rules.read' => [
|
||||
'description' => 'Access to read proxy rules.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
'rules.write' => [
|
||||
'description' => 'Access to create, update, and delete proxy rules.',
|
||||
'category' => 'Other',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -300,6 +300,26 @@ return [
|
||||
'repoBranch' => 'main',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/cursor-plugin/CHANGELOG.md'),
|
||||
],
|
||||
[
|
||||
'key' => 'claude-plugin',
|
||||
'name' => 'ClaudePlugin',
|
||||
'version' => '0.1.0',
|
||||
'url' => 'https://github.com/appwrite/claude-plugin.git',
|
||||
'enabled' => true,
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'spec' => 'static',
|
||||
'family' => APP_SDK_PLATFORM_STATIC,
|
||||
'prism' => 'claude-plugin',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/static-claude-plugin'),
|
||||
'gitUrl' => 'git@github.com:appwrite/claude-plugin.git',
|
||||
'gitRepoName' => 'claude-plugin',
|
||||
'gitUserName' => 'appwrite',
|
||||
'gitBranch' => 'dev',
|
||||
'repoBranch' => 'main',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/claude-plugin/CHANGELOG.md'),
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -286,7 +286,7 @@ return [
|
||||
'name' => 'Migrations',
|
||||
'subtitle' => 'The Migrations service allows you to migrate third-party data to your Appwrite project.',
|
||||
'description' => '/docs/services/migrations.md',
|
||||
'controller' => 'api/migrations.php',
|
||||
'controller' => '', // Uses modules
|
||||
'sdk' => true,
|
||||
'docs' => true,
|
||||
'docsUrl' => 'https://appwrite.io/docs/migrations',
|
||||
|
||||
@@ -1487,13 +1487,13 @@ return [
|
||||
]
|
||||
],
|
||||
[
|
||||
'key' => 'crm-dashboard-react-admin',
|
||||
'name' => 'CRM dashboard with React Admin',
|
||||
'tagline' => 'A React-based admin dashboard template with CRM features.',
|
||||
'key' => 'dashboard-react-admin',
|
||||
'name' => 'E-commerce dashboard with React Admin',
|
||||
'tagline' => 'A React-based admin dashboard template with e-commerce features.',
|
||||
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
|
||||
'useCases' => [SiteUseCases::DASHBOARD],
|
||||
'screenshotDark' => $url . '/images/sites/templates/crm-dashboard-react-admin-dark.png',
|
||||
'screenshotLight' => $url . '/images/sites/templates/crm-dashboard-react-admin-light.png',
|
||||
'useCases' => [SiteUseCases::DASHBOARD, SiteUseCases::ECOMMERCE],
|
||||
'screenshotDark' => $url . '/images/sites/templates/dashboard-react-admin-dark.png',
|
||||
'screenshotLight' => $url . '/images/sites/templates/dashboard-react-admin-light.png',
|
||||
'frameworks' => [
|
||||
getFramework('REACT', [
|
||||
'providerRootDirectory' => './react/react-admin',
|
||||
|
||||
@@ -34,6 +34,15 @@ return [
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_LOCKING_ENABLED',
|
||||
'description' => 'Enable distributed locking for platform writes. Locks coordinate concurrent updates across API pods so that read-modify-write operations on shared documents do not lose updates. By default, set to \'enabled\'. Set to \'disabled\' as an emergency kill switch — locks become no-ops (fail-open) and concurrent writes will race.',
|
||||
'introduction' => '1.9.3',
|
||||
'default' => 'enabled',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_OPTIONS_FORCE_HTTPS',
|
||||
'description' => 'Allows you to force HTTPS connection to your API. This feature redirects any HTTP call to HTTPS and adds the \'Strict-Transport-Security\' header to all HTTP responses. By default, set to \'enabled\'. To disable, set to \'disabled\'. This feature will work only when your ports are set to default 80 and 443, and you have set up wildcard certificates with DNS challenge.',
|
||||
|
||||
+124
-80
@@ -133,9 +133,6 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
});
|
||||
|
||||
$provider = match ($verifiedToken->getAttribute('type')) {
|
||||
TOKEN_TYPE_VERIFICATION,
|
||||
TOKEN_TYPE_RECOVERY,
|
||||
TOKEN_TYPE_INVITE => SESSION_PROVIDER_EMAIL,
|
||||
TOKEN_TYPE_MAGIC_URL => SESSION_PROVIDER_MAGIC_URL,
|
||||
TOKEN_TYPE_PHONE => SESSION_PROVIDER_PHONE,
|
||||
TOKEN_TYPE_OAUTH2 => $oauthProvider,
|
||||
@@ -335,15 +332,15 @@ Http::post('/v1/account')
|
||||
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
|
||||
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
|
||||
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
|
||||
throw new Exception(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
@@ -453,7 +450,7 @@ Http::delete('/v1/account')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'account')
|
||||
->label('audits.event', 'user.delete')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('audits.resource', 'user/{user.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'account',
|
||||
group: 'account',
|
||||
@@ -474,19 +471,26 @@ Http::delete('/v1/account')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForDeletes')
|
||||
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes) {
|
||||
->inject('authorization')
|
||||
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes, Authorization $authorization) {
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
// get all memberships
|
||||
$memberships = $user->getAttribute('memberships', []);
|
||||
foreach ($memberships as $membership) {
|
||||
// prevent deletion if at least one active membership
|
||||
if ($membership->getAttribute('confirm', false)) {
|
||||
throw new Exception(Exception::USER_DELETION_PROHIBITED);
|
||||
if (!$membership->getAttribute('confirm', false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$team = $dbForProject->getDocument('teams', $membership->getAttribute('teamId'));
|
||||
if ($team->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Team is left as-is — we don't promote non-owner members to owner.
|
||||
// Orphan teams are cleaned up later by Cloud's inactive project cleanup.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -830,7 +834,7 @@ Http::patch('/v1/account/sessions/:sessionId')
|
||||
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
|
||||
}
|
||||
|
||||
if (!empty($provider) && $className !== null && \class_exists($className)) {
|
||||
if (!empty($provider) && \class_exists($className)) {
|
||||
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
|
||||
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
|
||||
|
||||
@@ -1597,7 +1601,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
}
|
||||
}
|
||||
|
||||
if ($user === false || $user->isEmpty()) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email
|
||||
if ($user->isEmpty()) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email
|
||||
if (empty($email)) {
|
||||
$failureRedirect(Exception::USER_UNAUTHORIZED, 'OAuth provider failed to return email.');
|
||||
}
|
||||
@@ -1614,7 +1618,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
}
|
||||
|
||||
// If user is not found, check if there is a user with the same email
|
||||
if ($user === false || $user->isEmpty()) {
|
||||
if ($user->isEmpty()) {
|
||||
$userWithEmail = $dbForProject->findOne('users', [
|
||||
Query::equal('email', [$email]),
|
||||
]);
|
||||
@@ -1627,7 +1631,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
}
|
||||
|
||||
// If user is not found, check if there is an identity with the same email
|
||||
if ($user === false || $user->isEmpty()) {
|
||||
if ($user->isEmpty()) {
|
||||
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
|
||||
Query::equal('providerEmail', [$email]),
|
||||
]);
|
||||
@@ -1639,7 +1643,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
}
|
||||
}
|
||||
|
||||
if ($user === false || $user->isEmpty()) { // Last option -> create the user
|
||||
if ($user->isEmpty()) { // Last option -> create the user
|
||||
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
|
||||
|
||||
if ($limit !== 0) {
|
||||
@@ -1672,15 +1676,15 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
|
||||
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
|
||||
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
|
||||
$failureRedirect(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
@@ -1813,15 +1817,15 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
|
||||
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
|
||||
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
|
||||
$failureRedirect(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
@@ -1947,7 +1951,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
}
|
||||
|
||||
if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) {
|
||||
if (isset($sessionUpgrade) && isset($session)) {
|
||||
foreach ($user->getAttribute('targets', []) as $target) {
|
||||
if ($target->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) {
|
||||
continue;
|
||||
@@ -2171,15 +2175,15 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
|
||||
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
|
||||
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
|
||||
throw new Exception(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
@@ -2265,7 +2269,10 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
|
||||
$subject = $locale->getText("emails.magicSession.subject");
|
||||
$preview = $locale->getText("emails.magicSession.preview");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ?? [];
|
||||
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.magicSession-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.magicSession-' . $locale->fallback] ?? [];
|
||||
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
$agentOs = $detector->getOS();
|
||||
@@ -2295,8 +2302,8 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
|
||||
$senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
||||
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
|
||||
|
||||
$replyTo = "";
|
||||
$replyToEmail = '';
|
||||
$replyToName = '';
|
||||
|
||||
if ($smtpEnabled) {
|
||||
if (!empty($smtp['senderEmail'])) {
|
||||
@@ -2305,8 +2312,13 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
if (!empty($smtp['senderName'])) {
|
||||
$senderName = $smtp['senderName'];
|
||||
}
|
||||
if (!empty($smtp['replyTo'])) {
|
||||
$replyTo = $smtp['replyTo'];
|
||||
// Includes backwards compatibility: fall back to legacy `replyTo` key
|
||||
$smtpReplyToEmail = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '';
|
||||
if (!empty($smtpReplyToEmail)) {
|
||||
$replyToEmail = $smtpReplyToEmail;
|
||||
}
|
||||
if (!empty($smtp['replyToName'])) {
|
||||
$replyToName = $smtp['replyToName'];
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
@@ -2323,8 +2335,13 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
if (!empty($customTemplate['senderName'])) {
|
||||
$senderName = $customTemplate['senderName'];
|
||||
}
|
||||
if (!empty($customTemplate['replyTo'])) {
|
||||
$replyTo = $customTemplate['replyTo'];
|
||||
// Includes backwards compatibility: fall back to legacy `replyTo` key
|
||||
$customReplyToEmail = $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? '';
|
||||
if (!empty($customReplyToEmail)) {
|
||||
$replyToEmail = $customReplyToEmail;
|
||||
}
|
||||
if (!empty($customTemplate['replyToName'])) {
|
||||
$replyToName = $customTemplate['replyToName'];
|
||||
}
|
||||
|
||||
$body = $customTemplate['message'] ?? '';
|
||||
@@ -2332,7 +2349,8 @@ Http::post('/v1/account/tokens/magic-url')
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
->setSmtpReplyTo($replyTo)
|
||||
->setSmtpReplyToEmail($replyToEmail)
|
||||
->setSmtpReplyToName($replyToName)
|
||||
->setSmtpSenderEmail($senderEmail)
|
||||
->setSmtpSenderName($senderName);
|
||||
}
|
||||
@@ -2478,15 +2496,15 @@ Http::post('/v1/account/tokens/email')
|
||||
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
|
||||
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
|
||||
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
|
||||
throw new Exception(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
@@ -2575,7 +2593,9 @@ Http::post('/v1/account/tokens/email')
|
||||
$preview = $locale->getText("emails.otpSession.preview");
|
||||
$heading = $locale->getText("emails.otpSession.heading");
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ?? [];
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.otpSession-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.otpSession-' . $locale->fallback] ?? [];
|
||||
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
|
||||
|
||||
$validator = new FileName();
|
||||
@@ -2611,7 +2631,8 @@ Http::post('/v1/account/tokens/email')
|
||||
|
||||
$senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
||||
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
|
||||
$replyTo = "";
|
||||
$replyToEmail = '';
|
||||
$replyToName = '';
|
||||
|
||||
if ($smtpEnabled) {
|
||||
if (!empty($smtp['senderEmail'])) {
|
||||
@@ -2620,8 +2641,13 @@ Http::post('/v1/account/tokens/email')
|
||||
if (!empty($smtp['senderName'])) {
|
||||
$senderName = $smtp['senderName'];
|
||||
}
|
||||
if (!empty($smtp['replyTo'])) {
|
||||
$replyTo = $smtp['replyTo'];
|
||||
// Includes backwards compatibility: fall back to legacy `replyTo` key
|
||||
$smtpReplyToEmail = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '';
|
||||
if (!empty($smtpReplyToEmail)) {
|
||||
$replyToEmail = $smtpReplyToEmail;
|
||||
}
|
||||
if (!empty($smtp['replyToName'])) {
|
||||
$replyToName = $smtp['replyToName'];
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
@@ -2638,8 +2664,13 @@ Http::post('/v1/account/tokens/email')
|
||||
if (!empty($customTemplate['senderName'])) {
|
||||
$senderName = $customTemplate['senderName'];
|
||||
}
|
||||
if (!empty($customTemplate['replyTo'])) {
|
||||
$replyTo = $customTemplate['replyTo'];
|
||||
// Includes backwards compatibility: fall back to legacy `replyTo` key
|
||||
$customReplyToEmail = $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? '';
|
||||
if (!empty($customReplyToEmail)) {
|
||||
$replyToEmail = $customReplyToEmail;
|
||||
}
|
||||
if (!empty($customTemplate['replyToName'])) {
|
||||
$replyToName = $customTemplate['replyToName'];
|
||||
}
|
||||
|
||||
$body = $customTemplate['message'] ?? '';
|
||||
@@ -2647,7 +2678,8 @@ Http::post('/v1/account/tokens/email')
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
->setSmtpReplyTo($replyTo)
|
||||
->setSmtpReplyToEmail($replyToEmail)
|
||||
->setSmtpReplyToName($replyToName)
|
||||
->setSmtpSenderEmail($senderEmail)
|
||||
->setSmtpSenderName($senderName);
|
||||
}
|
||||
@@ -2968,11 +3000,6 @@ Http::post('/v1/account/tokens/phone')
|
||||
if ($sendSMS) {
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['sms.login-' . $locale->default] ?? [];
|
||||
if (!empty($customTemplate)) {
|
||||
$message = $customTemplate['message'] ?? $message;
|
||||
}
|
||||
|
||||
$projectName = $project->getAttribute('name');
|
||||
if ($project->getId() === 'console') {
|
||||
$projectName = $platform['platformName'];
|
||||
@@ -3267,7 +3294,7 @@ Http::patch('/v1/account/password')
|
||||
}
|
||||
|
||||
$history[] = $newPassword;
|
||||
$history = array_slice($history, (count($history) - $historyLimit), $historyLimit);
|
||||
$history = array_slice($history, -$historyLimit);
|
||||
}
|
||||
|
||||
if ($project->getAttribute('auths', [])['personalDataCheck'] ?? false) {
|
||||
@@ -3390,15 +3417,15 @@ Http::patch('/v1/account/email')
|
||||
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
|
||||
}
|
||||
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
|
||||
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
|
||||
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
|
||||
}
|
||||
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
|
||||
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
|
||||
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
|
||||
}
|
||||
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
|
||||
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
|
||||
throw new Exception(Exception::USER_EMAIL_FREE);
|
||||
}
|
||||
|
||||
@@ -3430,9 +3457,6 @@ Http::patch('/v1/account/email')
|
||||
|
||||
try {
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
/**
|
||||
* @var Document $oldTarget
|
||||
*/
|
||||
$oldTarget = $user->find('identifier', $oldEmail, 'targets');
|
||||
|
||||
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
|
||||
@@ -3519,9 +3543,6 @@ Http::patch('/v1/account/phone')
|
||||
|
||||
try {
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
/**
|
||||
* @var Document $oldTarget
|
||||
*/
|
||||
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
|
||||
|
||||
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
|
||||
@@ -3726,7 +3747,9 @@ Http::post('/v1/account/recovery')
|
||||
$body = $locale->getText("emails.recovery.body");
|
||||
$subject = $locale->getText("emails.recovery.subject");
|
||||
$preview = $locale->getText("emails.recovery.preview");
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.recovery-' . $locale->default] ?? [];
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.recovery-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.recovery-' . $locale->fallback] ?? [];
|
||||
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-inner-base.tpl');
|
||||
$message
|
||||
@@ -3743,7 +3766,8 @@ Http::post('/v1/account/recovery')
|
||||
|
||||
$senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
||||
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
|
||||
$replyTo = "";
|
||||
$replyToEmail = '';
|
||||
$replyToName = '';
|
||||
|
||||
if ($smtpEnabled) {
|
||||
if (!empty($smtp['senderEmail'])) {
|
||||
@@ -3752,8 +3776,13 @@ Http::post('/v1/account/recovery')
|
||||
if (!empty($smtp['senderName'])) {
|
||||
$senderName = $smtp['senderName'];
|
||||
}
|
||||
if (!empty($smtp['replyTo'])) {
|
||||
$replyTo = $smtp['replyTo'];
|
||||
// Includes backwards compatibility: fall back to legacy `replyTo` key
|
||||
$smtpReplyToEmail = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '';
|
||||
if (!empty($smtpReplyToEmail)) {
|
||||
$replyToEmail = $smtpReplyToEmail;
|
||||
}
|
||||
if (!empty($smtp['replyToName'])) {
|
||||
$replyToName = $smtp['replyToName'];
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
@@ -3770,8 +3799,13 @@ Http::post('/v1/account/recovery')
|
||||
if (!empty($customTemplate['senderName'])) {
|
||||
$senderName = $customTemplate['senderName'];
|
||||
}
|
||||
if (!empty($customTemplate['replyTo'])) {
|
||||
$replyTo = $customTemplate['replyTo'];
|
||||
// Includes backwards compatibility: fall back to legacy `replyTo` key
|
||||
$customReplyToEmail = $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? '';
|
||||
if (!empty($customReplyToEmail)) {
|
||||
$replyToEmail = $customReplyToEmail;
|
||||
}
|
||||
if (!empty($customTemplate['replyToName'])) {
|
||||
$replyToName = $customTemplate['replyToName'];
|
||||
}
|
||||
|
||||
$body = $customTemplate['message'] ?? '';
|
||||
@@ -3779,7 +3813,8 @@ Http::post('/v1/account/recovery')
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
->setSmtpReplyTo($replyTo)
|
||||
->setSmtpReplyToEmail($replyToEmail)
|
||||
->setSmtpReplyToName($replyToName)
|
||||
->setSmtpSenderEmail($senderEmail)
|
||||
->setSmtpSenderName($senderName);
|
||||
}
|
||||
@@ -4034,7 +4069,9 @@ Http::post('/v1/account/verifications/email')
|
||||
$subject = $locale->getText("emails.verification.subject");
|
||||
$heading = $locale->getText("emails.verification.heading");
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['email.verification-' . $locale->default] ?? [];
|
||||
$customTemplate =
|
||||
$project->getAttribute('templates', [])['email.verification-' . $locale->default] ??
|
||||
$project->getAttribute('templates', [])['email.verification-' . $locale->fallback] ?? [];
|
||||
$smtpBaseTemplate = $project->getAttribute('smtpBaseTemplate', 'email-base');
|
||||
|
||||
$validator = new FileName();
|
||||
@@ -4060,7 +4097,8 @@ Http::post('/v1/account/verifications/email')
|
||||
|
||||
$senderEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
||||
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
|
||||
$replyTo = "";
|
||||
$replyToEmail = '';
|
||||
$replyToName = '';
|
||||
|
||||
if ($smtpEnabled) {
|
||||
if (!empty($smtp['senderEmail'])) {
|
||||
@@ -4069,8 +4107,13 @@ Http::post('/v1/account/verifications/email')
|
||||
if (!empty($smtp['senderName'])) {
|
||||
$senderName = $smtp['senderName'];
|
||||
}
|
||||
if (!empty($smtp['replyTo'])) {
|
||||
$replyTo = $smtp['replyTo'];
|
||||
// Includes backwards compatibility: fall back to legacy `replyTo` key
|
||||
$smtpReplyToEmail = $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '';
|
||||
if (!empty($smtpReplyToEmail)) {
|
||||
$replyToEmail = $smtpReplyToEmail;
|
||||
}
|
||||
if (!empty($smtp['replyToName'])) {
|
||||
$replyToName = $smtp['replyToName'];
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
@@ -4087,8 +4130,13 @@ Http::post('/v1/account/verifications/email')
|
||||
if (!empty($customTemplate['senderName'])) {
|
||||
$senderName = $customTemplate['senderName'];
|
||||
}
|
||||
if (!empty($customTemplate['replyTo'])) {
|
||||
$replyTo = $customTemplate['replyTo'];
|
||||
// Includes backwards compatibility: fall back to legacy `replyTo` key
|
||||
$customReplyToEmail = $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? '';
|
||||
if (!empty($customReplyToEmail)) {
|
||||
$replyToEmail = $customReplyToEmail;
|
||||
}
|
||||
if (!empty($customTemplate['replyToName'])) {
|
||||
$replyToName = $customTemplate['replyToName'];
|
||||
}
|
||||
|
||||
$body = $customTemplate['message'] ?? '';
|
||||
@@ -4096,7 +4144,8 @@ Http::post('/v1/account/verifications/email')
|
||||
}
|
||||
|
||||
$queueForMails
|
||||
->setSmtpReplyTo($replyTo)
|
||||
->setSmtpReplyToEmail($replyToEmail)
|
||||
->setSmtpReplyToName($replyToName)
|
||||
->setSmtpSenderEmail($senderEmail)
|
||||
->setSmtpSenderName($senderName);
|
||||
}
|
||||
@@ -4333,11 +4382,6 @@ Http::post('/v1/account/verifications/phone')
|
||||
if ($sendSMS) {
|
||||
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
|
||||
|
||||
$customTemplate = $project->getAttribute('templates', [])['sms.verification-' . $locale->default] ?? [];
|
||||
if (!empty($customTemplate)) {
|
||||
$message = $customTemplate['message'] ?? $message;
|
||||
}
|
||||
|
||||
$messageContent = Template::fromString($locale->getText("sms.verification.body"));
|
||||
$messageContent
|
||||
->setParam('{{project}}', $project->getAttribute('name'))
|
||||
@@ -4612,7 +4656,7 @@ Http::delete('/v1/account/targets/:targetId/push')
|
||||
->groups(['api', 'account'])
|
||||
->label('scope', 'targets.write')
|
||||
->label('audits.event', 'target.delete')
|
||||
->label('audits.resource', 'target/response.$id')
|
||||
->label('audits.resource', 'target/{request.targetId}')
|
||||
->label('event', 'users.[userId].targets.[targetId].delete')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'account',
|
||||
|
||||
@@ -482,7 +482,6 @@ Http::post('/v1/messaging/providers/msg91')
|
||||
$enabled === true
|
||||
&& \array_key_exists('senderId', $credentials)
|
||||
&& \array_key_exists('authKey', $credentials)
|
||||
&& \array_key_exists('from', $options)
|
||||
) {
|
||||
$enabled = true;
|
||||
} else {
|
||||
@@ -3207,10 +3206,6 @@ Http::post('/v1/messaging/messages/email')
|
||||
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
|
||||
}
|
||||
|
||||
if ($status === MessageStatus::SCHEDULED && \is_null($scheduledAt)) {
|
||||
throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE);
|
||||
}
|
||||
|
||||
$mergedTargets = \array_merge($targets, $cc, $bcc);
|
||||
|
||||
if (!empty($mergedTargets)) {
|
||||
@@ -3386,10 +3381,6 @@ Http::post('/v1/messaging/messages/sms')
|
||||
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
|
||||
}
|
||||
|
||||
if ($status === MessageStatus::SCHEDULED && \is_null($scheduledAt)) {
|
||||
throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE);
|
||||
}
|
||||
|
||||
if (!empty($targets)) {
|
||||
$foundTargets = $dbForProject->find('targets', [
|
||||
Query::equal('$id', $targets),
|
||||
@@ -3527,10 +3518,6 @@ Http::post('/v1/messaging/messages/push')
|
||||
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
|
||||
}
|
||||
|
||||
if ($status === MessageStatus::SCHEDULED && \is_null($scheduledAt)) {
|
||||
throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE);
|
||||
}
|
||||
|
||||
if (!empty($targets)) {
|
||||
$foundTargets = $dbForProject->find('targets', [
|
||||
Query::equal('$id', $targets),
|
||||
@@ -4660,7 +4647,7 @@ Http::delete('/v1/messaging/messages/:messageId')
|
||||
if (!empty($scheduleId)) {
|
||||
try {
|
||||
$dbForPlatform->deleteDocument('schedules', $scheduleId);
|
||||
} catch (Exception) {
|
||||
} catch (\Throwable) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -113,11 +113,12 @@ Http::get('/v1/project/usage')
|
||||
$factor = match ($period) {
|
||||
'1h' => 3600,
|
||||
'1d' => 86400,
|
||||
default => throw new \LogicException('Unsupported period: ' . $period),
|
||||
};
|
||||
|
||||
$limit = match ($period) {
|
||||
'1h' => (new DateTime($startDate))->diff(new DateTime($endDate))->days * 24,
|
||||
'1d' => (new DateTime($startDate))->diff(new DateTime($endDate))->days
|
||||
'1d' => (new DateTime($startDate))->diff(new DateTime($endDate))->days,
|
||||
};
|
||||
|
||||
$format = match ($period) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -856,7 +856,7 @@ Http::get('/v1/users/:userId/targets/:targetId')
|
||||
Http::get('/v1/users/:userId/sessions')
|
||||
->desc('List user sessions')
|
||||
->groups(['api', 'users'])
|
||||
->label('scope', 'users.read')
|
||||
->label('scope', ['users.read', 'sessions.read'])
|
||||
->label('sdk', new Method(
|
||||
namespace: 'users',
|
||||
group: 'sessions',
|
||||
@@ -1535,7 +1535,7 @@ Http::patch('/v1/users/:userId/email')
|
||||
Query::equal('identifier', [$email]),
|
||||
]);
|
||||
|
||||
if ($target instanceof Document && !$target->isEmpty()) {
|
||||
if (!$target->isEmpty()) {
|
||||
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
|
||||
}
|
||||
}
|
||||
@@ -1595,9 +1595,6 @@ Http::patch('/v1/users/:userId/email')
|
||||
'emailIsDisposable' => $user->getAttribute('emailIsDisposable'),
|
||||
'emailIsFree' => $user->getAttribute('emailIsFree'),
|
||||
]));
|
||||
/**
|
||||
* @var Document $oldTarget
|
||||
*/
|
||||
$oldTarget = $user->find('identifier', $oldEmail, 'targets');
|
||||
|
||||
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
|
||||
@@ -1681,7 +1678,7 @@ Http::patch('/v1/users/:userId/phone')
|
||||
Query::equal('identifier', [$number]),
|
||||
]);
|
||||
|
||||
if ($target instanceof Document && !$target->isEmpty()) {
|
||||
if (!$target->isEmpty()) {
|
||||
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
|
||||
}
|
||||
}
|
||||
@@ -1691,9 +1688,6 @@ Http::patch('/v1/users/:userId/phone')
|
||||
'phone' => $phoneValue,
|
||||
'phoneVerification' => $user->getAttribute('phoneVerification'),
|
||||
]));
|
||||
/**
|
||||
* @var Document $oldTarget
|
||||
*/
|
||||
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
|
||||
|
||||
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
|
||||
@@ -2252,8 +2246,8 @@ Http::delete('/v1/users/:userId/mfa/authenticators/:type')
|
||||
->label('event', 'users.[userId].delete.mfa')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('audits.userId', '{response.$id}')
|
||||
->label('audits.resource', 'user/{request.userId}')
|
||||
->label('audits.userId', '{request.userId}')
|
||||
->label('usage.metric', 'users.{scope}.requests.update')
|
||||
->label('sdk', [
|
||||
new Method(
|
||||
@@ -2320,7 +2314,7 @@ Http::post('/v1/users/:userId/sessions')
|
||||
->desc('Create session')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].sessions.[sessionId].create')
|
||||
->label('scope', 'users.write')
|
||||
->label('scope', ['users.write', 'sessions.write'])
|
||||
->label('audits.event', 'session.create')
|
||||
->label('audits.resource', 'user/{request.userId}')
|
||||
->label('usage.metric', 'sessions.{scope}.requests.create')
|
||||
@@ -2476,7 +2470,7 @@ Http::delete('/v1/users/:userId/sessions/:sessionId')
|
||||
->desc('Delete user session')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].sessions.[sessionId].delete')
|
||||
->label('scope', 'users.write')
|
||||
->label('scope', ['users.write', 'sessions.write'])
|
||||
->label('audits.event', 'session.delete')
|
||||
->label('audits.resource', 'user/{request.userId}')
|
||||
->label('sdk', new Method(
|
||||
@@ -2527,7 +2521,7 @@ Http::delete('/v1/users/:userId/sessions')
|
||||
->desc('Delete user sessions')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].sessions.delete')
|
||||
->label('scope', 'users.write')
|
||||
->label('scope', ['users.write', 'sessions.write'])
|
||||
->label('audits.event', 'session.delete')
|
||||
->label('audits.resource', 'user/{user.$id}')
|
||||
->label('sdk', new Method(
|
||||
@@ -2842,6 +2836,7 @@ Http::get('/v1/users/usage')
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
default => throw new \LogicException('Unsupported period: ' . $days['period']),
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
|
||||
+72
-55
@@ -7,10 +7,11 @@ use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Bus\Events\ExecutionCompleted;
|
||||
use Appwrite\Bus\Events\RequestCompleted;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete as DeleteEvent;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Publisher\Certificate;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\Locking\Lock;
|
||||
use Appwrite\Network\Cors;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Appwrite\SDK\Method;
|
||||
@@ -26,6 +27,8 @@ use Appwrite\Utopia\Request\Filters\V19 as RequestV19;
|
||||
use Appwrite\Utopia\Request\Filters\V20 as RequestV20;
|
||||
use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
|
||||
use Appwrite\Utopia\Request\Filters\V22 as RequestV22;
|
||||
use Appwrite\Utopia\Request\Filters\V23 as RequestV23;
|
||||
use Appwrite\Utopia\Request\Filters\V24 as RequestV24;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
|
||||
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
|
||||
@@ -34,6 +37,8 @@ use Appwrite\Utopia\Response\Filters\V19 as ResponseV19;
|
||||
use Appwrite\Utopia\Response\Filters\V20 as ResponseV20;
|
||||
use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
|
||||
use Appwrite\Utopia\Response\Filters\V22 as ResponseV22;
|
||||
use Appwrite\Utopia\Response\Filters\V23 as ResponseV23;
|
||||
use Appwrite\Utopia\Response\Filters\V24 as ResponseV24;
|
||||
use Appwrite\Utopia\View;
|
||||
use Executor\Executor;
|
||||
use MaxMind\Db\Reader;
|
||||
@@ -65,9 +70,9 @@ use Utopia\Validator\Text;
|
||||
|
||||
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
|
||||
|
||||
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
|
||||
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount, Lock $lock)
|
||||
{
|
||||
$host = $request->getHostname() ?? '';
|
||||
$host = $request->getHostname();
|
||||
if (!empty($previewHostname)) {
|
||||
$host = $previewHostname;
|
||||
}
|
||||
@@ -118,7 +123,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
}
|
||||
}
|
||||
|
||||
if (!in_array($host, $platformHostnames)) {
|
||||
if (!in_array($host, $platformHostnames) && System::getEnv('_APP_OPTIONS_ROUTER_PROTECTION', 'enabled') === 'enabled') {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.', view: $errorView);
|
||||
}
|
||||
|
||||
@@ -134,9 +139,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$accessedAt = $project->getAttribute('accessedAt', 0);
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
|
||||
'accessedAt' => DateTime::now()
|
||||
])));
|
||||
$lock->set('projects', $project->getId(), 'accessedAt', DateTime::now());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,12 +203,6 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $activeDeploymentId));
|
||||
}
|
||||
|
||||
if ($deployment->getAttribute('resourceType', '') === 'functions') {
|
||||
$type = 'function';
|
||||
} elseif ($deployment->getAttribute('resourceType', '') === 'sites') {
|
||||
$type = 'site';
|
||||
}
|
||||
|
||||
if ($deployment->isEmpty()) {
|
||||
$resourceType = $rule->getAttribute('deploymentResourceType', '');
|
||||
$resourceId = $rule->getAttribute('deploymentResourceId', '');
|
||||
@@ -215,6 +212,14 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
if ($deployment->getAttribute('resourceType', '') === 'functions') {
|
||||
$type = 'function';
|
||||
} elseif ($deployment->getAttribute('resourceType', '') === 'sites') {
|
||||
$type = 'site';
|
||||
} else {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown deployment resource type', view: $errorView);
|
||||
}
|
||||
|
||||
$resource = $type === 'function' ?
|
||||
$authorization->skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) :
|
||||
$authorization->skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', '')));
|
||||
@@ -302,13 +307,13 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
}
|
||||
}
|
||||
|
||||
$body = $swooleRequest->getContent() ?? '';
|
||||
$body = $swooleRequest->getContent() ?: '';
|
||||
$method = $swooleRequest->server['request_method'];
|
||||
|
||||
$requestHeaders = $request->getHeaders();
|
||||
|
||||
if ($resource->isEmpty() || !$resource->getAttribute('enabled')) {
|
||||
if ($type === 'functions') {
|
||||
if ($type === 'function') {
|
||||
throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND, view: $errorView);
|
||||
} else {
|
||||
throw new AppwriteException(AppwriteException::SITE_NOT_FOUND, view: $errorView);
|
||||
@@ -330,7 +335,6 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
$runtime = match ($type) {
|
||||
'function' => $runtimes[$resource->getAttribute('runtime')] ?? null,
|
||||
'site' => $runtimes[$resource->getAttribute('buildRuntime')] ?? null,
|
||||
default => null
|
||||
};
|
||||
|
||||
// Static site enforced runtime
|
||||
@@ -394,7 +398,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
'projectId' => $project->getId(),
|
||||
'scopes' => $resource->getAttribute('scopes', [])
|
||||
]);
|
||||
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $jwtKey;
|
||||
$headers['x-appwrite-key'] = API_KEY_EPHEMERAL . '_' . $jwtKey;
|
||||
$headers['x-appwrite-trigger'] = 'http';
|
||||
$headers['x-appwrite-user-jwt'] = '';
|
||||
|
||||
@@ -459,10 +463,10 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
// V2 vars
|
||||
if ($version === 'v2') {
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
|
||||
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'],
|
||||
'APPWRITE_FUNCTION_DATA' => $body,
|
||||
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
|
||||
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
|
||||
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'],
|
||||
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt']
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -678,9 +682,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
|
||||
if (\is_string($logs) && \strlen($logs) > $maxLogLength) {
|
||||
$warningMessage = "[WARNING] Logs truncated. The output exceeded {$maxLogLength} characters.\n";
|
||||
$warningLength = \strlen($warningMessage);
|
||||
$maxContentLength = max(0, $maxLogLength - $warningLength);
|
||||
$logs = $warningMessage . ($maxContentLength > 0 ? \substr($logs, -$maxContentLength) : '');
|
||||
$maxContentLength = $maxLogLength - \strlen($warningMessage);
|
||||
$logs = $warningMessage . \substr($logs, -$maxContentLength);
|
||||
}
|
||||
|
||||
// Truncate errors if they exceed the limit
|
||||
@@ -689,9 +692,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
|
||||
if (\is_string($errors) && \strlen($errors) > $maxErrorLength) {
|
||||
$warningMessage = "[WARNING] Errors truncated. The output exceeded {$maxErrorLength} characters.\n";
|
||||
$warningLength = \strlen($warningMessage);
|
||||
$maxContentLength = max(0, $maxErrorLength - $warningLength);
|
||||
$errors = $warningMessage . ($maxContentLength > 0 ? \substr($errors, -$maxContentLength) : '');
|
||||
$maxContentLength = $maxErrorLength - \strlen($warningMessage);
|
||||
$errors = $warningMessage . \substr($errors, -$maxContentLength);
|
||||
}
|
||||
/** Update execution status */
|
||||
$status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed';
|
||||
@@ -719,14 +721,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
throw $th;
|
||||
}
|
||||
} finally {
|
||||
if ($type === 'function' || $type === 'site') {
|
||||
$bus->dispatch(new ExecutionCompleted(
|
||||
execution: $execution->getArrayCopy(),
|
||||
project: $project->getArrayCopy(),
|
||||
spec: $spec,
|
||||
resource: $resource->getArrayCopy(),
|
||||
));
|
||||
}
|
||||
$bus->dispatch(new ExecutionCompleted(
|
||||
execution: $execution->getArrayCopy(),
|
||||
project: $project->getArrayCopy(),
|
||||
spec: $spec,
|
||||
resource: $resource->getArrayCopy(),
|
||||
));
|
||||
}
|
||||
|
||||
$execution->setAttribute('logs', '');
|
||||
@@ -848,15 +848,16 @@ Http::init()
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
->inject('lock')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount, Lock $lock) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
$hostname = $request->getHostname() ?? '';
|
||||
$hostname = $request->getHostname();
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
// Only run Router when external domain
|
||||
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount, $lock)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -897,6 +898,12 @@ Http::init()
|
||||
if (version_compare($requestFormat, '1.9.1', '<')) {
|
||||
$request->addFilter(new RequestV22());
|
||||
}
|
||||
if (version_compare($requestFormat, '1.9.2', '<')) {
|
||||
$request->addFilter(new RequestV23());
|
||||
}
|
||||
if (version_compare($requestFormat, '1.9.3', '<')) {
|
||||
$request->addFilter(new RequestV24());
|
||||
}
|
||||
}
|
||||
|
||||
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
|
||||
@@ -921,6 +928,12 @@ Http::init()
|
||||
*/
|
||||
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
if ($responseFormat) {
|
||||
if (version_compare($responseFormat, '1.9.3', '<')) {
|
||||
$response->addFilter(new ResponseV24());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.9.2', '<')) {
|
||||
$response->addFilter(new ResponseV23());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.9.1', '<')) {
|
||||
$response->addFilter(new ResponseV22());
|
||||
}
|
||||
@@ -1014,11 +1027,11 @@ Http::init()
|
||||
->inject('request')
|
||||
->inject('console')
|
||||
->inject('dbForPlatform')
|
||||
->inject('queueForCertificates')
|
||||
->inject('publisherForCertificates')
|
||||
->inject('platform')
|
||||
->inject('authorization')
|
||||
->inject('certifiedDomains')
|
||||
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) {
|
||||
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $publisherForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) {
|
||||
$hostname = $request->getHostname();
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
|
||||
@@ -1044,7 +1057,7 @@ Http::init()
|
||||
}
|
||||
|
||||
// 4. Check/create rule (requires DB access)
|
||||
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, $certifiedDomains) {
|
||||
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $publisherForCertificates, $certifiedDomains) {
|
||||
try {
|
||||
// TODO: (@Meldiron) Remove after 1.7.x migration
|
||||
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
|
||||
@@ -1100,10 +1113,11 @@ Http::init()
|
||||
$dbForPlatform->createDocument('rules', $document);
|
||||
|
||||
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
|
||||
$queueForCertificates
|
||||
->setDomain($document)
|
||||
->setSkipRenewCheck(true)
|
||||
->trigger();
|
||||
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
|
||||
project: $console,
|
||||
domain: $document,
|
||||
skipRenewCheck: true,
|
||||
));
|
||||
} catch (Duplicate $e) {
|
||||
Console::info('Certificate already exists');
|
||||
} finally {
|
||||
@@ -1134,14 +1148,15 @@ Http::options()
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
->inject('lock')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount, Lock $lock) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
// Only run Router when external domain
|
||||
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount, $lock)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -1498,9 +1513,9 @@ Http::error()
|
||||
->setParam('development', Http::isDevelopment())
|
||||
->setParam('projectName', $project->getAttribute('name'))
|
||||
->setParam('projectURL', $project->getAttribute('url'))
|
||||
->setParam('message', $output['message'] ?? '')
|
||||
->setParam('type', $output['type'] ?? '')
|
||||
->setParam('code', $output['code'] ?? '')
|
||||
->setParam('message', $output['message'])
|
||||
->setParam('type', $output['type'])
|
||||
->setParam('code', $output['code'])
|
||||
->setParam('trace', $output['trace'] ?? [])
|
||||
->setParam('exception', $error);
|
||||
|
||||
@@ -1536,13 +1551,14 @@ Http::get('/robots.txt')
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
->inject('lock')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount, Lock $lock) {
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
$template = new View(__DIR__ . '/../views/general/robots.phtml');
|
||||
$response->text($template->render(false));
|
||||
} else {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount, $lock)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -1570,13 +1586,14 @@ Http::get('/humans.txt')
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
->inject('lock')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount, Lock $lock) {
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
$template = new View(__DIR__ . '/../views/general/humans.phtml');
|
||||
$response->text($template->render(false));
|
||||
} else {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount, $lock)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -1615,7 +1632,7 @@ Http::get('/.well-known/acme-challenge/*')
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND, 'Unknown path');
|
||||
}
|
||||
|
||||
if (!\substr($absolute, 0, \strlen($base)) === $base) {
|
||||
if (\substr($absolute, 0, \strlen($base)) !== $base) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_UNAUTHORIZED_SCOPE, 'Invalid path');
|
||||
}
|
||||
|
||||
@@ -1694,7 +1711,7 @@ Http::get('/_appwrite/authorize')
|
||||
->inject('previewHostname')
|
||||
->action(function (Request $request, Response $response, string $previewHostname) {
|
||||
|
||||
$host = $request->getHostname() ?? '';
|
||||
$host = $request->getHostname();
|
||||
if (!empty($previewHostname)) {
|
||||
$host = $previewHostname;
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ Http::get('/v1/mock/github/callback')
|
||||
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
|
||||
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
|
||||
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
|
||||
$owner = $github->getOwnerName($providerInstallationId) ?? '';
|
||||
$owner = $github->getOwnerName($providerInstallationId);
|
||||
|
||||
$projectInternalId = $project->getSequence();
|
||||
|
||||
|
||||
@@ -3,21 +3,24 @@
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Auth\MFA\Type\TOTP;
|
||||
use Appwrite\Bus\Events\RequestCompleted;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Context\Audit as AuditContext;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Message\Audit as AuditMessage;
|
||||
use Appwrite\Event\Message\Usage as UsageMessage;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Publisher\Audit;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\Functions\EventProcessor;
|
||||
use Appwrite\Locking\Lock;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
@@ -42,9 +45,9 @@ use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter as Telemetry;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
$parseLabel = function (string $label, array $responsePayload, array $requestParams, User $user) {
|
||||
$parseLabel = function (string $label, array $responsePayload, array $requestParams, User $user, Document $project) {
|
||||
preg_match_all('/{(.*?)}/', $label, $matches);
|
||||
foreach ($matches[1] ?? [] as $pos => $match) {
|
||||
foreach ($matches[1] as $pos => $match) {
|
||||
$find = $matches[0][$pos];
|
||||
$parts = explode('.', $match);
|
||||
|
||||
@@ -52,11 +55,12 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, "The server encountered an error while parsing the label: $label. Please create an issue on GitHub to allow us to investigate further https://github.com/appwrite/appwrite/issues/new/choose");
|
||||
}
|
||||
|
||||
$namespace = $parts[0] ?? '';
|
||||
$replace = $parts[1] ?? '';
|
||||
$namespace = $parts[0];
|
||||
$replace = $parts[1];
|
||||
|
||||
$params = match ($namespace) {
|
||||
'user' => (array) $user,
|
||||
'project' => $project->getArrayCopy(),
|
||||
'request' => $requestParams,
|
||||
default => $responsePayload,
|
||||
};
|
||||
@@ -88,7 +92,7 @@ Http::init()
|
||||
->inject('request')
|
||||
->inject('dbForPlatform')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForAudits')
|
||||
->inject('auditContext')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('session')
|
||||
@@ -97,7 +101,8 @@ Http::init()
|
||||
->inject('team')
|
||||
->inject('apiKey')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
|
||||
->inject('lock')
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization, Lock $lock) {
|
||||
$route = $utopia->getRoute();
|
||||
if ($route === null) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
|
||||
@@ -180,7 +185,8 @@ Http::init()
|
||||
// Handle special app role case
|
||||
if ($apiKey->getRole() === User::ROLE_APPS) {
|
||||
// Disable authorization checks for project API keys
|
||||
if (($apiKey->getType() === API_KEY_STANDARD || $apiKey->getType() === API_KEY_DYNAMIC) && $apiKey->getProjectId() === $project->getId()) {
|
||||
// Dynamic supported for backwards compatibility
|
||||
if (($apiKey->getType() === API_KEY_STANDARD || $apiKey->getType() === API_KEY_EPHEMERAL || $apiKey->getType() === 'dynamic') && $apiKey->getProjectId() === $project->getId()) {
|
||||
$authorization->setDefaultStatus(false);
|
||||
}
|
||||
|
||||
@@ -193,7 +199,7 @@ Http::init()
|
||||
'name' => $apiKey->getName(),
|
||||
]);
|
||||
|
||||
$queueForAudits->setUser($user);
|
||||
$auditContext->user = $user;
|
||||
}
|
||||
|
||||
// For standard keys, update last accessed time
|
||||
@@ -241,30 +247,31 @@ Http::init()
|
||||
$sdks[] = $sdk;
|
||||
|
||||
$updates->setAttribute('sdks', $sdks);
|
||||
$updates->setAttribute('accessedAt', Datetime::now());
|
||||
$updates->setAttribute('accessedAt', DateTime::now());
|
||||
}
|
||||
}
|
||||
|
||||
if (! $updates->isEmpty()) {
|
||||
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->updateDocument('keys', $dbKey->getId(), $updates));
|
||||
$lock->run('keys', $dbKey->getId(), function () use ($dbForPlatform, $dbKey, $updates, $apiKey, $project, $user, $team) {
|
||||
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->updateDocument('keys', $dbKey->getId(), $updates));
|
||||
|
||||
if (! empty($apiKey->getProjectId())) {
|
||||
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
} elseif (! empty($apiKey->getUserId())) {
|
||||
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('users', $user->getId()));
|
||||
} elseif (! empty($apiKey->getTeamId())) {
|
||||
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('teams', $team->getId()));
|
||||
}
|
||||
if (! empty($apiKey->getProjectId())) {
|
||||
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
} elseif (! empty($apiKey->getUserId())) {
|
||||
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('users', $user->getId()));
|
||||
} elseif (! empty($apiKey->getTeamId())) {
|
||||
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('teams', $team->getId()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$userClone = clone $user;
|
||||
$userClone->setAttribute('type', match ($apiKey->getType()) {
|
||||
API_KEY_STANDARD => ACTIVITY_TYPE_KEY_PROJECT,
|
||||
API_KEY_ACCOUNT => ACTIVITY_TYPE_KEY_ACCOUNT,
|
||||
API_KEY_ORGANIZATION => ACTIVITY_TYPE_KEY_ORGANIZATION,
|
||||
default => ACTIVITY_TYPE_KEY_PROJECT,
|
||||
default => ACTIVITY_TYPE_KEY_ORGANIZATION,
|
||||
});
|
||||
$queueForAudits->setUser($userClone);
|
||||
$auditContext->user = $userClone;
|
||||
}
|
||||
|
||||
// Apply permission
|
||||
@@ -383,12 +390,10 @@ Http::init()
|
||||
}
|
||||
|
||||
// Step 6: Update project and user last activity
|
||||
if (! $project->isEmpty() && $project->getId() !== 'console') {
|
||||
if ($project->getId() !== 'console') {
|
||||
$accessedAt = $project->getAttribute('accessedAt', 0);
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
|
||||
'accessedAt' => DateTime::now()
|
||||
])));
|
||||
$lock->set('projects', $project->getId(), 'accessedAt', DateTime::now());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,17 +410,12 @@ Http::init()
|
||||
'accessedAt' => $user->getAttribute('accessedAt')
|
||||
]));
|
||||
} else {
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), new Document([
|
||||
'accessedAt' => $user->getAttribute('accessedAt')
|
||||
])));
|
||||
$lock->set('users', $user->getId(), 'accessedAt', $user->getAttribute('accessedAt'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Steps 7-9: Access Control - Method, Namespace and Scope Validation
|
||||
/**
|
||||
* @var ?Method $method
|
||||
*/
|
||||
$method = $route->getLabel('sdk', false);
|
||||
|
||||
// Take the first method if there's more than one,
|
||||
@@ -486,7 +486,7 @@ Http::init()
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMessaging')
|
||||
->inject('queueForAudits')
|
||||
->inject('auditContext')
|
||||
->inject('queueForDeletes')
|
||||
->inject('queueForDatabase')
|
||||
->inject('queueForBuilds')
|
||||
@@ -503,7 +503,7 @@ Http::init()
|
||||
->inject('telemetry')
|
||||
->inject('platform')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
|
||||
|
||||
$response->setUser($user);
|
||||
$request->setUser($user);
|
||||
@@ -596,13 +596,12 @@ Http::init()
|
||||
->setProject($project)
|
||||
->setUser($user);
|
||||
|
||||
$queueForAudits
|
||||
->setMode($mode)
|
||||
->setUserAgent($request->getUserAgent(''))
|
||||
->setIP($request->getIP())
|
||||
->setHostname($request->getHostname())
|
||||
->setEvent($route->getLabel('audits.event', ''))
|
||||
->setProject($project);
|
||||
$auditContext->mode = $mode;
|
||||
$auditContext->userAgent = $request->getUserAgent('');
|
||||
$auditContext->ip = $request->getIP();
|
||||
$auditContext->hostname = $request->getHostname();
|
||||
$auditContext->event = $route->getLabel('audits.event', '');
|
||||
$auditContext->project = $project;
|
||||
|
||||
/* If a session exists, use the user associated with the session */
|
||||
if (! $user->isEmpty()) {
|
||||
@@ -611,7 +610,7 @@ Http::init()
|
||||
if (empty($user->getAttribute('type'))) {
|
||||
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
|
||||
}
|
||||
$queueForAudits->setUser($userClone);
|
||||
$auditContext->user = $userClone;
|
||||
}
|
||||
|
||||
/* Auto-set projects */
|
||||
@@ -645,7 +644,7 @@ Http::init()
|
||||
|
||||
if (! empty($data) && ! $cacheLog->isEmpty()) {
|
||||
$parts = explode('/', $cacheLog->getAttribute('resourceType', ''));
|
||||
$type = $parts[0] ?? null;
|
||||
$type = $parts[0];
|
||||
|
||||
if ($type === 'bucket' && (! $isImageTransformation || ! $isDisabled)) {
|
||||
$bucketId = $parts[1] ?? null;
|
||||
@@ -756,7 +755,12 @@ Http::shutdown()
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, Database $dbForProject) {
|
||||
$sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT;
|
||||
$sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? 0;
|
||||
|
||||
if ($sessionLimit === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$session = $response->getPayload();
|
||||
$userId = $session['userId'] ?? '';
|
||||
if (empty($userId)) {
|
||||
@@ -790,7 +794,8 @@ Http::shutdown()
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForAudits')
|
||||
->inject('auditContext')
|
||||
->inject('publisherForAudits')
|
||||
->inject('usage')
|
||||
->inject('publisherForUsage')
|
||||
->inject('queueForDeletes')
|
||||
@@ -807,7 +812,7 @@ Http::shutdown()
|
||||
->inject('bus')
|
||||
->inject('apiKey')
|
||||
->inject('mode')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
|
||||
|
||||
$responsePayload = $response->getPayload();
|
||||
|
||||
@@ -900,9 +905,9 @@ Http::shutdown()
|
||||
*/
|
||||
$pattern = $route->getLabel('audits.resource', null);
|
||||
if (! empty($pattern)) {
|
||||
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
|
||||
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user, $project);
|
||||
if (! empty($resource) && $resource !== $pattern) {
|
||||
$queueForAudits->setResource($resource);
|
||||
$auditContext->resource = $resource;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -912,8 +917,8 @@ Http::shutdown()
|
||||
if (empty($user->getAttribute('type'))) {
|
||||
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
|
||||
}
|
||||
$queueForAudits->setUser($userClone);
|
||||
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
|
||||
$auditContext->user = $userClone;
|
||||
} elseif ($auditContext->user === null || $auditContext->user->isEmpty()) {
|
||||
/**
|
||||
* User in the request is empty, and no user was set for auditing previously.
|
||||
* This indicates:
|
||||
@@ -931,24 +936,21 @@ Http::shutdown()
|
||||
'name' => 'Guest',
|
||||
]);
|
||||
|
||||
$queueForAudits->setUser($user);
|
||||
$auditContext->user = $user;
|
||||
}
|
||||
|
||||
if (! empty($queueForAudits->getResource()) && ! $queueForAudits->getUser()->isEmpty()) {
|
||||
$auditUser = $auditContext->user;
|
||||
if (! empty($auditContext->resource) && ! $auditUser->isEmpty()) {
|
||||
/**
|
||||
* audits.payload is switched to default true
|
||||
* in order to auto audit payload for all endpoints
|
||||
*/
|
||||
$pattern = $route->getLabel('audits.payload', true);
|
||||
if (! empty($pattern)) {
|
||||
$queueForAudits->setPayload($responsePayload);
|
||||
$auditContext->payload = $responsePayload;
|
||||
}
|
||||
|
||||
foreach ($queueForEvents->getParams() as $key => $value) {
|
||||
$queueForAudits->setParam($key, $value);
|
||||
}
|
||||
|
||||
$queueForAudits->trigger();
|
||||
$publisherForAudits->enqueue(AuditMessage::fromContext($auditContext));
|
||||
}
|
||||
|
||||
if (! empty($queueForDeletes->getType())) {
|
||||
@@ -972,15 +974,16 @@ Http::shutdown()
|
||||
if ($useCache) {
|
||||
$resource = $resourceType = null;
|
||||
$data = $response->getPayload();
|
||||
if (! empty($data['payload'])) {
|
||||
$statusCode = $response->getStatusCode();
|
||||
if (! empty($data['payload']) && $statusCode >= 200 && $statusCode < 300) {
|
||||
$pattern = $route->getLabel('cache.resource', null);
|
||||
if (! empty($pattern)) {
|
||||
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
|
||||
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user, $project);
|
||||
}
|
||||
|
||||
$pattern = $route->getLabel('cache.resourceType', null);
|
||||
if (! empty($pattern)) {
|
||||
$resourceType = $parseLabel($pattern, $responsePayload, $requestParams, $user);
|
||||
$resourceType = $parseLabel($pattern, $responsePayload, $requestParams, $user, $project);
|
||||
}
|
||||
|
||||
$cache = new Cache(
|
||||
|
||||
+7
-16
@@ -72,8 +72,6 @@ $swooleAdapter = new Server(
|
||||
container: $container,
|
||||
);
|
||||
|
||||
$container->set('container', fn () => fn () => $swooleAdapter->getContainer());
|
||||
|
||||
$http = $swooleAdapter->getServer();
|
||||
|
||||
/**
|
||||
@@ -415,27 +413,19 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
|
||||
$projectCollections = $collections['projects'];
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
$sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
|
||||
$sharedTablesV2 = \array_diff($sharedTables, $sharedTablesV1);
|
||||
|
||||
$documentsSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
|
||||
$documentsSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', ''));
|
||||
$documentsSharedTablesV2 = \array_diff($documentsSharedTables, $documentsSharedTablesV1);
|
||||
|
||||
$vectorSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
|
||||
$vectorSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', ''));
|
||||
$vectorSharedTablesV2 = \array_diff($vectorSharedTables, $vectorSharedTablesV1);
|
||||
|
||||
$cache = $app->getResource('cache');
|
||||
|
||||
// All shared tables V2 pools that need project metadata collections
|
||||
$sharedTablesV2All = \array_values(\array_unique(\array_filter([
|
||||
...$sharedTablesV2,
|
||||
...$documentsSharedTablesV2,
|
||||
...$vectorSharedTablesV2,
|
||||
// All shared tables pools that need project metadata collections
|
||||
$allSharedTables = \array_values(\array_unique(\array_filter([
|
||||
...$sharedTables,
|
||||
...$documentsSharedTables,
|
||||
...$vectorSharedTables,
|
||||
])));
|
||||
|
||||
foreach ($sharedTablesV2All as $hostname) {
|
||||
foreach ($allSharedTables as $hostname) {
|
||||
Span::init('database.setup');
|
||||
Span::add('database.hostname', $hostname);
|
||||
|
||||
@@ -533,6 +523,7 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
|
||||
}
|
||||
|
||||
$requestContainer = $swooleAdapter->getContainer();
|
||||
$requestContainer->set('container', fn () => $requestContainer);
|
||||
$requestContainer->set('request', fn () => $request);
|
||||
$requestContainer->set('response', fn () => $response);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Platform\Modules\Compute\Specification;
|
||||
use Utopia\System\System;
|
||||
|
||||
const APP_NAME = 'Appwrite';
|
||||
const APP_DOMAIN = 'appwrite.io';
|
||||
@@ -24,9 +25,6 @@ const APP_MODE_ADMIN = 'admin';
|
||||
const APP_PAGING_LIMIT = 12;
|
||||
const APP_LIMIT_COUNT = 5000;
|
||||
const APP_LIMIT_USERS = 10_000;
|
||||
const APP_LIMIT_USER_PASSWORD_HISTORY = 20;
|
||||
const APP_LIMIT_USER_SESSIONS_MAX = 100;
|
||||
const APP_LIMIT_USER_SESSIONS_DEFAULT = 10;
|
||||
const APP_LIMIT_ANTIVIRUS = 20_000_000; //20MB
|
||||
const APP_LIMIT_ENCRYPTION = 20_000_000; //20MB
|
||||
const APP_LIMIT_COMPRESSION = 20_000_000; //20MB
|
||||
@@ -46,8 +44,8 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_BUSTER = 4322;
|
||||
const APP_VERSION_STABLE = '1.9.1';
|
||||
const APP_CACHE_BUSTER = 4324;
|
||||
const APP_VERSION_STABLE = '1.9.3';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
|
||||
@@ -190,7 +188,7 @@ const BUILD_TYPE_RETRY = 'retry';
|
||||
|
||||
// Deletion Types
|
||||
|
||||
const ENABLE_EXECUTIONS_LIMIT_ON_ROUTE = false;
|
||||
\define('ENABLE_EXECUTIONS_LIMIT_ON_ROUTE', System::getEnv('_APP_EXECUTIONS_LIMIT_ON_ROUTE', 'disabled') === 'enabled');
|
||||
|
||||
const DELETE_TYPE_DATABASES = 'databases';
|
||||
const DELETE_TYPE_DOCUMENT = 'document';
|
||||
@@ -246,6 +244,7 @@ const APP_AUTH_TYPE_KEY = 'Key';
|
||||
const APP_AUTH_TYPE_ADMIN = 'Admin';
|
||||
// Response related
|
||||
const MAX_OUTPUT_CHUNK_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
const APP_LIMIT_UPLOAD_CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const APP_FUNCTION_LOG_LENGTH_LIMIT = 1000000;
|
||||
const APP_FUNCTION_ERROR_LENGTH_LIMIT = 1000000;
|
||||
// Function headers
|
||||
@@ -257,7 +256,7 @@ const MESSAGE_TYPE_SMS = 'sms';
|
||||
const MESSAGE_TYPE_PUSH = 'push';
|
||||
// API key types
|
||||
const API_KEY_STANDARD = 'standard';
|
||||
const API_KEY_DYNAMIC = 'dynamic';
|
||||
const API_KEY_EPHEMERAL = 'ephemeral';
|
||||
const API_KEY_ORGANIZATION = 'organization';
|
||||
const API_KEY_ACCOUNT = 'account';
|
||||
// Usage metrics
|
||||
|
||||
+120
-4
@@ -56,6 +56,11 @@ use Appwrite\Utopia\Response\Model\ColumnString;
|
||||
use Appwrite\Utopia\Response\Model\ColumnText;
|
||||
use Appwrite\Utopia\Response\Model\ColumnURL;
|
||||
use Appwrite\Utopia\Response\Model\ColumnVarchar;
|
||||
use Appwrite\Utopia\Response\Model\ConsoleKeyScope;
|
||||
use Appwrite\Utopia\Response\Model\ConsoleKeyScopeList;
|
||||
use Appwrite\Utopia\Response\Model\ConsoleOAuth2Provider;
|
||||
use Appwrite\Utopia\Response\Model\ConsoleOAuth2ProviderList;
|
||||
use Appwrite\Utopia\Response\Model\ConsoleOAuth2ProviderParameter;
|
||||
use Appwrite\Utopia\Response\Model\ConsoleVariables;
|
||||
use Appwrite\Utopia\Response\Model\Continent;
|
||||
use Appwrite\Utopia\Response\Model\Country;
|
||||
@@ -68,6 +73,7 @@ use Appwrite\Utopia\Response\Model\DetectionVariable;
|
||||
use Appwrite\Utopia\Response\Model\DevKey;
|
||||
use Appwrite\Utopia\Response\Model\Document as ModelDocument;
|
||||
use Appwrite\Utopia\Response\Model\Embedding;
|
||||
use Appwrite\Utopia\Response\Model\EphemeralKey;
|
||||
use Appwrite\Utopia\Response\Model\Error;
|
||||
use Appwrite\Utopia\Response\Model\ErrorDev;
|
||||
use Appwrite\Utopia\Response\Model\Execution;
|
||||
@@ -105,6 +111,47 @@ use Appwrite\Utopia\Response\Model\MigrationReport;
|
||||
use Appwrite\Utopia\Response\Model\Mock;
|
||||
use Appwrite\Utopia\Response\Model\MockNumber;
|
||||
use Appwrite\Utopia\Response\Model\None;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Amazon;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Apple;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Auth0;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Authentik;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Autodesk;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Bitbucket;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Bitly;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Box;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Dailymotion;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Discord;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Disqus;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Dropbox;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Etsy;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Facebook;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Figma;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2FusionAuth;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2GitHub;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Gitlab;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Google;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Keycloak;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Kick;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Linkedin;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Microsoft;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Notion;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Oidc;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Okta;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Paypal;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Podio;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2ProviderList;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Salesforce;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Slack;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Spotify;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Stripe;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Tradeshift;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Twitch;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2WordPress;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2X;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Yahoo;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Yandex;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Zoho;
|
||||
use Appwrite\Utopia\Response\Model\OAuth2Zoom;
|
||||
use Appwrite\Utopia\Response\Model\Phone;
|
||||
use Appwrite\Utopia\Response\Model\PlatformAndroid;
|
||||
use Appwrite\Utopia\Response\Model\PlatformApple;
|
||||
@@ -112,12 +159,24 @@ use Appwrite\Utopia\Response\Model\PlatformLinux;
|
||||
use Appwrite\Utopia\Response\Model\PlatformList;
|
||||
use Appwrite\Utopia\Response\Model\PlatformWeb;
|
||||
use Appwrite\Utopia\Response\Model\PlatformWindows;
|
||||
use Appwrite\Utopia\Response\Model\PolicyList;
|
||||
use Appwrite\Utopia\Response\Model\PolicyMembershipPrivacy;
|
||||
use Appwrite\Utopia\Response\Model\PolicyPasswordDictionary;
|
||||
use Appwrite\Utopia\Response\Model\PolicyPasswordHistory;
|
||||
use Appwrite\Utopia\Response\Model\PolicyPasswordPersonalData;
|
||||
use Appwrite\Utopia\Response\Model\PolicySessionAlert;
|
||||
use Appwrite\Utopia\Response\Model\PolicySessionDuration;
|
||||
use Appwrite\Utopia\Response\Model\PolicySessionInvalidation;
|
||||
use Appwrite\Utopia\Response\Model\PolicySessionLimit;
|
||||
use Appwrite\Utopia\Response\Model\PolicyUserLimit;
|
||||
use Appwrite\Utopia\Response\Model\Preferences;
|
||||
use Appwrite\Utopia\Response\Model\Project;
|
||||
use Appwrite\Utopia\Response\Model\Provider;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepository;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryFrameworkList;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntimeList;
|
||||
use Appwrite\Utopia\Response\Model\ResourceToken;
|
||||
use Appwrite\Utopia\Response\Model\Row;
|
||||
use Appwrite\Utopia\Response\Model\Rule;
|
||||
@@ -135,7 +194,6 @@ use Appwrite\Utopia\Response\Model\TemplateFramework;
|
||||
use Appwrite\Utopia\Response\Model\TemplateFunction;
|
||||
use Appwrite\Utopia\Response\Model\TemplateRuntime;
|
||||
use Appwrite\Utopia\Response\Model\TemplateSite;
|
||||
use Appwrite\Utopia\Response\Model\TemplateSMS;
|
||||
use Appwrite\Utopia\Response\Model\TemplateVariable;
|
||||
use Appwrite\Utopia\Response\Model\Token;
|
||||
use Appwrite\Utopia\Response\Model\Topic;
|
||||
@@ -190,8 +248,8 @@ Response::setModel(new BaseList('Site Templates List', Response::MODEL_TEMPLATE_
|
||||
Response::setModel(new BaseList('Functions List', Response::MODEL_FUNCTION_LIST, 'functions', Response::MODEL_FUNCTION));
|
||||
Response::setModel(new BaseList('Function Templates List', Response::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', Response::MODEL_TEMPLATE_FUNCTION));
|
||||
Response::setModel(new BaseList('Installations List', Response::MODEL_INSTALLATION_LIST, 'installations', Response::MODEL_INSTALLATION));
|
||||
Response::setModel(new BaseList('Framework Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, 'frameworkProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK));
|
||||
Response::setModel(new BaseList('Runtime Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, 'runtimeProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME));
|
||||
Response::setModel(new ProviderRepositoryFrameworkList());
|
||||
Response::setModel(new ProviderRepositoryRuntimeList());
|
||||
Response::setModel(new BaseList('Branches List', Response::MODEL_BRANCH_LIST, 'branches', Response::MODEL_BRANCH));
|
||||
Response::setModel(new BaseList('Frameworks List', Response::MODEL_FRAMEWORK_LIST, 'frameworks', Response::MODEL_FRAMEWORK));
|
||||
Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, 'runtimes', Response::MODEL_RUNTIME));
|
||||
@@ -209,6 +267,9 @@ Response::setModel(new BaseList('Currencies List', Response::MODEL_CURRENCY_LIST
|
||||
Response::setModel(new BaseList('Phones List', Response::MODEL_PHONE_LIST, 'phones', Response::MODEL_PHONE));
|
||||
Response::setModel(new BaseList('Metric List', Response::MODEL_METRIC_LIST, 'metrics', Response::MODEL_METRIC, true, false));
|
||||
Response::setModel(new BaseList('Variables List', Response::MODEL_VARIABLE_LIST, 'variables', Response::MODEL_VARIABLE));
|
||||
Response::setModel(new BaseList('Mock Numbers List', Response::MODEL_MOCK_NUMBER_LIST, 'mockNumbers', Response::MODEL_MOCK_NUMBER));
|
||||
Response::setModel(new PolicyList());
|
||||
Response::setModel(new BaseList('Email Templates List', Response::MODEL_EMAIL_TEMPLATE_LIST, 'templates', Response::MODEL_EMAIL_TEMPLATE));
|
||||
Response::setModel(new BaseList('Status List', Response::MODEL_HEALTH_STATUS_LIST, 'statuses', Response::MODEL_HEALTH_STATUS));
|
||||
Response::setModel(new BaseList('Rule List', Response::MODEL_PROXY_RULE_LIST, 'rules', Response::MODEL_PROXY_RULE));
|
||||
Response::setModel(new BaseList('Schedules List', Response::MODEL_SCHEDULE_LIST, 'schedules', Response::MODEL_SCHEDULE));
|
||||
@@ -334,8 +395,59 @@ Response::setModel(new Execution());
|
||||
Response::setModel(new Project());
|
||||
Response::setModel(new Webhook());
|
||||
Response::setModel(new Key());
|
||||
Response::setModel(new EphemeralKey());
|
||||
Response::setModel(new DevKey());
|
||||
Response::setModel(new MockNumber());
|
||||
Response::setModel(new OAuth2GitHub());
|
||||
Response::setModel(new OAuth2Discord());
|
||||
Response::setModel(new OAuth2Figma());
|
||||
Response::setModel(new OAuth2Dropbox());
|
||||
Response::setModel(new OAuth2Dailymotion());
|
||||
Response::setModel(new OAuth2Bitbucket());
|
||||
Response::setModel(new OAuth2Bitly());
|
||||
Response::setModel(new OAuth2Box());
|
||||
Response::setModel(new OAuth2Autodesk());
|
||||
Response::setModel(new OAuth2Google());
|
||||
Response::setModel(new OAuth2Zoom());
|
||||
Response::setModel(new OAuth2Zoho());
|
||||
Response::setModel(new OAuth2Yandex());
|
||||
Response::setModel(new OAuth2X());
|
||||
Response::setModel(new OAuth2WordPress());
|
||||
Response::setModel(new OAuth2Twitch());
|
||||
Response::setModel(new OAuth2Stripe());
|
||||
Response::setModel(new OAuth2Spotify());
|
||||
Response::setModel(new OAuth2Slack());
|
||||
Response::setModel(new OAuth2Podio());
|
||||
Response::setModel(new OAuth2Notion());
|
||||
Response::setModel(new OAuth2Salesforce());
|
||||
Response::setModel(new OAuth2Yahoo());
|
||||
Response::setModel(new OAuth2Linkedin());
|
||||
Response::setModel(new OAuth2Disqus());
|
||||
Response::setModel(new OAuth2Amazon());
|
||||
Response::setModel(new OAuth2Etsy());
|
||||
Response::setModel(new OAuth2Facebook());
|
||||
Response::setModel(new OAuth2Tradeshift());
|
||||
Response::setModel(new OAuth2Paypal());
|
||||
Response::setModel(new OAuth2Gitlab());
|
||||
Response::setModel(new OAuth2Authentik());
|
||||
Response::setModel(new OAuth2Auth0());
|
||||
Response::setModel(new OAuth2FusionAuth());
|
||||
Response::setModel(new OAuth2Keycloak());
|
||||
Response::setModel(new OAuth2Oidc());
|
||||
Response::setModel(new OAuth2Okta());
|
||||
Response::setModel(new OAuth2Kick());
|
||||
Response::setModel(new OAuth2Apple());
|
||||
Response::setModel(new OAuth2Microsoft());
|
||||
Response::setModel(new OAuth2ProviderList());
|
||||
Response::setModel(new PolicyPasswordDictionary());
|
||||
Response::setModel(new PolicyPasswordHistory());
|
||||
Response::setModel(new PolicyPasswordPersonalData());
|
||||
Response::setModel(new PolicySessionAlert());
|
||||
Response::setModel(new PolicySessionDuration());
|
||||
Response::setModel(new PolicySessionInvalidation());
|
||||
Response::setModel(new PolicySessionLimit());
|
||||
Response::setModel(new PolicyUserLimit());
|
||||
Response::setModel(new PolicyMembershipPrivacy());
|
||||
Response::setModel(new AuthProvider());
|
||||
Response::setModel(new PlatformWeb());
|
||||
Response::setModel(new PlatformApple());
|
||||
@@ -373,9 +485,13 @@ Response::setModel(new Headers());
|
||||
Response::setModel(new Specification());
|
||||
Response::setModel(new Rule());
|
||||
Response::setModel(new Schedule());
|
||||
Response::setModel(new TemplateSMS());
|
||||
Response::setModel(new TemplateEmail());
|
||||
Response::setModel(new ConsoleVariables());
|
||||
Response::setModel(new ConsoleOAuth2ProviderParameter());
|
||||
Response::setModel(new ConsoleOAuth2Provider());
|
||||
Response::setModel(new ConsoleOAuth2ProviderList());
|
||||
Response::setModel(new ConsoleKeyScope());
|
||||
Response::setModel(new ConsoleKeyScopeList());
|
||||
Response::setModel(new MFAChallenge());
|
||||
Response::setModel(new MFARecoveryCodes());
|
||||
Response::setModel(new MFAType());
|
||||
|
||||
@@ -85,7 +85,7 @@ return function (Container $container): void {
|
||||
|
||||
return $dbForPlatform->findOne('rules', [
|
||||
Query::equal('domain', [$domain]),
|
||||
]) ?? new Document();
|
||||
]);
|
||||
});
|
||||
|
||||
$permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence();
|
||||
@@ -139,7 +139,7 @@ return function (Container $container): void {
|
||||
$sdkValidator = new WhiteList($servers, true);
|
||||
$sdk = \strtolower($request->getHeader('x-sdk-name', 'UNKNOWN'));
|
||||
|
||||
if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) {
|
||||
if ($sdk !== 'unknown' && $sdkValidator->isValid($sdk)) {
|
||||
$sdks = $key->getAttribute('sdks', []);
|
||||
|
||||
if (!\in_array($sdk, $sdks, true)) {
|
||||
@@ -327,9 +327,11 @@ return function (Container $container): void {
|
||||
}
|
||||
}
|
||||
|
||||
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', '');
|
||||
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', '');
|
||||
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', '');
|
||||
// Query params mirror the header fallback pattern used by ?project= and ?devKey=,
|
||||
// allowing Console to embed impersonation in direct file/image URLs where headers cannot be set.
|
||||
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', (string)$request->getParam('impersonateUserId', ''));
|
||||
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', (string)$request->getParam('impersonateEmail', ''));
|
||||
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', (string)$request->getParam('impersonatePhone', ''));
|
||||
|
||||
if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) {
|
||||
$userDb = ($mode === APP_MODE_ADMIN || $project->getId() === 'console') ? $dbForPlatform : $dbForProject;
|
||||
|
||||
@@ -71,7 +71,7 @@ $register->set('logger', function () {
|
||||
|
||||
$providerConfig = match ($providerName) {
|
||||
'sentry' => [ 'key' => $configChunks[0], 'projectId' => $configChunks[1] ?? '', 'host' => '',],
|
||||
'logowl' => ['ticket' => $configChunks[0] ?? '', 'host' => ''],
|
||||
'logowl' => ['ticket' => $configChunks[0], 'host' => ''],
|
||||
default => ['key' => $providerConfig],
|
||||
};
|
||||
}
|
||||
@@ -249,11 +249,11 @@ $register->set('pools', function () {
|
||||
$poolSize = max(1, (int)($instanceConnections / $workerCount));
|
||||
|
||||
foreach ($connections as $key => $connection) {
|
||||
$type = $connection['type'] ?? '';
|
||||
$multiple = $connection['multiple'] ?? false;
|
||||
$schemes = $connection['schemes'] ?? [];
|
||||
$type = $connection['type'];
|
||||
$multiple = $connection['multiple'];
|
||||
$schemes = $connection['schemes'];
|
||||
$config = [];
|
||||
$dsns = explode(',', $connection['dsns'] ?? '');
|
||||
$dsns = explode(',', $connection['dsns']);
|
||||
foreach ($dsns as &$dsn) {
|
||||
$dsn = explode('=', $dsn);
|
||||
$name = ($multiple) ? $key . '_' . $dsn[0] : $key;
|
||||
@@ -318,7 +318,7 @@ $register->set('pools', function () {
|
||||
));
|
||||
});
|
||||
},
|
||||
'redis' => function () use ($dsnHost, $dsnPort, $dsnPass) {
|
||||
default => function () use ($dsnHost, $dsnPort, $dsnPass) {
|
||||
$redis = new \Redis();
|
||||
@$redis->pconnect($dsnHost, (int)$dsnPort);
|
||||
if ($dsnPass) {
|
||||
@@ -328,7 +328,6 @@ $register->set('pools', function () {
|
||||
|
||||
return $redis;
|
||||
},
|
||||
default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Invalid scheme'),
|
||||
};
|
||||
|
||||
$poolAdapter = System::getEnv('_APP_POOL_ADAPTER', default: 'stack') === 'swoole' ? new SwoolePool() : new StackPool();
|
||||
|
||||
+16
-1
@@ -1,8 +1,11 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Publisher\Audit as AuditPublisher;
|
||||
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
|
||||
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
|
||||
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
|
||||
use Appwrite\Event\Publisher\Screenshot as ScreenshotPublisher;
|
||||
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
@@ -81,6 +84,18 @@ $container->set('publisherMessaging', function (Publisher $publisher) {
|
||||
$container->set('publisherWebhooks', function (Publisher $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$container->set('publisherForAudits', fn (Publisher $publisher) => new AuditPublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('publisherForScreenshots', fn (Publisher $publisher) => new ScreenshotPublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
|
||||
@@ -251,7 +266,7 @@ function getDevice(string $root, string $connection = ''): Device
|
||||
return new Local($root);
|
||||
}
|
||||
} else {
|
||||
switch (strtolower(System::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL) ?? '')) {
|
||||
switch (strtolower(System::getEnv('_APP_STORAGE_DEVICE', Storage::DEVICE_LOCAL))) {
|
||||
case Storage::DEVICE_LOCAL:
|
||||
default:
|
||||
return new Local($root);
|
||||
|
||||
@@ -4,9 +4,8 @@ use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\Audit as AuditEvent;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Context\Audit as AuditContext;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
@@ -14,11 +13,11 @@ use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Functions\EventProcessor;
|
||||
use Appwrite\GraphQL\Schema;
|
||||
use Appwrite\Locking\Lock;
|
||||
use Appwrite\Network\Cors;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
@@ -51,6 +50,7 @@ use Utopia\DSN\DSN;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Storage\Device;
|
||||
@@ -75,6 +75,10 @@ return function (Container $container): void {
|
||||
return $register->get('logger');
|
||||
}, ['register']);
|
||||
|
||||
$container->set('lock', function (\Redis $redis, Telemetry $telemetry, Database $dbForPlatform, Authorization $authorization, Log $log, ?Logger $logger, Document $project): Lock {
|
||||
return new Lock($redis, $telemetry, $dbForPlatform, $authorization, $log, $logger, $project);
|
||||
}, ['redis', 'telemetry', 'dbForPlatform', 'authorization', 'log', 'logger', 'project']);
|
||||
|
||||
$container->set('authorization', function () {
|
||||
return new Authorization();
|
||||
}, []);
|
||||
@@ -128,9 +132,6 @@ return function (Container $container): void {
|
||||
$container->set('queueForBuilds', function (Publisher $publisher) {
|
||||
return new Build($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('queueForScreenshots', function (Publisher $publisher) {
|
||||
return new Screenshot($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('queueForDatabase', function (Publisher $publisher) {
|
||||
return new EventDatabase($publisher);
|
||||
}, ['publisher']);
|
||||
@@ -149,18 +150,13 @@ return function (Container $container): void {
|
||||
$container->set('usage', function () {
|
||||
return new UsageContext();
|
||||
}, []);
|
||||
$container->set('queueForAudits', function (Publisher $publisher) {
|
||||
return new AuditEvent($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('auditContext', fn () => new AuditContext(), []);
|
||||
$container->set('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('eventProcessor', function () {
|
||||
return new EventProcessor();
|
||||
}, []);
|
||||
$container->set('queueForCertificates', function (Publisher $publisher) {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
$container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$adapter = new DatabasePool($pools->get('console'));
|
||||
$database = new Database($adapter, $cache);
|
||||
@@ -385,7 +381,7 @@ return function (Container $container): void {
|
||||
|
||||
return $dbForPlatform->findOne('rules', [
|
||||
Query::equal('domain', [$domain]),
|
||||
]) ?? new Document();
|
||||
]);
|
||||
});
|
||||
|
||||
$permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence();
|
||||
@@ -488,14 +484,10 @@ return function (Container $container): void {
|
||||
}
|
||||
|
||||
// Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies
|
||||
if ($response) { // if in http context - add debug header
|
||||
$response->addHeader('X-Debug-Fallback', 'false');
|
||||
}
|
||||
$response->addHeader('X-Debug-Fallback', 'false');
|
||||
|
||||
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
|
||||
if ($response) {
|
||||
$response->addHeader('X-Debug-Fallback', 'true');
|
||||
}
|
||||
$response->addHeader('X-Debug-Fallback', 'true');
|
||||
$fallback = $request->getHeader('x-fallback-cookies', '');
|
||||
$fallback = \json_decode($fallback, true);
|
||||
$store->decode(((is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : ''));
|
||||
@@ -585,10 +577,12 @@ return function (Container $container): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Impersonation: if current user has impersonator capability and headers are set, act as another user
|
||||
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', '');
|
||||
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', '');
|
||||
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', '');
|
||||
// Impersonation: if current user has impersonator capability and headers/params are set, act as another user
|
||||
// Query params mirror the header fallback pattern used by ?project= and ?devKey=,
|
||||
// allowing Console to embed impersonation in direct file/image URLs where headers cannot be set.
|
||||
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', (string)$request->getParam('impersonateUserId', ''));
|
||||
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', (string)$request->getParam('impersonateEmail', ''));
|
||||
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', (string)$request->getParam('impersonatePhone', ''));
|
||||
if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) {
|
||||
$userDb = (APP_MODE_ADMIN === $mode || $project->getId() === 'console') ? $dbForPlatform : $dbForProject;
|
||||
$targetUser = null;
|
||||
@@ -1094,7 +1088,7 @@ return function (Container $container): void {
|
||||
$sdkValidator = new WhiteList($servers, true);
|
||||
$sdk = \strtolower($request->getHeader('x-sdk-name', 'UNKNOWN'));
|
||||
|
||||
if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) {
|
||||
if ($sdk !== 'unknown' && $sdkValidator->isValid($sdk)) {
|
||||
$sdks = $key->getAttribute('sdks', []);
|
||||
|
||||
if (! in_array($sdk, $sdks)) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
@@ -10,7 +8,6 @@ use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
@@ -311,10 +308,6 @@ return function (Container $container): void {
|
||||
return new Build($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForScreenshots', function (Publisher $publisher) {
|
||||
return new Screenshot($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForDeletes', function (Publisher $publisher) {
|
||||
return new Delete($publisher);
|
||||
}, ['publisher']);
|
||||
@@ -323,10 +316,6 @@ return function (Container $container): void {
|
||||
return new Event($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForAudits', function (Publisher $publisher) {
|
||||
return new Audit($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('queueForWebhooks', function (Publisher $publisher) {
|
||||
return new Webhook($publisher);
|
||||
}, ['publisher']);
|
||||
@@ -339,10 +328,6 @@ return function (Container $container): void {
|
||||
return new Realtime();
|
||||
}, []);
|
||||
|
||||
$container->set('queueForCertificates', function (Publisher $publisher) {
|
||||
return new Certificate($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
$container->set('deviceForSites', function (Document $project, Telemetry $telemetry) {
|
||||
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
|
||||
}, ['project', 'telemetry']);
|
||||
@@ -383,7 +368,7 @@ return function (Container $container): void {
|
||||
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('verboseType', \get_class($error));
|
||||
$log->addTag('projectId', $project->getId() ?? '');
|
||||
$log->addTag('projectId', $project->getId());
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
|
||||
+297
-18
@@ -38,6 +38,7 @@ use Utopia\DSN\DSN;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter\None as NoTelemetry;
|
||||
use Utopia\WebSocket\Adapter;
|
||||
@@ -45,6 +46,10 @@ use Utopia\WebSocket\Server;
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
|
||||
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') {
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
}
|
||||
|
||||
/** @var Registry $register */
|
||||
$register = $GLOBALS['register'] ?? throw new \RuntimeException('Registry not initialized');
|
||||
|
||||
@@ -262,7 +267,9 @@ $stats->create();
|
||||
|
||||
$containerId = uniqid();
|
||||
$statsDocument = null;
|
||||
$workerNumber = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
|
||||
$workerNumber = intval(System::getEnv('_APP_WORKERS_NUM', 0))
|
||||
?: intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
|
||||
|
||||
$adapter = new Adapter\Swoole(port: System::getEnv('PORT', 80));
|
||||
$adapter
|
||||
@@ -394,10 +401,27 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
Console::success('Worker ' . $workerId . ' started successfully');
|
||||
|
||||
$telemetry = getTelemetry($workerId);
|
||||
$realtimeDelayBuckets = [100, 250, 500, 750, 1000, 1500, 2000, 3000, 5000, 7500, 10000, 15000, 30000];
|
||||
$workerTelemetryAttributes = ['workerId' => (string) $workerId];
|
||||
$register->set('telemetry', fn () => $telemetry);
|
||||
$register->set('telemetry.workerAttributes', fn () => $workerTelemetryAttributes);
|
||||
$register->set('telemetry.workerCounter', fn () => $telemetry->createUpDownCounter('realtime.server.active_workers'));
|
||||
$register->set('telemetry.workerClientCounter', fn () => $telemetry->createUpDownCounter('realtime.server.worker_clients'));
|
||||
$register->set('telemetry.workerSubscriptionCounter', fn () => $telemetry->createUpDownCounter('realtime.server.worker_subscriptions'));
|
||||
$register->set('telemetry.connectionCounter', fn () => $telemetry->createUpDownCounter('realtime.server.open_connections'));
|
||||
$register->set('telemetry.connectionCreatedCounter', fn () => $telemetry->createCounter('realtime.server.connection.created'));
|
||||
$register->set('telemetry.messageSentCounter', fn () => $telemetry->createCounter('realtime.server.message.sent'));
|
||||
$register->set('telemetry.deliveryDelayHistogram', fn () => $telemetry->createHistogram(
|
||||
name: 'realtime.server.delivery_delay',
|
||||
unit: 'ms',
|
||||
advisory: ['ExplicitBucketBoundaries' => $realtimeDelayBuckets],
|
||||
));
|
||||
$register->set('telemetry.arrivalDelayHistogram', fn () => $telemetry->createHistogram(
|
||||
name: 'realtime.server.arrival_delay',
|
||||
unit: 'ms',
|
||||
advisory: ['ExplicitBucketBoundaries' => $realtimeDelayBuckets],
|
||||
));
|
||||
$register->get('telemetry.workerCounter')->add(1);
|
||||
|
||||
$attempts = 0;
|
||||
$start = time();
|
||||
@@ -514,12 +538,28 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
$pubsub->subscribe(['realtime'], function (mixed $redis, string $channel, string $payload) use ($server, $workerId, $stats, $register, $realtime) {
|
||||
$event = json_decode($payload, true);
|
||||
|
||||
$eventTimestamp = $event['data']['timestamp'] ?? null;
|
||||
if (\is_string($eventTimestamp)) {
|
||||
try {
|
||||
$eventDate = new \DateTimeImmutable($eventTimestamp, new \DateTimeZone('UTC'));
|
||||
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
||||
$eventTimestampMs = (float) $eventDate->format('U.u') * 1000;
|
||||
$nowTimestampMs = (float) $now->format('U.u') * 1000;
|
||||
$arrivalDelayMs = (int) \max(0, $nowTimestampMs - $eventTimestampMs);
|
||||
|
||||
$register->get('telemetry.arrivalDelayHistogram')->record($arrivalDelayMs);
|
||||
} catch (\Throwable) {
|
||||
// Ignore invalid timestamp payloads.
|
||||
}
|
||||
}
|
||||
|
||||
if ($event['permissionsChanged'] && isset($event['userId'])) {
|
||||
$projectId = $event['project'];
|
||||
$userId = $event['userId'];
|
||||
|
||||
if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) {
|
||||
$connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId]));
|
||||
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
|
||||
$consoleDatabase = getConsoleDB();
|
||||
$project = $consoleDatabase->getAuthorization()->skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
|
||||
$database = getProjectDB($project);
|
||||
@@ -529,6 +569,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
|
||||
$roles = $user->getRoles($database->getAuthorization());
|
||||
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
|
||||
$previousUserId = $realtime->connections[$connection]['userId'] ?? '';
|
||||
|
||||
$meta = $realtime->getSubscriptionMetadata($connection);
|
||||
|
||||
@@ -536,13 +577,19 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
|
||||
foreach ($meta as $subscriptionId => $subscription) {
|
||||
$queries = Query::parseQueries($subscription['queries'] ?? []);
|
||||
$channels = Realtime::rebindAccountChannels(
|
||||
$subscription['channels'] ?? [],
|
||||
$previousUserId,
|
||||
$userId
|
||||
);
|
||||
$realtime->subscribe(
|
||||
$projectId,
|
||||
$connection,
|
||||
$subscriptionId,
|
||||
$roles,
|
||||
$subscription['channels'] ?? [],
|
||||
$queries
|
||||
$channels,
|
||||
$queries,
|
||||
$userId
|
||||
);
|
||||
}
|
||||
|
||||
@@ -550,6 +597,12 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
if ($authorization !== null) {
|
||||
$realtime->connections[$connection]['authorization'] = $authorization;
|
||||
}
|
||||
|
||||
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
|
||||
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
|
||||
if ($subscriptionDelta !== 0) {
|
||||
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,6 +645,20 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
if ($total > 0) {
|
||||
$register->get('telemetry.messageSentCounter')->add($total);
|
||||
$stats->incr($event['project'], 'messages', $total);
|
||||
$updatedAt = $event['data']['payload']['$updatedAt'] ?? null;
|
||||
if (\is_string($updatedAt)) {
|
||||
try {
|
||||
$updatedAtDate = new \DateTimeImmutable($updatedAt, new \DateTimeZone('UTC'));
|
||||
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
||||
$updatedAtTimestampMs = (float) $updatedAtDate->format('U.u') * 1000;
|
||||
$nowTimestampMs = (float) $now->format('U.u') * 1000;
|
||||
$delayMs = (int) \max(0, $nowTimestampMs - $updatedAtTimestampMs);
|
||||
|
||||
$register->get('telemetry.deliveryDelayHistogram')->record($delayMs);
|
||||
} catch (\Throwable) {
|
||||
// Ignore invalid timestamp payloads.
|
||||
}
|
||||
}
|
||||
|
||||
$projectId = $event['project'] ?? null;
|
||||
|
||||
@@ -621,6 +688,16 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
Console::error('Failed to restart pub/sub...');
|
||||
});
|
||||
|
||||
$server->onWorkerStop(function (int $workerId) use ($register) {
|
||||
Console::warning('Worker ' . $workerId . ' stopping');
|
||||
|
||||
try {
|
||||
$register->get('telemetry.workerCounter')->add(-1);
|
||||
} catch (\Throwable $th) {
|
||||
Console::error('Realtime onWorkerStop telemetry error: ' . $th->getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $registerConnectionResources) {
|
||||
global $container;
|
||||
$request = new Request($request);
|
||||
@@ -636,6 +713,20 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
$project = null;
|
||||
$logUser = null;
|
||||
$authorization = null;
|
||||
$rawSize = $request->getSize();
|
||||
$channelCount = 0;
|
||||
$subscriptionCount = 0;
|
||||
$outboundBytes = 0;
|
||||
$responseCode = 200;
|
||||
$subscriptionMode = 'message';
|
||||
$success = false;
|
||||
|
||||
Span::init('realtime.open');
|
||||
Span::add('realtime.connectionId', $connection);
|
||||
Span::add('realtime.inboundBytes', $rawSize);
|
||||
if (!empty($request->getOrigin())) {
|
||||
Span::add('realtime.origin', $request->getOrigin());
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var Document $project */
|
||||
@@ -685,8 +776,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many requests');
|
||||
}
|
||||
|
||||
$rawSize = $request->getSize();
|
||||
|
||||
triggerStats([
|
||||
METRIC_REALTIME_INBOUND => $rawSize,
|
||||
], $project->getId());
|
||||
@@ -706,9 +795,11 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
$roles = $user->getRoles($authorization);
|
||||
|
||||
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
|
||||
$channelCount = \count($channels);
|
||||
|
||||
$updateStats = static function (string $projectId, ?string $teamId, string $payloadJson) use ($register, $stats): void {
|
||||
$register->get('telemetry.connectionCounter')->add(1);
|
||||
$register->get('telemetry.workerClientCounter')->add(1, $register->get('telemetry.workerAttributes'));
|
||||
$register->get('telemetry.connectionCreatedCounter')->add(1);
|
||||
|
||||
$stats->set($projectId, [
|
||||
@@ -742,11 +833,15 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
$realtime->subscribe($project->getId(), $connection, '', $roles, [], [], $user->getId());
|
||||
$realtime->connections[$connection]['authorization'] = $authorization;
|
||||
$server->send([$connection], $connectedPayloadJson);
|
||||
$outboundBytes += \strlen($connectedPayloadJson);
|
||||
$updateStats($project->getId(), $project->getAttribute('teamId'), $connectedPayloadJson);
|
||||
$subscriptionMode = 'message';
|
||||
$success = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$names = array_keys($channels);
|
||||
$subscriptionMode = 'url';
|
||||
|
||||
try {
|
||||
$subscriptions = Realtime::constructSubscriptions(
|
||||
@@ -773,6 +868,10 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
|
||||
$mapping[$index] = $subscriptionId;
|
||||
}
|
||||
$subscriptionCount = \count($subscriptions);
|
||||
if (!empty($subscriptions)) {
|
||||
$register->get('telemetry.workerSubscriptionCounter')->add(\count($subscriptions), $register->get('telemetry.workerAttributes'));
|
||||
}
|
||||
|
||||
$realtime->connections[$connection]['authorization'] = $authorization;
|
||||
|
||||
@@ -788,8 +887,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
]);
|
||||
|
||||
$server->send([$connection], $connectedPayloadJson);
|
||||
$outboundBytes += \strlen($connectedPayloadJson);
|
||||
$updateStats($project->getId(), $project->getAttribute('teamId'), $connectedPayloadJson);
|
||||
|
||||
$success = true;
|
||||
|
||||
} catch (Throwable $th) {
|
||||
logError($th, 'realtime', project: $project, user: $logUser, authorization: $authorization);
|
||||
@@ -799,6 +899,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
if (!\is_int($code)) {
|
||||
$code = 500;
|
||||
}
|
||||
$responseCode = $code;
|
||||
|
||||
$message = $th->getMessage();
|
||||
|
||||
@@ -816,7 +917,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
]
|
||||
];
|
||||
|
||||
$server->send([$connection], json_encode($response));
|
||||
$responsePayloadJson = json_encode($response);
|
||||
$server->send([$connection], $responsePayloadJson);
|
||||
$outboundBytes += \strlen($responsePayloadJson);
|
||||
$server->close($connection, $code);
|
||||
|
||||
if (System::getEnv('_APP_ENV', 'production') === 'development') {
|
||||
@@ -824,16 +927,44 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
Console::error('[Error] Code: ' . $response['data']['code']);
|
||||
Console::error('[Error] Message: ' . $response['data']['message']);
|
||||
}
|
||||
Span::error($th);
|
||||
} finally {
|
||||
Span::add('realtime.success', $success);
|
||||
Span::add('realtime.responseCode', $responseCode);
|
||||
Span::add('realtime.subscriptionMode', $subscriptionMode);
|
||||
Span::add('realtime.channelCount', $channelCount);
|
||||
Span::add('realtime.subscriptionCount', $subscriptionCount);
|
||||
Span::add('realtime.outboundBytes', $outboundBytes);
|
||||
if (!empty($project?->getId())) {
|
||||
Span::add('realtime.projectId', $project->getId());
|
||||
}
|
||||
if (!empty($logUser?->getId())) {
|
||||
Span::add('realtime.userId', $logUser->getId());
|
||||
}
|
||||
Span::current()?->finish();
|
||||
}
|
||||
});
|
||||
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId) {
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId, $register) {
|
||||
$project = null;
|
||||
$authorization = null;
|
||||
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
|
||||
$rawSize = \strlen($message);
|
||||
$messageType = 'invalid';
|
||||
$subscriptionDelta = 0;
|
||||
$subscriptionsRequested = 0;
|
||||
$subscriptionsRemoved = 0;
|
||||
$outboundBytes = 0;
|
||||
$responseCode = 200;
|
||||
$success = false;
|
||||
|
||||
Span::init('realtime.message');
|
||||
Span::add('realtime.connectionId', $connection);
|
||||
Span::add('realtime.inboundBytes', $rawSize);
|
||||
Span::add('realtime.containerId', $containerId);
|
||||
|
||||
try {
|
||||
$rawSize = \strlen($message);
|
||||
$response = new Response(new SwooleResponse());
|
||||
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
|
||||
|
||||
// Get authorization from connection (stored during onOpen)
|
||||
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
|
||||
@@ -883,6 +1014,12 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message format is not valid.');
|
||||
}
|
||||
|
||||
$messageType = $message['type'] ?? 'invalid';
|
||||
|
||||
if (!\is_scalar($messageType)) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
|
||||
}
|
||||
|
||||
// Ping does not require project context; other messages do (e.g. after unsubscribe during auth)
|
||||
if (empty($projectId) && ($message['type'] ?? '') !== 'ping') {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing project context. Reconnect to the project first.');
|
||||
@@ -895,6 +1032,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
]);
|
||||
|
||||
$server->send([$connection], $pongPayloadJson);
|
||||
$outboundBytes += \strlen($pongPayloadJson);
|
||||
|
||||
if ($project !== null && !$project->isEmpty()) {
|
||||
$pongOutboundBytes = \strlen($pongPayloadJson);
|
||||
@@ -940,7 +1078,13 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
|
||||
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
|
||||
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
|
||||
// Capture the pre-auth userId so we can rebind any account channels
|
||||
// that were stored under it (e.g. guest who subscribed to `account`
|
||||
// and now authenticates). unsubscribe() below clears the connection
|
||||
// entry, so we must read it first.
|
||||
$previousUserId = $realtime->connections[$connection]['userId'] ?? '';
|
||||
|
||||
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
|
||||
$meta = $realtime->getSubscriptionMetadata($connection);
|
||||
|
||||
$realtime->unsubscribe($connection);
|
||||
@@ -948,13 +1092,18 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
if (!empty($projectId)) {
|
||||
foreach ($meta as $subscriptionId => $subscription) {
|
||||
$queries = Query::parseQueries($subscription['queries'] ?? []);
|
||||
$channels = Realtime::rebindAccountChannels(
|
||||
$subscription['channels'] ?? [],
|
||||
$previousUserId,
|
||||
$user->getId()
|
||||
);
|
||||
|
||||
$realtime->subscribe(
|
||||
$projectId,
|
||||
$connection,
|
||||
$subscriptionId,
|
||||
$roles,
|
||||
$subscription['channels'] ?? [],
|
||||
$channels,
|
||||
$queries,
|
||||
$user->getId()
|
||||
);
|
||||
@@ -965,6 +1114,12 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
$realtime->connections[$connection]['authorization'] = $authorization;
|
||||
}
|
||||
|
||||
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
|
||||
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
|
||||
if ($subscriptionDelta !== 0) {
|
||||
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
|
||||
}
|
||||
|
||||
$user = $response->output($user, Response::MODEL_ACCOUNT);
|
||||
|
||||
$authResponsePayloadJson = json_encode([
|
||||
@@ -977,6 +1132,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
]);
|
||||
|
||||
$server->send([$connection], $authResponsePayloadJson);
|
||||
$outboundBytes += \strlen($authResponsePayloadJson);
|
||||
|
||||
if ($project !== null && !$project->isEmpty()) {
|
||||
$authOutboundBytes = \strlen($authResponsePayloadJson);
|
||||
@@ -1009,6 +1165,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
|
||||
// bulk validation + parsing before subscribing
|
||||
$parsedPayloads = [];
|
||||
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
|
||||
foreach ($message['data'] as $payload) {
|
||||
if (!\is_array($payload)) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Each subscribe payload must be an object.');
|
||||
@@ -1037,22 +1194,28 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Invalid query: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$convertedChannels = \array_keys(Realtime::convertChannels($payload['channels'], $userId));
|
||||
|
||||
$parsedPayloads[] = [
|
||||
'subscriptionId' => $subscriptionId,
|
||||
'channels' => $payload['channels'],
|
||||
'convertedChannels' => $convertedChannels,
|
||||
'queries' => $convertedQueries,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($parsedPayloads as $parsedPayload) {
|
||||
$subscriptionId = $parsedPayload['subscriptionId'];
|
||||
$channels = \array_keys(Realtime::convertChannels($parsedPayload['channels'], $userId));
|
||||
$channels = $parsedPayload['convertedChannels'];
|
||||
$queries = $parsedPayload['queries'];
|
||||
$realtime->subscribe($projectId, $connection, $subscriptionId, $roles, $channels, $queries);
|
||||
}
|
||||
|
||||
// subscribe() overwrites the connection entry; restore auth so later onMessage uses the same context.
|
||||
$realtime->connections[$connection]['authorization'] = $authorization;
|
||||
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
|
||||
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
|
||||
$subscriptionsRequested = \count($parsedPayloads);
|
||||
if ($subscriptionDelta !== 0) {
|
||||
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
|
||||
}
|
||||
|
||||
$responsePayload = json_encode([
|
||||
'type' => 'response',
|
||||
@@ -1062,7 +1225,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
'subscriptions' => \array_map(function (array $parsedPayload) {
|
||||
return [
|
||||
'subscriptionId' => $parsedPayload['subscriptionId'],
|
||||
'channels' => $parsedPayload['channels'],
|
||||
'channels' => $parsedPayload['convertedChannels'],
|
||||
'queries' => \array_map(fn ($q) => $q->toString(), $parsedPayload['queries']),
|
||||
];
|
||||
}, $parsedPayloads),
|
||||
@@ -1070,6 +1233,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
]);
|
||||
|
||||
$server->send([$connection], $responsePayload);
|
||||
$outboundBytes += \strlen($responsePayload);
|
||||
|
||||
if ($project !== null && !$project->isEmpty()) {
|
||||
$subscribeOutboundBytes = \strlen($responsePayload);
|
||||
@@ -1083,15 +1247,79 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
|
||||
break;
|
||||
|
||||
case 'unsubscribe':
|
||||
if (!\is_array($message['data']) || !\array_is_list($message['data'])) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
|
||||
}
|
||||
|
||||
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
|
||||
|
||||
// Validate every payload before executing any removal so an invalid entry
|
||||
// later in the batch does not leave earlier entries half-applied on the server.
|
||||
$validatedIds = [];
|
||||
foreach ($message['data'] as $payload) {
|
||||
if (
|
||||
!\is_array($payload)
|
||||
|| !\array_key_exists('subscriptionId', $payload)
|
||||
|| !\is_string($payload['subscriptionId'])
|
||||
|| $payload['subscriptionId'] === ''
|
||||
) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Each unsubscribe payload must include a non-empty subscriptionId.');
|
||||
}
|
||||
$validatedIds[] = $payload['subscriptionId'];
|
||||
}
|
||||
|
||||
$unsubscribeResults = [];
|
||||
foreach ($validatedIds as $subscriptionId) {
|
||||
$wasRemoved = $realtime->unsubscribeSubscription($connection, $subscriptionId);
|
||||
$unsubscribeResults[] = [
|
||||
'subscriptionId' => $subscriptionId,
|
||||
'removed' => $wasRemoved,
|
||||
];
|
||||
}
|
||||
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
|
||||
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
|
||||
$subscriptionsRequested = \count($validatedIds);
|
||||
$subscriptionsRemoved = \count(\array_filter($unsubscribeResults, fn (array $item) => $item['removed']));
|
||||
if ($subscriptionDelta !== 0) {
|
||||
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
|
||||
}
|
||||
|
||||
$unsubscribeResponsePayload = json_encode([
|
||||
'type' => 'response',
|
||||
'data' => [
|
||||
'to' => 'unsubscribe',
|
||||
'success' => true,
|
||||
'subscriptions' => $unsubscribeResults,
|
||||
],
|
||||
]);
|
||||
|
||||
$server->send([$connection], $unsubscribeResponsePayload);
|
||||
$outboundBytes += \strlen($unsubscribeResponsePayload);
|
||||
|
||||
if ($project !== null && !$project->isEmpty()) {
|
||||
$unsubscribeOutboundBytes = \strlen($unsubscribeResponsePayload);
|
||||
|
||||
if ($unsubscribeOutboundBytes > 0) {
|
||||
triggerStats([
|
||||
METRIC_REALTIME_OUTBOUND => $unsubscribeOutboundBytes,
|
||||
], $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
|
||||
}
|
||||
$success = true;
|
||||
} catch (Throwable $th) {
|
||||
logError($th, 'realtimeMessage', project: $project, authorization: $authorization);
|
||||
$code = $th->getCode();
|
||||
if (!is_int($code)) {
|
||||
$code = 500;
|
||||
}
|
||||
$responseCode = $code;
|
||||
|
||||
$message = $th->getMessage();
|
||||
|
||||
@@ -1108,19 +1336,52 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
]
|
||||
];
|
||||
|
||||
$server->send([$connection], json_encode($response));
|
||||
$responsePayloadJson = json_encode($response);
|
||||
$server->send([$connection], $responsePayloadJson);
|
||||
$outboundBytes += \strlen($responsePayloadJson);
|
||||
|
||||
if ($th->getCode() === 1008) {
|
||||
$server->close($connection, $th->getCode());
|
||||
}
|
||||
Span::error($th);
|
||||
} finally {
|
||||
Span::add('realtime.success', $success);
|
||||
Span::add('realtime.responseCode', $responseCode);
|
||||
Span::add('realtime.subscriptionDelta', $subscriptionDelta);
|
||||
Span::add('realtime.subscriptionsRequested', $subscriptionsRequested);
|
||||
Span::add('realtime.subscriptionsRemoved', $subscriptionsRemoved);
|
||||
Span::add('realtime.subscribe.subscriptionsCount', $subscriptionsRequested);
|
||||
Span::add('realtime.outboundBytes', $outboundBytes);
|
||||
Span::add('realtime.projectId', $project?->getId() ?? $projectId);
|
||||
Span::add('realtime.userId', $realtime->connections[$connection]['userId'] ?? null);
|
||||
Span::add('realtime.messageType', $messageType);
|
||||
Span::current()?->finish();
|
||||
}
|
||||
});
|
||||
|
||||
$server->onClose(function (int $connection) use ($realtime, $stats, $register) {
|
||||
$projectId = null;
|
||||
$userId = null;
|
||||
$subscriptionsBeforeClose = 0;
|
||||
$success = false;
|
||||
|
||||
Span::init('realtime.close');
|
||||
Span::add('realtime.connectionId', $connection);
|
||||
|
||||
if (array_key_exists($connection, $realtime->connections)) {
|
||||
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
|
||||
$userId = $realtime->connections[$connection]['userId'] ?? null;
|
||||
}
|
||||
|
||||
try {
|
||||
if (array_key_exists($connection, $realtime->connections)) {
|
||||
$stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal');
|
||||
$register->get('telemetry.connectionCounter')->add(-1);
|
||||
$register->get('telemetry.workerClientCounter')->add(-1, $register->get('telemetry.workerAttributes'));
|
||||
$subscriptionsBeforeClose = \count($realtime->getSubscriptionMetadata($connection));
|
||||
if ($subscriptionsBeforeClose > 0) {
|
||||
$register->get('telemetry.workerSubscriptionCounter')->add(-$subscriptionsBeforeClose, $register->get('telemetry.workerAttributes'));
|
||||
}
|
||||
|
||||
$projectId = $realtime->connections[$connection]['projectId'];
|
||||
|
||||
@@ -1128,12 +1389,30 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) {
|
||||
METRIC_REALTIME_CONNECTIONS => -1,
|
||||
], $projectId);
|
||||
}
|
||||
$success = true;
|
||||
} catch (\Throwable $th) {
|
||||
// Log only; do not rethrow. If we let this bubble, Swoole dumps full coroutine
|
||||
// backtraces and unsubscribe() below would never run (connection cleanup would fail).
|
||||
Console::error('Realtime onClose error: ' . $th->getMessage());
|
||||
Span::error($th);
|
||||
} finally {
|
||||
try {
|
||||
$realtime->unsubscribe($connection);
|
||||
} catch (\Throwable $th) {
|
||||
Console::error('Realtime onClose unsubscribe error: ' . $th->getMessage());
|
||||
Span::error($th);
|
||||
}
|
||||
|
||||
Span::add('realtime.success', $success);
|
||||
if (!empty($projectId)) {
|
||||
Span::add('realtime.projectId', $projectId);
|
||||
}
|
||||
if (!empty($userId)) {
|
||||
Span::add('realtime.userId', $userId);
|
||||
}
|
||||
Span::add('realtime.subscriptionsBeforeClose', $subscriptionsBeforeClose);
|
||||
Span::current()?->finish();
|
||||
}
|
||||
$realtime->unsubscribe($connection);
|
||||
|
||||
Console::info('Connection close: ' . $connection);
|
||||
});
|
||||
|
||||
@@ -120,7 +120,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_SMTP_HOST
|
||||
- _APP_SMTP_PORT
|
||||
- _APP_SMTP_SECURE
|
||||
@@ -256,7 +255,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
@@ -287,7 +285,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-webhooks:
|
||||
@@ -315,7 +312,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -356,7 +352,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
@@ -416,7 +411,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-builds:
|
||||
@@ -453,7 +447,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_VCS_GITHUB_APP_NAME
|
||||
- _APP_VCS_GITHUB_PRIVATE_KEY
|
||||
@@ -529,7 +522,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-executions:
|
||||
@@ -592,7 +584,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_SITES_TIMEOUT
|
||||
- _APP_COMPUTE_BUILD_TIMEOUT
|
||||
@@ -630,7 +621,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -673,7 +663,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_SMS_FROM
|
||||
- _APP_SMS_PROVIDER
|
||||
@@ -734,7 +723,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
|
||||
@@ -773,7 +761,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_CACHE
|
||||
@@ -806,7 +793,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -839,7 +825,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -871,7 +856,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -907,7 +891,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
appwrite-task-scheduler-executions:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
@@ -936,7 +919,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
appwrite-task-scheduler-messages:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
@@ -965,7 +947,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
<?php if ($enableAssistant): ?>
|
||||
appwrite-assistant:
|
||||
@@ -1068,13 +1049,12 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
image: mongo:8.2.5
|
||||
container_name: appwrite-mongodb
|
||||
<<: *x-logging
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-mongodb:/data/db
|
||||
- appwrite-mongodb-keyfile:/data/keyfile
|
||||
ports:
|
||||
- "27017:27017"
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=root
|
||||
- MONGO_INITDB_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
|
||||
@@ -1205,7 +1185,6 @@ volumes:
|
||||
<?php elseif ($dbService === 'mongodb'): ?>
|
||||
appwrite-mongodb:
|
||||
appwrite-mongodb-keyfile:
|
||||
appwrite-mongodb-config:
|
||||
<?php endif; ?>
|
||||
appwrite-redis:
|
||||
appwrite-cache:
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ $worker
|
||||
$log->setAction('appwrite-queue-' . $queueName);
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('projectId', $project->getId() ?? 'n/a');
|
||||
$log->addTag('projectId', $project->getId());
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
|
||||
+11
-3
@@ -69,19 +69,20 @@
|
||||
"utopia-php/dsn": "0.2.1",
|
||||
"utopia-php/http": "0.34.*",
|
||||
"utopia-php/fetch": "0.5.*",
|
||||
"utopia-php/validators": "0.2.*",
|
||||
"utopia-php/image": "0.8.*",
|
||||
"utopia-php/locale": "0.8.*",
|
||||
"utopia-php/logger": "0.6.*",
|
||||
"utopia-php/messaging": "0.22.*",
|
||||
"utopia-php/migration": "1.9.*",
|
||||
"utopia-php/platform": "0.12.*",
|
||||
"utopia-php/platform": "0.13.*",
|
||||
"utopia-php/pools": "1.*",
|
||||
"utopia-php/span": "1.1.*",
|
||||
"utopia-php/preloader": "0.2.*",
|
||||
"utopia-php/queue": "0.17.*",
|
||||
"utopia-php/servers": "0.3.*",
|
||||
"utopia-php/registry": "0.5.*",
|
||||
"utopia-php/storage": "1.0.*",
|
||||
"utopia-php/storage": "2.*",
|
||||
"utopia-php/system": "0.10.*",
|
||||
"utopia-php/telemetry": "0.2.*",
|
||||
"utopia-php/vcs": "3.*",
|
||||
@@ -94,8 +95,15 @@
|
||||
"spomky-labs/otphp": "11.*",
|
||||
"webonyx/graphql-php": "15.31.*",
|
||||
"league/csv": "9.14.*",
|
||||
"enshrined/svg-sanitize": "0.22.*"
|
||||
"enshrined/svg-sanitize": "0.22.*",
|
||||
"utopia-php/lock": "0.2.*"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/utopia-php/lock"
|
||||
}
|
||||
],
|
||||
"require-dev": {
|
||||
"ext-fileinfo": "*",
|
||||
"appwrite/sdk-generator": "*",
|
||||
|
||||
Generated
+233
-159
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "f6a87c1012b316e614258f8f57a28e48",
|
||||
"content-hash": "a9d25a0518aee3fc29ea57498cb95744",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -1996,16 +1996,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpseclib/phpseclib",
|
||||
"version": "3.0.51",
|
||||
"version": "3.0.52",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpseclib/phpseclib.git",
|
||||
"reference": "d59c94077f9c9915abb51ddb52ce85188ece1748"
|
||||
"reference": "2adaefc83df2ec548558307690f376dd7d4f4fce"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d59c94077f9c9915abb51ddb52ce85188ece1748",
|
||||
"reference": "d59c94077f9c9915abb51ddb52ce85188ece1748",
|
||||
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2adaefc83df2ec548558307690f376dd7d4f4fce",
|
||||
"reference": "2adaefc83df2ec548558307690f376dd7d4f4fce",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2086,7 +2086,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpseclib/phpseclib/issues",
|
||||
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.51"
|
||||
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.52"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2102,7 +2102,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-10T01:33:53+00:00"
|
||||
"time": "2026-04-27T07:02:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/clock",
|
||||
@@ -2887,7 +2887,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
@@ -2948,7 +2948,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2972,7 +2972,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php82",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php82.git",
|
||||
@@ -3028,7 +3028,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php82/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-php82/tree/v1.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -3052,7 +3052,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php83",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php83.git",
|
||||
@@ -3108,7 +3108,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -3132,16 +3132,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php85",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php85.git",
|
||||
"reference": "2c408a6bb0313e6001a83628dc5506100474254e"
|
||||
"reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/2c408a6bb0313e6001a83628dc5506100474254e",
|
||||
"reference": "2c408a6bb0313e6001a83628dc5506100474254e",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee",
|
||||
"reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3188,7 +3188,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php85/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -3208,7 +3208,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-10T16:50:15+00:00"
|
||||
"time": "2026-04-26T13:10:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
@@ -3351,16 +3351,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/abuse",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/abuse.git",
|
||||
"reference": "20bee84fd14dbe81d50ecabf1ffd81cceca06152"
|
||||
"reference": "53f4274939353522ba331f55bcff6e6011ffc56c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/20bee84fd14dbe81d50ecabf1ffd81cceca06152",
|
||||
"reference": "20bee84fd14dbe81d50ecabf1ffd81cceca06152",
|
||||
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/53f4274939353522ba331f55bcff6e6011ffc56c",
|
||||
"reference": "53f4274939353522ba331f55bcff6e6011ffc56c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3397,9 +3397,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/abuse/issues",
|
||||
"source": "https://github.com/utopia-php/abuse/tree/1.2.2"
|
||||
"source": "https://github.com/utopia-php/abuse/tree/1.2.3"
|
||||
},
|
||||
"time": "2026-02-02T10:43:10+00:00"
|
||||
"time": "2026-04-29T11:19:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/agents",
|
||||
@@ -3658,16 +3658,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/cli",
|
||||
"version": "0.23.1",
|
||||
"version": "0.23.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/cli.git",
|
||||
"reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621"
|
||||
"reference": "145b91fef827853bcceaa3ab8ca2b1d6faaca2ab"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/cli/zipball/8d1955b8bc4dc631f45d7c7df689ed7b63f70621",
|
||||
"reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621",
|
||||
"url": "https://api.github.com/repos/utopia-php/cli/zipball/145b91fef827853bcceaa3ab8ca2b1d6faaca2ab",
|
||||
"reference": "145b91fef827853bcceaa3ab8ca2b1d6faaca2ab",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3703,9 +3703,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/cli/issues",
|
||||
"source": "https://github.com/utopia-php/cli/tree/0.23.1"
|
||||
"source": "https://github.com/utopia-php/cli/tree/0.23.2"
|
||||
},
|
||||
"time": "2026-04-05T15:27:35+00:00"
|
||||
"time": "2026-04-27T09:19:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/compression",
|
||||
@@ -3850,16 +3850,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "5.3.21",
|
||||
"version": "5.4.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "ee2d7d4c87b3a3fae954089ad7494ceb454f619d"
|
||||
"reference": "688d9422b5ff42ac2ecc29397d94891cfd772e93"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/ee2d7d4c87b3a3fae954089ad7494ceb454f619d",
|
||||
"reference": "ee2d7d4c87b3a3fae954089ad7494ceb454f619d",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/688d9422b5ff42ac2ecc29397d94891cfd772e93",
|
||||
"reference": "688d9422b5ff42ac2ecc29397d94891cfd772e93",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3903,9 +3903,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/database/issues",
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.21"
|
||||
"source": "https://github.com/utopia-php/database/tree/5.4.1"
|
||||
},
|
||||
"time": "2026-04-10T12:38:57+00:00"
|
||||
"time": "2026-04-29T07:32:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -4062,16 +4062,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/domains",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/domains.git",
|
||||
"reference": "0edf6bb2b07f30db849a267027077bf5abb994c6"
|
||||
"reference": "c87ba0a1da4cbf75d2cff9d3ea0262b78f1d86f6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/domains/zipball/0edf6bb2b07f30db849a267027077bf5abb994c6",
|
||||
"reference": "0edf6bb2b07f30db849a267027077bf5abb994c6",
|
||||
"url": "https://api.github.com/repos/utopia-php/domains/zipball/c87ba0a1da4cbf75d2cff9d3ea0262b78f1d86f6",
|
||||
"reference": "c87ba0a1da4cbf75d2cff9d3ea0262b78f1d86f6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4118,9 +4118,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/domains/issues",
|
||||
"source": "https://github.com/utopia-php/domains/tree/1.0.5"
|
||||
"source": "https://github.com/utopia-php/domains/tree/1.0.6"
|
||||
},
|
||||
"time": "2026-03-03T09:20:50+00:00"
|
||||
"time": "2026-04-29T11:08:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/dsn",
|
||||
@@ -4271,21 +4271,20 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/http",
|
||||
"version": "0.34.20",
|
||||
"version": "0.34.24",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/http.git",
|
||||
"reference": "d6b360d555022d16c16d40be51f86180364819f8"
|
||||
"reference": "d1eced0627c5a9fceddf53992ed97d664b810d33"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/d6b360d555022d16c16d40be51f86180364819f8",
|
||||
"reference": "d6b360d555022d16c16d40be51f86180364819f8",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/d1eced0627c5a9fceddf53992ed97d664b810d33",
|
||||
"reference": "d1eced0627c5a9fceddf53992ed97d664b810d33",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-swoole": "*",
|
||||
"php": ">=8.2",
|
||||
"php": ">=8.3",
|
||||
"utopia-php/compression": "0.1.*",
|
||||
"utopia-php/di": "0.3.*",
|
||||
"utopia-php/servers": "0.3.*",
|
||||
@@ -4295,11 +4294,14 @@
|
||||
"require-dev": {
|
||||
"doctrine/instantiator": "^1.5",
|
||||
"laravel/pint": "1.*",
|
||||
"phpbench/phpbench": "^1.2",
|
||||
"phpstan/phpstan": "1.*",
|
||||
"phpunit/phpunit": "^9.5.25",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpunit/phpunit": "^12.0",
|
||||
"rector/rector": "^2.4",
|
||||
"swoole/ide-helper": "4.8.3"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-swoole": "Required to use the Swoole server adapter (\\Utopia\\Http\\Adapter\\Swoole\\Server)."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -4319,22 +4321,22 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/http/issues",
|
||||
"source": "https://github.com/utopia-php/http/tree/0.34.20"
|
||||
"source": "https://github.com/utopia-php/http/tree/0.34.24"
|
||||
},
|
||||
"time": "2026-04-12T14:25:22+00:00"
|
||||
"time": "2026-04-24T12:16:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
"version": "0.8.4",
|
||||
"version": "0.8.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/image.git",
|
||||
"reference": "ce788ff0121a79286fdbe3ef3eba566de646df65"
|
||||
"reference": "85ab7027873e11bc901110d8f7830252247ba724"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/image/zipball/ce788ff0121a79286fdbe3ef3eba566de646df65",
|
||||
"reference": "ce788ff0121a79286fdbe3ef3eba566de646df65",
|
||||
"url": "https://api.github.com/repos/utopia-php/image/zipball/85ab7027873e11bc901110d8f7830252247ba724",
|
||||
"reference": "85ab7027873e11bc901110d8f7830252247ba724",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4343,10 +4345,12 @@
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.2.*",
|
||||
"phpstan/phpstan": "^1.10.0",
|
||||
"phpunit/phpunit": "^9.3",
|
||||
"vimeo/psalm": "4.13.1"
|
||||
"laravel/pint": "1.24.*",
|
||||
"phpstan/phpstan": "2.1.*",
|
||||
"phpunit/phpunit": "10.5.*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-imagick": "Imagick extension is required for Imagick adapter"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -4368,9 +4372,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/image/issues",
|
||||
"source": "https://github.com/utopia-php/image/tree/0.8.4"
|
||||
"source": "https://github.com/utopia-php/image/tree/0.8.6"
|
||||
},
|
||||
"time": "2025-06-03T08:32:20+00:00"
|
||||
"time": "2026-04-19T12:52:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/locale",
|
||||
@@ -4419,6 +4423,78 @@
|
||||
},
|
||||
"time": "2025-08-12T12:58:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/lock",
|
||||
"version": "0.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/lock.git",
|
||||
"reference": "49317c9493d8f747e4299aa24c22862aa5f6e106"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/lock/zipball/49317c9493d8f747e4299aa24c22862aa5f6e106",
|
||||
"reference": "49317c9493d8f747e4299aa24c22862aa5f6e106",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.*",
|
||||
"phpstan/phpstan": "2.*",
|
||||
"phpunit/phpunit": "11.*",
|
||||
"swoole/ide-helper": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-pcntl": "Required to run the File lock tests",
|
||||
"ext-redis": "Required for the Distributed lock",
|
||||
"ext-swoole": "Required for the Mutex and Semaphore locks (>=6.0)"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Utopia\\Lock\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Utopia\\Lock\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": [
|
||||
"vendor/bin/phpunit"
|
||||
],
|
||||
"lint": [
|
||||
"vendor/bin/pint --test"
|
||||
],
|
||||
"format": [
|
||||
"vendor/bin/pint"
|
||||
],
|
||||
"format:check": [
|
||||
"vendor/bin/pint --test"
|
||||
],
|
||||
"analyze": [
|
||||
"vendor/bin/phpstan analyse --memory-limit=512M"
|
||||
]
|
||||
},
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Appwrite Team",
|
||||
"email": "team@appwrite.io"
|
||||
}
|
||||
],
|
||||
"description": "Mutex, semaphore, file and distributed locks for PHP — one interface, four backends.",
|
||||
"support": {
|
||||
"source": "https://github.com/utopia-php/lock/tree/0.2.0",
|
||||
"issues": "https://github.com/utopia-php/lock/issues"
|
||||
},
|
||||
"time": "2026-04-24T10:47:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/logger",
|
||||
"version": "0.6.2",
|
||||
@@ -4526,16 +4602,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
"version": "1.9.1",
|
||||
"version": "1.9.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/migration.git",
|
||||
"reference": "7a86aeadf182b63a9f4ceba7e137588b31c5d2e2"
|
||||
"reference": "952a4dfe232702f80e45c35129466a8d8cb4c599"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/7a86aeadf182b63a9f4ceba7e137588b31c5d2e2",
|
||||
"reference": "7a86aeadf182b63a9f4ceba7e137588b31c5d2e2",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/952a4dfe232702f80e45c35129466a8d8cb4c599",
|
||||
"reference": "952a4dfe232702f80e45c35129466a8d8cb4c599",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4546,7 +4622,7 @@
|
||||
"php": ">=8.1",
|
||||
"utopia-php/database": "5.*",
|
||||
"utopia-php/dsn": "0.2.*",
|
||||
"utopia-php/storage": "1.0.*"
|
||||
"utopia-php/storage": "2.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-pdo": "*",
|
||||
@@ -4575,22 +4651,22 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/migration/issues",
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.9.1"
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.9.5"
|
||||
},
|
||||
"time": "2026-03-25T07:05:27+00:00"
|
||||
"time": "2026-04-29T11:19:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/mongo",
|
||||
"version": "1.0.2",
|
||||
"version": "1.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/mongo.git",
|
||||
"reference": "677a21c53f7a1316c528b4b45b3fce886cee7223"
|
||||
"reference": "73593682deee4696525a04e26524c1c1226e1530"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/mongo/zipball/677a21c53f7a1316c528b4b45b3fce886cee7223",
|
||||
"reference": "677a21c53f7a1316c528b4b45b3fce886cee7223",
|
||||
"url": "https://api.github.com/repos/utopia-php/mongo/zipball/73593682deee4696525a04e26524c1c1226e1530",
|
||||
"reference": "73593682deee4696525a04e26524c1c1226e1530",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4636,22 +4712,22 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/mongo/issues",
|
||||
"source": "https://github.com/utopia-php/mongo/tree/1.0.2"
|
||||
"source": "https://github.com/utopia-php/mongo/tree/1.1.0"
|
||||
},
|
||||
"time": "2026-03-18T02:45:50+00:00"
|
||||
"time": "2026-04-24T06:15:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/platform",
|
||||
"version": "0.12.1",
|
||||
"version": "0.13.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/platform.git",
|
||||
"reference": "2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc"
|
||||
"reference": "d23af5349a7ea9ee11f9920a13626226f985522e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/platform/zipball/2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc",
|
||||
"reference": "2a6b88168b3a99d4d7d3b37d927f2cb91da5e0fc",
|
||||
"url": "https://api.github.com/repos/utopia-php/platform/zipball/d23af5349a7ea9ee11f9920a13626226f985522e",
|
||||
"reference": "d23af5349a7ea9ee11f9920a13626226f985522e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4687,9 +4763,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/platform/issues",
|
||||
"source": "https://github.com/utopia-php/platform/tree/0.12.1"
|
||||
"source": "https://github.com/utopia-php/platform/tree/0.13.0"
|
||||
},
|
||||
"time": "2026-04-08T04:11:31+00:00"
|
||||
"time": "2026-04-17T09:57:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/pools",
|
||||
@@ -5016,16 +5092,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/storage",
|
||||
"version": "1.0.1",
|
||||
"version": "2.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/storage.git",
|
||||
"reference": "f014be445f0baa635d0764e1673196f412511618"
|
||||
"reference": "8a2e3a86fd01aaed675884146665308c2122264e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/storage/zipball/f014be445f0baa635d0764e1673196f412511618",
|
||||
"reference": "f014be445f0baa635d0764e1673196f412511618",
|
||||
"url": "https://api.github.com/repos/utopia-php/storage/zipball/8a2e3a86fd01aaed675884146665308c2122264e",
|
||||
"reference": "8a2e3a86fd01aaed675884146665308c2122264e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5039,9 +5115,8 @@
|
||||
"utopia-php/validators": "0.2.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.2.*",
|
||||
"phpunit/phpunit": "^9.3",
|
||||
"vimeo/psalm": "4.0.1"
|
||||
"laravel/pint": "^1.21",
|
||||
"phpunit/phpunit": "^9.3"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -5063,9 +5138,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/storage/issues",
|
||||
"source": "https://github.com/utopia-php/storage/tree/1.0.1"
|
||||
"source": "https://github.com/utopia-php/storage/tree/2.0.1"
|
||||
},
|
||||
"time": "2026-02-23T05:59:32+00:00"
|
||||
"time": "2026-04-29T09:05:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/system",
|
||||
@@ -5180,16 +5255,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/validators",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/validators.git",
|
||||
"reference": "30b6030a5b100fc1dff34506e5053759594b2a20"
|
||||
"reference": "5d7d494e64457cd4eb67fdcfd9481f2c89796aa6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/validators/zipball/30b6030a5b100fc1dff34506e5053759594b2a20",
|
||||
"reference": "30b6030a5b100fc1dff34506e5053759594b2a20",
|
||||
"url": "https://api.github.com/repos/utopia-php/validators/zipball/5d7d494e64457cd4eb67fdcfd9481f2c89796aa6",
|
||||
"reference": "5d7d494e64457cd4eb67fdcfd9481f2c89796aa6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5219,9 +5294,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/validators/issues",
|
||||
"source": "https://github.com/utopia-php/validators/tree/0.2.0"
|
||||
"source": "https://github.com/utopia-php/validators/tree/0.2.2"
|
||||
},
|
||||
"time": "2026-01-13T09:16:51+00:00"
|
||||
"time": "2026-04-27T16:30:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/vcs",
|
||||
@@ -5462,16 +5537,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.17.11",
|
||||
"version": "1.25.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "c714ee52659ef5968b3372ff4da0e407140a6250"
|
||||
"reference": "f21a556b9acdbf75bbdcdc90a078af641646eade"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/c714ee52659ef5968b3372ff4da0e407140a6250",
|
||||
"reference": "c714ee52659ef5968b3372ff4da0e407140a6250",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f21a556b9acdbf75bbdcdc90a078af641646eade",
|
||||
"reference": "f21a556b9acdbf75bbdcdc90a078af641646eade",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5507,9 +5582,9 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.17.11"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.25.1"
|
||||
},
|
||||
"time": "2026-04-11T02:42:32+00:00"
|
||||
"time": "2026-04-28T11:12:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
@@ -5791,16 +5866,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/pint",
|
||||
"version": "v1.29.0",
|
||||
"version": "v1.29.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/pint.git",
|
||||
"reference": "bdec963f53172c5e36330f3a400604c69bf02d39"
|
||||
"reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39",
|
||||
"reference": "bdec963f53172c5e36330f3a400604c69bf02d39",
|
||||
"url": "https://api.github.com/repos/laravel/pint/zipball/0770e9b7fafd50d4586881d456d6eb41c9247a80",
|
||||
"reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5811,14 +5886,14 @@
|
||||
"php": "^8.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.94.2",
|
||||
"illuminate/view": "^12.54.1",
|
||||
"larastan/larastan": "^3.9.3",
|
||||
"laravel-zero/framework": "^12.0.5",
|
||||
"friendsofphp/php-cs-fixer": "^3.95.1",
|
||||
"illuminate/view": "^12.56.0",
|
||||
"larastan/larastan": "^3.9.6",
|
||||
"laravel-zero/framework": "^12.1.0",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"nunomaduro/termwind": "^2.4.0",
|
||||
"pestphp/pest": "^3.8.6",
|
||||
"shipfastlabs/agent-detector": "^1.1.0"
|
||||
"shipfastlabs/agent-detector": "^1.1.3"
|
||||
},
|
||||
"bin": [
|
||||
"builds/pint"
|
||||
@@ -5855,7 +5930,7 @@
|
||||
"issues": "https://github.com/laravel/pint/issues",
|
||||
"source": "https://github.com/laravel/pint"
|
||||
},
|
||||
"time": "2026-03-12T15:51:39+00:00"
|
||||
"time": "2026-04-20T15:26:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "matthiasmullie/minify",
|
||||
@@ -6218,11 +6293,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.46",
|
||||
"version": "2.1.54",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
|
||||
"reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
|
||||
"reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6267,20 +6342,20 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-01T09:25:14+00:00"
|
||||
"time": "2026-04-29T13:31:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "12.5.3",
|
||||
"version": "12.5.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
|
||||
"reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d"
|
||||
"reference": "876099a072646c7745f673d7aeab5382c4439691"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d",
|
||||
"reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/876099a072646c7745f673d7aeab5382c4439691",
|
||||
"reference": "876099a072646c7745f673d7aeab5382c4439691",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6289,7 +6364,6 @@
|
||||
"ext-xmlwriter": "*",
|
||||
"nikic/php-parser": "^5.7.0",
|
||||
"php": ">=8.3",
|
||||
"phpunit/php-file-iterator": "^6.0",
|
||||
"phpunit/php-text-template": "^5.0",
|
||||
"sebastian/complexity": "^5.0",
|
||||
"sebastian/environment": "^8.0.3",
|
||||
@@ -6336,7 +6410,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
|
||||
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.3"
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6356,7 +6430,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-06T06:01:44+00:00"
|
||||
"time": "2026-04-15T08:23:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-file-iterator",
|
||||
@@ -6617,16 +6691,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "12.5.17",
|
||||
"version": "12.5.23",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "85b62adab1a340982df64e66daa4a4435eb5723b"
|
||||
"reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/85b62adab1a340982df64e66daa4a4435eb5723b",
|
||||
"reference": "85b62adab1a340982df64e66daa4a4435eb5723b",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969",
|
||||
"reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6640,15 +6714,15 @@
|
||||
"phar-io/manifest": "^2.0.4",
|
||||
"phar-io/version": "^3.2.1",
|
||||
"php": ">=8.3",
|
||||
"phpunit/php-code-coverage": "^12.5.3",
|
||||
"phpunit/php-code-coverage": "^12.5.6",
|
||||
"phpunit/php-file-iterator": "^6.0.1",
|
||||
"phpunit/php-invoker": "^6.0.0",
|
||||
"phpunit/php-text-template": "^5.0.0",
|
||||
"phpunit/php-timer": "^8.0.0",
|
||||
"sebastian/cli-parser": "^4.2.0",
|
||||
"sebastian/comparator": "^7.1.4",
|
||||
"sebastian/comparator": "^7.1.6",
|
||||
"sebastian/diff": "^7.0.0",
|
||||
"sebastian/environment": "^8.0.4",
|
||||
"sebastian/environment": "^8.1.0",
|
||||
"sebastian/exporter": "^7.0.2",
|
||||
"sebastian/global-state": "^8.0.2",
|
||||
"sebastian/object-enumerator": "^7.0.0",
|
||||
@@ -6695,7 +6769,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.17"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.23"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6703,7 +6777,7 @@
|
||||
"type": "other"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-08T03:04:19+00:00"
|
||||
"time": "2026-04-18T06:12:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
@@ -6776,16 +6850,16 @@
|
||||
},
|
||||
{
|
||||
"name": "sebastian/comparator",
|
||||
"version": "7.1.5",
|
||||
"version": "7.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/comparator.git",
|
||||
"reference": "c284f55811f43d555e51e8e5c166ac40d3e33c63"
|
||||
"reference": "c769009dee98f494e0edc3fd4f4087501688f11e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c284f55811f43d555e51e8e5c166ac40d3e33c63",
|
||||
"reference": "c284f55811f43d555e51e8e5c166ac40d3e33c63",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c769009dee98f494e0edc3fd4f4087501688f11e",
|
||||
"reference": "c769009dee98f494e0edc3fd4f4087501688f11e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6844,7 +6918,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/comparator/issues",
|
||||
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/comparator/tree/7.1.5"
|
||||
"source": "https://github.com/sebastianbergmann/comparator/tree/7.1.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6864,7 +6938,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-08T04:43:00+00:00"
|
||||
"time": "2026-04-14T08:23:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/complexity",
|
||||
@@ -6993,16 +7067,16 @@
|
||||
},
|
||||
{
|
||||
"name": "sebastian/environment",
|
||||
"version": "8.0.4",
|
||||
"version": "8.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/environment.git",
|
||||
"reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11"
|
||||
"reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11",
|
||||
"reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/b121608b28a13f721e76ffbbd386d08eff58f3f6",
|
||||
"reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7017,7 +7091,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "8.0-dev"
|
||||
"dev-main": "8.1-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -7045,7 +7119,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/environment/issues",
|
||||
"security": "https://github.com/sebastianbergmann/environment/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/environment/tree/8.0.4"
|
||||
"source": "https://github.com/sebastianbergmann/environment/tree/8.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7065,7 +7139,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-15T07:05:40+00:00"
|
||||
"time": "2026-04-15T12:13:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/exporter",
|
||||
@@ -7778,7 +7852,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
@@ -7837,7 +7911,7 @@
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7861,16 +7935,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
|
||||
"reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df"
|
||||
"reference": "4864388bfbd3001ce88e234fab652acd91fdc57e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
|
||||
"reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e",
|
||||
"reference": "4864388bfbd3001ce88e234fab652acd91fdc57e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7919,7 +7993,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7939,11 +8013,11 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-10T16:19:22+00:00"
|
||||
"time": "2026-04-26T13:13:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
@@ -8004,7 +8078,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8028,7 +8102,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php81",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php81.git",
|
||||
@@ -8084,7 +8158,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
||||
+5
-3
@@ -212,6 +212,7 @@ services:
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_LOCKING_ENABLED
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_CACHE
|
||||
@@ -242,19 +243,20 @@ services:
|
||||
- _APP_EXPERIMENT_LOGGING_PROVIDER
|
||||
- _APP_EXPERIMENT_LOGGING_CONFIG
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
- _APP_DATABASE_SHARED_TABLES_V1
|
||||
- _APP_DATABASE_SHARED_NAMESPACE
|
||||
- _APP_FUNCTIONS_CREATION_ABUSE_LIMIT
|
||||
- _APP_CUSTOM_DOMAIN_DENY_LIST
|
||||
- _APP_TRUSTED_HEADERS
|
||||
- _APP_MIGRATION_HOST
|
||||
- _TESTS_OAUTH2_GITHUB_CLIENT_ID
|
||||
- _TESTS_OAUTH2_GITHUB_CLIENT_SECRET
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
appwrite-console:
|
||||
<<: *x-logging
|
||||
container_name: appwrite-console
|
||||
image: appwrite/console:7.8.26
|
||||
image: appwrite/console:7.8.45
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
@@ -462,7 +464,6 @@ services:
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
- _APP_DATABASE_SHARED_TABLES_V1
|
||||
- _APP_EMAIL_CERTIFICATES
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE
|
||||
@@ -1288,6 +1289,7 @@ services:
|
||||
image: mongo:8.2.5
|
||||
container_name: appwrite-mongodb
|
||||
<<: *x-logging
|
||||
restart: on-failure:3
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Initialize an MFA challenge of the specified factor. The factor must be available on the account.
|
||||
@@ -1 +0,0 @@
|
||||
Use this endpoint to log out the currently logged in user from their account. When successful this endpoint will delete the user session and remove the session secret cookie from the user client.
|
||||
@@ -1 +0,0 @@
|
||||
Get all Environment Variables that are relevant for the console.
|
||||
@@ -1 +0,0 @@
|
||||
Get the collection activity logs list by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get the document activity logs list by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
List attributes in the collection.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new build for an existing function deployment. This endpoint allows you to rebuild a deployment with the updated function configuration, including its entrypoint and build commands if they have been modified. The build process will be queued and executed asynchronously. The original deployment's code will be preserved and used for the new build.
|
||||
@@ -1,5 +0,0 @@
|
||||
Create a new function code deployment. Use this endpoint to upload a new version of your code function. To execute your newly uploaded code, you'll need to update the function's deployment to use your new deployment UID.
|
||||
|
||||
This endpoint accepts a tar.gz file compressed with your code. Make sure to include any dependencies your code has within the compressed file. You can learn more about code packaging in the [Appwrite Cloud Functions tutorial](https://appwrite.io/docs/functions).
|
||||
|
||||
Use the "command" param to set the entrypoint used to execute your code.
|
||||
@@ -1 +0,0 @@
|
||||
Trigger a function execution. The returned object will return you the current execution status. You can ping the `Get Execution` endpoint to get updates on the current execution status. Once this endpoint is called, your function execution process will start asynchronously.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new function. You can pass a list of [permissions](https://appwrite.io/docs/permissions) to allow different project users or team with access to execute the function using the client API.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new function environment variable. These variables can be accessed in the function at runtime as environment variables.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a code deployment by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a function execution by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a function by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a variable by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get a Deployment's contents by its unique ID. This endpoint supports range requests for partial or streaming file download.
|
||||
@@ -1 +0,0 @@
|
||||
Get a code deployment by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get a function execution log by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get usage metrics and statistics for a for a specific function. View statistics including total deployments, builds, executions, storage usage, and compute time. The response includes both current totals and historical data for each metric. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, defaults to 30 days.
|
||||
@@ -1 +0,0 @@
|
||||
Get a function by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get usage metrics and statistics for a for all functions. View statistics including total functions, deployments, builds, executions, storage usage, and compute time. The response includes both current totals and historical data for each metric. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, defaults to 30 days.
|
||||
@@ -1 +0,0 @@
|
||||
Get a function template using ID. You can use template details in [createFunction](/docs/references/cloud/server-nodejs/functions#create) method.
|
||||
@@ -1 +0,0 @@
|
||||
Get a variable by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get a list of all the function's code deployments. You can use the query params to filter your results.
|
||||
@@ -1 +0,0 @@
|
||||
Get a list of all the current user function execution logs. You can use the query params to filter your results.
|
||||
@@ -1 +0,0 @@
|
||||
Get a list of all the project's functions. You can use the query params to filter your results.
|
||||
@@ -1 +0,0 @@
|
||||
Get a list of all runtimes that are currently active on your instance.
|
||||
@@ -1 +0,0 @@
|
||||
List allowed function specifications for this instance.
|
||||
@@ -1 +0,0 @@
|
||||
List available function templates. You can use template details in [createFunction](/docs/references/cloud/server-nodejs/functions#create) method.
|
||||
@@ -1 +0,0 @@
|
||||
Get a list of all variables of a specific function.
|
||||
@@ -1 +0,0 @@
|
||||
Cancel an ongoing function deployment build. If the build is already in progress, it will be stopped and marked as canceled. If the build hasn't started yet, it will be marked as canceled without executing. You cannot cancel builds that have already completed (status 'ready') or failed. The response includes the final build status and details.
|
||||
@@ -1 +0,0 @@
|
||||
Update the function code deployment ID using the unique function ID. Use this endpoint to switch the code deployment that should be executed by the execution endpoint.
|
||||
@@ -1 +0,0 @@
|
||||
Update function by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Update variable by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get the number of projects containing metrics that are waiting to be processed in the Appwrite internal queue server.
|
||||
@@ -1 +0,0 @@
|
||||
Get the number of tasks that are waiting to be processed in the Appwrite internal queue server.
|
||||
@@ -1 +0,0 @@
|
||||
Check the Appwrite queue messaging servers are up and connection is successful.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a message by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new project variable. This variable will be accessible in all Appwrite Functions at runtime.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a project variable by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get a project variable by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get a list of all project variables. These variables will be accessible in all Appwrite Functions at runtime.
|
||||
@@ -1 +0,0 @@
|
||||
Update project variable by its unique ID. This variable will be accessible in all Appwrite Functions at runtime.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new API key. It's recommended to have multiple API keys with strict scopes for separate functions within your project.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new platform for your project. Use this endpoint to register a new platform where your users will run your application which will interact with the Appwrite API.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new webhook. Use this endpoint to configure a URL that will receive events from Appwrite when specific events occur.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a key by its unique ID. Once deleted, the key can no longer be used to authenticate API calls.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a platform by its unique ID. This endpoint removes the platform and all its configurations from the project.
|
||||
@@ -1 +0,0 @@
|
||||
Reset a custom SMS template to its default value. This endpoint removes any custom message and restores the template to its original state.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a webhook by its unique ID. Once deleted, the webhook will no longer receive project events.
|
||||
@@ -1 +0,0 @@
|
||||
Get a key by its unique ID. This endpoint returns details about a specific API key in your project including it's scopes.
|
||||
@@ -1 +0,0 @@
|
||||
Get a platform by its unique ID. This endpoint returns the platform's details, including its name, type, and key configurations.
|
||||
@@ -1 +0,0 @@
|
||||
Get a custom SMS template for the specified locale and type returning it's contents.
|
||||
@@ -1 +0,0 @@
|
||||
Get a webhook by its unique ID. This endpoint returns details about a specific webhook configured for a project.
|
||||
@@ -1 +0,0 @@
|
||||
Get a list of all API keys from the current project.
|
||||
@@ -1 +0,0 @@
|
||||
Get a list of all platforms in the project. This endpoint returns an array of all platforms and their configurations.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user