mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
454
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a92ad3aa8b | ||
|
|
4de9ec7fba | ||
|
|
a659038ad2 | ||
|
|
9564c9b065 | ||
|
|
4da726029c | ||
|
|
f016d4b7cd | ||
|
|
2a7925b362 | ||
|
|
cfe600c653 | ||
|
|
22e1969895 | ||
|
|
49688a6c75 | ||
|
|
d0978d891f | ||
|
|
0cf206cacf | ||
|
|
b9b5d396b8 | ||
|
|
2b33dc3c72 | ||
|
|
9e595588bc | ||
|
|
20bd7af370 | ||
|
|
5ca30d37f7 | ||
|
|
76684874e9 | ||
|
|
4fffeda596 | ||
|
|
1acfef5f5d | ||
|
|
d4f7d51df8 | ||
|
|
5b5020fda4 | ||
|
|
a1441174f2 | ||
|
|
e58f0b6378 | ||
|
|
c5c78e0f2b | ||
|
|
2e5d7a53eb | ||
|
|
60b5f4433c | ||
|
|
56ce0d4b47 | ||
|
|
8666bf1325 | ||
|
|
d53cad2b0f | ||
|
|
9d976fdc9f | ||
|
|
995deea801 | ||
|
|
0114e260f0 | ||
|
|
7ab474b963 | ||
|
|
10da066075 | ||
|
|
01142bba2c | ||
|
|
71146118d2 | ||
|
|
682105c068 | ||
|
|
07ff923d38 | ||
|
|
2c5e029116 | ||
|
|
336bc3a74f | ||
|
|
8a80ec12ed | ||
|
|
b7f06a76aa | ||
|
|
a9acba916a | ||
|
|
fa97823385 | ||
|
|
8ae07ac61f | ||
|
|
ffbe17f34e | ||
|
|
5466f55cfa | ||
|
|
6c8338c3e9 | ||
|
|
28ba11c71e | ||
|
|
a14d51321a | ||
|
|
d8f3d581cc | ||
|
|
be76990bb6 | ||
|
|
6e59e1307d | ||
|
|
24848a872c | ||
|
|
b630426893 | ||
|
|
0a5a8c5b6c | ||
|
|
875637bf35 | ||
|
|
1aa86708f3 | ||
|
|
6410c2dcdf | ||
|
|
3a40b6b629 | ||
|
|
ef0954cdda | ||
|
|
d4c9af2eb2 | ||
|
|
3baf2681df | ||
|
|
1ad2cd68ef | ||
|
|
04680d68f5 | ||
|
|
9a70445395 | ||
|
|
dabe98d4a1 | ||
|
|
c9d023991d | ||
|
|
91e252382b | ||
|
|
47408f04dc | ||
|
|
827dd2d039 | ||
|
|
ed1ab85356 | ||
|
|
c155c88a31 | ||
|
|
df58831d92 | ||
|
|
aef9f447f7 | ||
|
|
48e99e70ac | ||
|
|
2007fbf241 | ||
|
|
2673af7b83 | ||
|
|
21d7e2dfb5 | ||
|
|
9235a4cc15 | ||
|
|
fb21211ae8 | ||
|
|
616762f696 | ||
|
|
3b5ba2a2b0 | ||
|
|
7ae1cf4db0 | ||
|
|
c537d09b5c | ||
|
|
efeb3b4a00 | ||
|
|
aa89128d10 | ||
|
|
e8c0ab0cc1 | ||
|
|
b81f3f8267 | ||
|
|
b79132556c | ||
|
|
99c76689ee | ||
|
|
c7907932e4 | ||
|
|
bdd3c2f9f5 | ||
|
|
1754d6cc81 | ||
|
|
351efa6cf2 | ||
|
|
1aa2c7ff2b | ||
|
|
9917f95dfd | ||
|
|
8af0efafd4 | ||
|
|
7e7cac017c | ||
|
|
b8cb146eb9 | ||
|
|
9ed6ac89f5 | ||
|
|
09313d0500 | ||
|
|
3a9e4305d3 | ||
|
|
4b163c02ad | ||
|
|
331fb7f689 | ||
|
|
b80d76e287 | ||
|
|
feddd77066 | ||
|
|
a55f3e1db8 | ||
|
|
9bc263ab22 | ||
|
|
6c76783c09 | ||
|
|
835bb49562 | ||
|
|
ab43d4995b | ||
|
|
f1e2ce3e09 | ||
|
|
6fa34aef04 | ||
|
|
d8748c9054 | ||
|
|
6f8a54273d | ||
|
|
37a7c70c2b | ||
|
|
7071e6f080 | ||
|
|
4c1a360797 | ||
|
|
29fcdcd766 | ||
|
|
62f91f746e | ||
|
|
d27aad6e67 | ||
|
|
221b5d104c | ||
|
|
826ac1abe9 | ||
|
|
e1c327235a | ||
|
|
4bfb958146 | ||
|
|
33e3e5e63d | ||
|
|
564f56e0f5 | ||
|
|
1a6a66406b | ||
|
|
84316fdfb5 | ||
|
|
a6eac0a01e | ||
|
|
412d858c49 | ||
|
|
ba94bff8d4 | ||
|
|
979cfb0ba8 | ||
|
|
85fcc52b84 | ||
|
|
6b851cccdb | ||
|
|
540979f9fb | ||
|
|
2add5e2d25 | ||
|
|
8d58383c2e | ||
|
|
a3bb7af634 | ||
|
|
114016bbca | ||
|
|
8ba35d8810 | ||
|
|
afd8d8a020 | ||
|
|
90f0282ce3 | ||
|
|
468eff690d | ||
|
|
e13fe32f78 | ||
|
|
f6d38fe1ce | ||
|
|
b85cf2fdb6 | ||
|
|
d8df5f1ea1 | ||
|
|
10c587cbc2 | ||
|
|
e409524033 | ||
|
|
29d9c138c9 | ||
|
|
8304a8e0e4 | ||
|
|
64dc730a3b | ||
|
|
3096404db5 | ||
|
|
6818f0b1c3 | ||
|
|
3ddc073874 | ||
|
|
4f3468de7b | ||
|
|
7a9d7202e5 | ||
|
|
09bfab03a7 | ||
|
|
52cd8b7992 | ||
|
|
1cb7ea49ec | ||
|
|
9989820e17 | ||
|
|
798c605492 | ||
|
|
ffa4645d25 | ||
|
|
ca0656ce35 | ||
|
|
e4178d9ae6 | ||
|
|
843dfa5c6c | ||
|
|
52e2276445 | ||
|
|
9f78fc5341 | ||
|
|
bf0d49cc76 | ||
|
|
b5b2338c82 | ||
|
|
cef4d899e3 | ||
|
|
f6655343c7 | ||
|
|
4efababbb4 | ||
|
|
5a258b9da1 | ||
|
|
ef248bd5fb | ||
|
|
9c737d906b | ||
|
|
b8d2f7ece5 | ||
|
|
5990422a2d | ||
|
|
ec500939df | ||
|
|
fddaebf254 | ||
|
|
d6bc9f120e | ||
|
|
1705266779 | ||
|
|
97d64c3e01 | ||
|
|
f839cf79d7 | ||
|
|
844891c0ae | ||
|
|
c417206e7c | ||
|
|
5326e6571b | ||
|
|
681d930da3 | ||
|
|
f102419d7c | ||
|
|
a863ac773a | ||
|
|
26e92c6a8a | ||
|
|
9f5172c1e9 | ||
|
|
519ade1e51 | ||
|
|
356cd0287c | ||
|
|
e84a26be76 | ||
|
|
d6c8aeee2a | ||
|
|
8c3f11b3d5 | ||
|
|
e37a9ad059 | ||
|
|
9fc11ce475 | ||
|
|
9a32c59340 | ||
|
|
c78d19de0d | ||
|
|
1353f1f56c | ||
|
|
ee34d83cd8 | ||
|
|
d68449d25b | ||
|
|
d34c83a798 | ||
|
|
6e485bcd5a | ||
|
|
bab3a49b0b | ||
|
|
55796eca5c | ||
|
|
abf497d509 | ||
|
|
a76a5cda38 | ||
|
|
377a9d915e | ||
|
|
c7b3972fa0 | ||
|
|
fda53c0520 | ||
|
|
f2e789537c | ||
|
|
3287ef6a57 | ||
|
|
74b3d8f493 | ||
|
|
e0972f4636 | ||
|
|
42f914f6aa | ||
|
|
58f4fff864 | ||
|
|
681c9743b1 | ||
|
|
cd2d2a1238 | ||
|
|
905763e755 | ||
|
|
2550482432 | ||
|
|
8d028066ae | ||
|
|
bd4adad545 | ||
|
|
c40f9b1de0 | ||
|
|
8237250453 | ||
|
|
c8a7ed2022 | ||
|
|
4847c5a308 | ||
|
|
f6985bae41 | ||
|
|
cc9df4f5c5 | ||
|
|
83b7b4e599 | ||
|
|
fdc573009c | ||
|
|
51d769b0a1 | ||
|
|
f6b04d05b2 | ||
|
|
2926412604 | ||
|
|
ee21b1feda | ||
|
|
2d0140f705 | ||
|
|
183bd4143c | ||
|
|
57ac67a106 | ||
|
|
4b6d809a3b | ||
|
|
0221556ae7 | ||
|
|
6a1955d20d | ||
|
|
8c7d2c58f5 | ||
|
|
5b7746b464 | ||
|
|
e6e0cef91e | ||
|
|
bdc4c4a18d | ||
|
|
49efa02e09 | ||
|
|
fd96bcbd3f | ||
|
|
9647c3fa7d | ||
|
|
1e411bbef5 | ||
|
|
7c47a4db72 | ||
|
|
ae33104ecf | ||
|
|
b90a9e8cab | ||
|
|
4024cf0492 | ||
|
|
e07f9a7b49 | ||
|
|
61f778196b | ||
|
|
93897f86d0 | ||
|
|
095e08441f | ||
|
|
da63463a3c | ||
|
|
8e140bae6c | ||
|
|
3b2eb02f8a | ||
|
|
e53f5b4023 | ||
|
|
02cddb5c08 | ||
|
|
cb4d11a1d4 | ||
|
|
139535e83c | ||
|
|
b9e95f6d82 | ||
|
|
79cb668a50 | ||
|
|
8078516e2f | ||
|
|
1729bd23b2 | ||
|
|
ac7fc7ae69 | ||
|
|
4b570dc3f0 | ||
|
|
aa3d66df7b | ||
|
|
a16e05ed4d | ||
|
|
84dc5682d5 | ||
|
|
a112de41c5 | ||
|
|
02bba85fe7 | ||
|
|
56f68d33df | ||
|
|
d4edf9ed13 | ||
|
|
12b38e2a27 | ||
|
|
9f1807a128 | ||
|
|
6bdfcc3fb5 | ||
|
|
d8dd1918c4 | ||
|
|
376c5e47f5 | ||
|
|
edfb9e0bd7 | ||
|
|
eab4c7f28a | ||
|
|
c361340252 | ||
|
|
41e197003e | ||
|
|
c7ba37ec49 | ||
|
|
ba82aab563 | ||
|
|
f580dc9145 | ||
|
|
421dca2f99 | ||
|
|
991b2b1c3b | ||
|
|
d22743a1cf | ||
|
|
a2039c0b76 | ||
|
|
c549ddd149 | ||
|
|
f3721f712a | ||
|
|
480757dcfe | ||
|
|
fbaeff8920 | ||
|
|
de88ec4065 | ||
|
|
f149ba33d4 | ||
|
|
b15f2eed8f | ||
|
|
ec63dc22d4 | ||
|
|
7c7b98399c | ||
|
|
9d679e726e | ||
|
|
070ec03c0c | ||
|
|
be89224c36 | ||
|
|
8edd3c87cb | ||
|
|
9ac3cd73a8 | ||
|
|
203170c36d | ||
|
|
6f5bfdb726 | ||
|
|
b499857871 | ||
|
|
1388b0ca21 | ||
|
|
af095bdf4a | ||
|
|
7af361fa16 | ||
|
|
069f231b3a | ||
|
|
def028e970 | ||
|
|
16f49d13c8 | ||
|
|
e72d7979e0 | ||
|
|
ca4c7b5361 | ||
|
|
7ad6bb048c | ||
|
|
d2c9ac079a | ||
|
|
abe1257bd5 | ||
|
|
3db22fd2db | ||
|
|
a423067b36 | ||
|
|
33aab0357f | ||
|
|
34e9c69a62 | ||
|
|
fdf5ca0067 | ||
|
|
6eb17b847f | ||
|
|
918ebb8ecc | ||
|
|
c0ac52810e | ||
|
|
b658cddc71 | ||
|
|
fe753db78c | ||
|
|
2f1e47e35a | ||
|
|
c3ad337398 | ||
|
|
cc759868b8 | ||
|
|
1004435e9e | ||
|
|
a9e749a81f | ||
|
|
553fe15ba9 | ||
|
|
fec758d237 | ||
|
|
70a1d5d0f7 | ||
|
|
c66308e4e9 | ||
|
|
21aae468f2 | ||
|
|
f007b83ccc | ||
|
|
28be12d827 | ||
|
|
93bd6b39a4 | ||
|
|
75045ffd58 | ||
|
|
ed8419986a | ||
|
|
954011b2c4 | ||
|
|
c3f221f1fb | ||
|
|
de59f53de3 | ||
|
|
04b169fab8 | ||
|
|
857ec3b099 | ||
|
|
b1dd377b67 | ||
|
|
81128e8446 | ||
|
|
86371e2082 | ||
|
|
bd6f62dd68 | ||
|
|
d2275c05fb | ||
|
|
4988881568 | ||
|
|
f9be1ddfc5 | ||
|
|
60e95458a7 | ||
|
|
2bb6b0a2a9 | ||
|
|
3311bd27cb | ||
|
|
5d9ef0dcca | ||
|
|
7ddecdd9ef | ||
|
|
c8f966b0e5 | ||
|
|
3d2f21be7c | ||
|
|
3e876f5bd8 | ||
|
|
a5e56438c7 | ||
|
|
4e79259c4e | ||
|
|
fae79e5c3f | ||
|
|
b169be9dda | ||
|
|
8f4496346d | ||
|
|
f306c03030 | ||
|
|
a8469a9b92 | ||
|
|
b63510acb5 | ||
|
|
3e612ec88d | ||
|
|
1be36ae552 | ||
|
|
445b657207 | ||
|
|
5098973e0d | ||
|
|
d0976ff429 | ||
|
|
157783f782 | ||
|
|
e3a70373e1 | ||
|
|
a6e5563f8e | ||
|
|
470d84a843 | ||
|
|
80037f7e8d | ||
|
|
cc848c90a7 | ||
|
|
e0c293c5b4 | ||
|
|
2d82013261 | ||
|
|
5a6561f158 | ||
|
|
f28588ecb6 | ||
|
|
6579ea5b56 | ||
|
|
9373688be2 | ||
|
|
2cec06be18 | ||
|
|
3a60bb306c | ||
|
|
e2ddb0d6d0 | ||
|
|
0049265c8a | ||
|
|
1f27867565 | ||
|
|
faf0cdec35 | ||
|
|
6073eab496 | ||
|
|
50ddc611a4 | ||
|
|
0ef2cfa753 | ||
|
|
790e503ad9 | ||
|
|
f6dbc0c882 | ||
|
|
a6c226aac9 | ||
|
|
2d41a3353d | ||
|
|
34e4208b3e | ||
|
|
d51486c62a | ||
|
|
f62f00d4d9 | ||
|
|
44e6ef3146 | ||
|
|
dea49d54c9 | ||
|
|
8a9523fa48 | ||
|
|
5dba0183c0 | ||
|
|
0108f86180 | ||
|
|
bc71d83697 | ||
|
|
0754c6263e | ||
|
|
2c037803f0 | ||
|
|
48410d0cf6 | ||
|
|
66b0f3c9df | ||
|
|
d8910eea73 | ||
|
|
f7f965a812 | ||
|
|
d806bdbca7 | ||
|
|
19b1196633 | ||
|
|
88f2b1ad50 | ||
|
|
f5ea19b15d | ||
|
|
d2645ecfb1 | ||
|
|
44b75d50f4 | ||
|
|
df23aacbc0 | ||
|
|
7d4dfe06e2 | ||
|
|
e50af4c981 | ||
|
|
4fdab6fd52 | ||
|
|
c54450f582 | ||
|
|
6bf9693146 | ||
|
|
422fd8b157 | ||
|
|
bf061b460b | ||
|
|
a035760b00 | ||
|
|
85c0454c49 | ||
|
|
7bfb0401c6 | ||
|
|
c097f4a450 | ||
|
|
a3faa108c5 | ||
|
|
c89b76ba80 | ||
|
|
80636e8a91 | ||
|
|
206845e106 | ||
|
|
64d015065f | ||
|
|
56af56e341 | ||
|
|
3086e1cbc1 | ||
|
|
b032a55045 | ||
|
|
d71c56b11f | ||
|
|
98ee242812 | ||
|
|
79267c8123 | ||
|
|
d2ad0e7c74 |
@@ -39,7 +39,7 @@ _APP_REDIS_HOST=redis
|
||||
_APP_REDIS_PORT=6379
|
||||
_APP_REDIS_PASS=
|
||||
_APP_REDIS_USER=
|
||||
COMPOSE_PROFILES=mongodb
|
||||
COMPOSE_PROFILES=mariadb,mongodb,postgresql
|
||||
_APP_DB_ADAPTER=mongodb
|
||||
_APP_DB_HOST=mongodb
|
||||
_APP_DB_PORT=27017
|
||||
@@ -47,6 +47,15 @@ _APP_DB_SCHEMA=appwrite
|
||||
_APP_DB_USER=user
|
||||
_APP_DB_PASS=password
|
||||
_APP_DB_ROOT_PASS=rootsecretpassword
|
||||
_APP_DB_ADAPTER_DOCUMENTSDB=mongodb
|
||||
_APP_DB_HOST_DOCUMENTSDB=mongodb
|
||||
_APP_DB_PORT_DOCUMENTSDB=27017
|
||||
_APP_DB_ADAPTER_VECTORSDB=postgresql
|
||||
_APP_DB_HOST_VECTORSDB=postgresql
|
||||
_APP_DB_PORT_VECTORSDB=5432
|
||||
_APP_EMBEDDING_MODELS=embeddinggemma
|
||||
_APP_EMBEDDING_ENDPOINT='http://ollama:11434/api/embed'
|
||||
_APP_EMBEDDING_TIMEOUT=30000
|
||||
_APP_STORAGE_DEVICE=Local
|
||||
_APP_STORAGE_S3_ACCESS_KEY=
|
||||
_APP_STORAGE_S3_SECRET=
|
||||
|
||||
+44
-36
@@ -7,7 +7,6 @@ concurrency:
|
||||
env:
|
||||
COMPOSE_FILE: docker-compose.yml
|
||||
IMAGE: appwrite-dev
|
||||
CACHE_KEY: appwrite-dev-${{ github.event.pull_request.head.sha }}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -248,11 +247,12 @@ jobs:
|
||||
TESTING=true
|
||||
VERSION=dev
|
||||
|
||||
- name: Cache Docker Image
|
||||
uses: actions/cache@v5
|
||||
- name: Upload Docker Image
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
retention-days: 1
|
||||
|
||||
unit:
|
||||
name: Tests / Unit
|
||||
@@ -265,12 +265,11 @@ jobs:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
@@ -313,12 +312,11 @@ jobs:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
@@ -363,7 +361,7 @@ jobs:
|
||||
|
||||
e2e_service:
|
||||
name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.runner || 'ubuntu-latest' }}
|
||||
needs: [build, matrix]
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -396,18 +394,31 @@ jobs:
|
||||
Webhooks,
|
||||
VCS,
|
||||
Messaging,
|
||||
Migrations
|
||||
Migrations,
|
||||
Project
|
||||
]
|
||||
include:
|
||||
- service: Databases
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Sites
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Functions
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Avatars
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Realtime
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: TablesDB
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Set database environment
|
||||
run: |
|
||||
@@ -496,12 +507,11 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
@@ -555,12 +565,11 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
@@ -616,12 +625,11 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Build the Docker image
|
||||
run: DOCKER_BUILDKIT=1 docker build . --target production -t appwrite_image:latest
|
||||
- name: Run Trivy vulnerability scanner on image
|
||||
uses: aquasecurity/trivy-action@0.20.0
|
||||
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
|
||||
with:
|
||||
image-ref: 'appwrite_image:latest'
|
||||
format: 'sarif'
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
- name: Run Trivy vulnerability scanner on filesystem
|
||||
uses: aquasecurity/trivy-action@0.20.0
|
||||
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
|
||||
with:
|
||||
scan-type: 'fs'
|
||||
format: 'sarif'
|
||||
|
||||
+119
-86
@@ -1,100 +1,133 @@
|
||||
# Version 1.8.1
|
||||
# Version 1.9.0
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Notable changes
|
||||
|
||||
* Add branch deployments support in [#10486](https://github.com/appwrite/appwrite/pull/10486)
|
||||
* Add TanStack Start sites support in [#10681](https://github.com/appwrite/appwrite/pull/10681)
|
||||
* Add Next.js standalone support in [#10747](https://github.com/appwrite/appwrite/pull/10747)
|
||||
* Add Resend integration in [#10690](https://github.com/appwrite/appwrite/pull/10690)
|
||||
* Add option to enable/disable image transformations per-bucket in [#10722](https://github.com/appwrite/appwrite/pull/10722)
|
||||
* Add operators support in [#10735](https://github.com/appwrite/appwrite/pull/10735) and [#10800](https://github.com/appwrite/appwrite/pull/10800)
|
||||
* Add function and sites stats in [#10786](https://github.com/appwrite/appwrite/pull/10786)
|
||||
* Add disable count feature in [#10668](https://github.com/appwrite/appwrite/pull/10668)
|
||||
* Add ElevenLabs site template in [#10782](https://github.com/appwrite/appwrite/pull/10782)
|
||||
* Add suggested environment variables in [#10795](https://github.com/appwrite/appwrite/pull/10795)
|
||||
* Update GeoDB database in [#10890](https://github.com/appwrite/appwrite/pull/10890)
|
||||
* Update Flutter default build runtime in [#10807](https://github.com/appwrite/appwrite/pull/10807)
|
||||
* Upgrade runtimes in [#10804](https://github.com/appwrite/appwrite/pull/10804)
|
||||
* Add PostgreSQL database adapter in [#9772](https://github.com/appwrite/appwrite/pull/9772) and [#11293](https://github.com/appwrite/appwrite/pull/11293)
|
||||
* Add MongoDB support in [#11312](https://github.com/appwrite/appwrite/pull/11312)
|
||||
* Add new webhooks API in [#11033](https://github.com/appwrite/appwrite/pull/11033) and [#11566](https://github.com/appwrite/appwrite/pull/11566)
|
||||
* Add schedules API endpoints in [#11331](https://github.com/appwrite/appwrite/pull/11331)
|
||||
* Add project labels in [#11056](https://github.com/appwrite/appwrite/pull/11056) and project status attribute in [#11291](https://github.com/appwrite/appwrite/pull/11291)
|
||||
* Add resource-based API key structure in [#11003](https://github.com/appwrite/appwrite/pull/11003) with custom ID support in [#11277](https://github.com/appwrite/appwrite/pull/11277) and list queries in [#11278](https://github.com/appwrite/appwrite/pull/11278)
|
||||
* Add string types (varchar, text, mediumtext, longtext) for attributes in [#11174](https://github.com/appwrite/appwrite/pull/11174)
|
||||
* Add encrypt parameter to string attribute types in [#11334](https://github.com/appwrite/appwrite/pull/11334)
|
||||
* Add int64 format support for integer attributes in [#11123](https://github.com/appwrite/appwrite/pull/11123)
|
||||
* Add collection and row storage size in [#11254](https://github.com/appwrite/appwrite/pull/11254) and [#11069](https://github.com/appwrite/appwrite/pull/11069)
|
||||
* Add totalSize on list responses in [#11102](https://github.com/appwrite/appwrite/pull/11102)
|
||||
* Add custom start command for sites and functions in [#10842](https://github.com/appwrite/appwrite/pull/10842)
|
||||
* Add separate build/runtime specifications in [#10849](https://github.com/appwrite/appwrite/pull/10849)
|
||||
* Add deployment retention for sites and functions in [#10959](https://github.com/appwrite/appwrite/pull/10959)
|
||||
* Add auto-delete old deployments in [#10959](https://github.com/appwrite/appwrite/pull/10959)
|
||||
* Add custom JWT duration in [#11009](https://github.com/appwrite/appwrite/pull/11009)
|
||||
* Add multiple application domains support in [#10911](https://github.com/appwrite/appwrite/pull/10911)
|
||||
* Add GraphQL introspection in [#11159](https://github.com/appwrite/appwrite/pull/11159)
|
||||
* Add realtime query subscriptions in [#11202](https://github.com/appwrite/appwrite/pull/11202) and [#11237](https://github.com/appwrite/appwrite/pull/11237)
|
||||
* Add realtime metrics for connections, messages, and bandwidth in [#11438](https://github.com/appwrite/appwrite/pull/11438) and [#11488](https://github.com/appwrite/appwrite/pull/11488)
|
||||
* Add messaging resource migration support in [#11495](https://github.com/appwrite/appwrite/pull/11495)
|
||||
* Add cached documents list in [#10832](https://github.com/appwrite/appwrite/pull/10832)
|
||||
* Add project queries support in [#10990](https://github.com/appwrite/appwrite/pull/10990)
|
||||
* Add batch document creation in [#10894](https://github.com/appwrite/appwrite/pull/10894)
|
||||
* Add async screenshots in [#11110](https://github.com/appwrite/appwrite/pull/11110)
|
||||
* Add new file parameters (encryption, compression) in [#11135](https://github.com/appwrite/appwrite/pull/11135)
|
||||
* Add new site templates in [#10031](https://github.com/appwrite/appwrite/pull/10031)
|
||||
* Add VCS repository authorized field in [#11421](https://github.com/appwrite/appwrite/pull/11421)
|
||||
* Add trusted console projects in [#11248](https://github.com/appwrite/appwrite/pull/11248)
|
||||
|
||||
### Refactoring
|
||||
|
||||
* Refactor to Utopia Platform modules architecture in [#11035](https://github.com/appwrite/appwrite/pull/11035), [#11049](https://github.com/appwrite/appwrite/pull/11049), [#11057](https://github.com/appwrite/appwrite/pull/11057), [#11103](https://github.com/appwrite/appwrite/pull/11103), [#11208](https://github.com/appwrite/appwrite/pull/11208), and [#11398](https://github.com/appwrite/appwrite/pull/11398)
|
||||
* Refactor auth to single instance in [#10872](https://github.com/appwrite/appwrite/pull/10872) and [#11130](https://github.com/appwrite/appwrite/pull/11130)
|
||||
* Refactor usage metrics to stateless publisher pattern in [#11449](https://github.com/appwrite/appwrite/pull/11449)
|
||||
* Refactor messaging and queue in [#10961](https://github.com/appwrite/appwrite/pull/10961)
|
||||
* Refactor functions schedule in [#10913](https://github.com/appwrite/appwrite/pull/10913)
|
||||
* Refactor make Bus dispatch synchronous in [#11449](https://github.com/appwrite/appwrite/pull/11449)
|
||||
* Remove proxy container in [#11039](https://github.com/appwrite/appwrite/pull/11039)
|
||||
|
||||
### Performance
|
||||
|
||||
* Optimize updateDocument() calls to use sparse documents in [#11465](https://github.com/appwrite/appwrite/pull/11465)
|
||||
* Optimize Dockerfile in [#10947](https://github.com/appwrite/appwrite/pull/10947)
|
||||
* Improve domain caching in [#11346](https://github.com/appwrite/appwrite/pull/11346)
|
||||
* Improve memory usage in [#11345](https://github.com/appwrite/appwrite/pull/11345)
|
||||
* Fix memory leak in [#11067](https://github.com/appwrite/appwrite/pull/11067) and [#11241](https://github.com/appwrite/appwrite/pull/11241)
|
||||
* Improve realtime performance in [#11251](https://github.com/appwrite/appwrite/pull/11251)
|
||||
* Enable SMTP keep-alive to reuse connections across mail jobs in [#11496](https://github.com/appwrite/appwrite/pull/11496)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix duplicate document error while creating file in [#10891](https://github.com/appwrite/appwrite/pull/10891)
|
||||
* Fix "Update external deployment (authorize)" throwing 500 error due to invalid query in [#10888](https://github.com/appwrite/appwrite/pull/10888)
|
||||
* Fix error setting user password in [#10889](https://github.com/appwrite/appwrite/pull/10889)
|
||||
* Fix error generating email MFA challenges in [#10884](https://github.com/appwrite/appwrite/pull/10884)
|
||||
* Fix file token expiry in [#10877](https://github.com/appwrite/appwrite/pull/10877)
|
||||
* Fix TanStack Nitro default in [#10860](https://github.com/appwrite/appwrite/pull/10860)
|
||||
* Fix TanStack builds in [#10767](https://github.com/appwrite/appwrite/pull/10767)
|
||||
* Fix nullable validation in [#10819](https://github.com/appwrite/appwrite/pull/10819) and [#10778](https://github.com/appwrite/appwrite/pull/10778)
|
||||
* Fix WebP library in [#10738](https://github.com/appwrite/appwrite/pull/10738)
|
||||
* Fix batch writes in [#10812](https://github.com/appwrite/appwrite/pull/10812)
|
||||
* Fix error handler error in [#10719](https://github.com/appwrite/appwrite/pull/10719)
|
||||
* Fix Next 16 compatibility in [#10713](https://github.com/appwrite/appwrite/pull/10713)
|
||||
* Fix stats usage memory leak in [#10683](https://github.com/appwrite/appwrite/pull/10683)
|
||||
* Fix author URL in template deployments in [#10535](https://github.com/appwrite/appwrite/pull/10535)
|
||||
* Fix VCS lock deletion in [#10691](https://github.com/appwrite/appwrite/pull/10691)
|
||||
* Fix blocked user/resource errors from 401 to 403 in [#11469](https://github.com/appwrite/appwrite/pull/11469)
|
||||
* Fix OAuth for custom domains in [#10967](https://github.com/appwrite/appwrite/pull/10967) and [#11269](https://github.com/appwrite/appwrite/pull/11269)
|
||||
* Fix OAuth redirect custom scheme in [#11292](https://github.com/appwrite/appwrite/pull/11292)
|
||||
* Fix OAuth verified emails in [#10986](https://github.com/appwrite/appwrite/pull/10986)
|
||||
* Fix MFA recovery code validation in [#10925](https://github.com/appwrite/appwrite/pull/10925)
|
||||
* Fix users allow updating phone number to empty in [#11521](https://github.com/appwrite/appwrite/pull/11521)
|
||||
* Fix users optional name error in [#11413](https://github.com/appwrite/appwrite/pull/11413)
|
||||
* Fix file permissions in [#11026](https://github.com/appwrite/appwrite/pull/11026)
|
||||
* Fix bulk insert webhook validation in [#11022](https://github.com/appwrite/appwrite/pull/11022)
|
||||
* Fix execution status update in [#11134](https://github.com/appwrite/appwrite/pull/11134)
|
||||
* Fix execution timeout status in [#11400](https://github.com/appwrite/appwrite/pull/11400)
|
||||
* Fix CORS wildcard in [#10956](https://github.com/appwrite/appwrite/pull/10956)
|
||||
* Fix preflight requests in [#10943](https://github.com/appwrite/appwrite/pull/10943)
|
||||
* Fix SMTP auth check in [#10939](https://github.com/appwrite/appwrite/pull/10939)
|
||||
* Fix scheduled executions trigger in [#10922](https://github.com/appwrite/appwrite/pull/10922)
|
||||
* Fix schedule executions bug in [#10916](https://github.com/appwrite/appwrite/pull/10916)
|
||||
* Fix deployment enum missing canceled value in [#11179](https://github.com/appwrite/appwrite/pull/11179)
|
||||
* Fix invalid chunk total in [#11270](https://github.com/appwrite/appwrite/pull/11270)
|
||||
* Fix sites domains in [#11240](https://github.com/appwrite/appwrite/pull/11240) and [#11355](https://github.com/appwrite/appwrite/pull/11355)
|
||||
* Fix rule domains in [#11355](https://github.com/appwrite/appwrite/pull/11355) and [#11276](https://github.com/appwrite/appwrite/pull/11276)
|
||||
* Fix rules deletion in [#11575](https://github.com/appwrite/appwrite/pull/11575)
|
||||
* Fix VCS template flow in [#11275](https://github.com/appwrite/appwrite/pull/11275)
|
||||
* Fix VCS comment empty in [#11490](https://github.com/appwrite/appwrite/pull/11490)
|
||||
* Fix DSN VCS error in [#11364](https://github.com/appwrite/appwrite/pull/11364)
|
||||
* Fix email URL params encoding in [#11369](https://github.com/appwrite/appwrite/pull/11369)
|
||||
* Fix missing email warning in [#11378](https://github.com/appwrite/appwrite/pull/11378)
|
||||
* Fix race condition in builds worker in [#11336](https://github.com/appwrite/appwrite/pull/11336)
|
||||
* Fix realtime regions in [#11414](https://github.com/appwrite/appwrite/pull/11414)
|
||||
* Fix realtime errors in [#11573](https://github.com/appwrite/appwrite/pull/11573)
|
||||
* Fix realtime TablesDB channels in [#11404](https://github.com/appwrite/appwrite/pull/11404) and [#11430](https://github.com/appwrite/appwrite/pull/11430)
|
||||
* Fix database shared table reconciliation in [#11578](https://github.com/appwrite/appwrite/pull/11578)
|
||||
* Fix PostgreSQL race condition in shared mode project creation in [#11536](https://github.com/appwrite/appwrite/pull/11536)
|
||||
* Fix compression enabled env in [#11171](https://github.com/appwrite/appwrite/pull/11171)
|
||||
* Fix deletes bug in [#10965](https://github.com/appwrite/appwrite/pull/10965)
|
||||
* Fix devkey scopes in [#10984](https://github.com/appwrite/appwrite/pull/10984)
|
||||
* Fix phone auth limit in [#11143](https://github.com/appwrite/appwrite/pull/11143)
|
||||
* Fix relationship document ID validation in [#11193](https://github.com/appwrite/appwrite/pull/11193)
|
||||
* Fix stale project overwrites OAuth in [#11461](https://github.com/appwrite/appwrite/pull/11461)
|
||||
* Fix storage health error swallowing in [#11492](https://github.com/appwrite/appwrite/pull/11492)
|
||||
* Fix Origin validator type error in [#11297](https://github.com/appwrite/appwrite/pull/11297)
|
||||
* Fix getScreenshot image format in [#11017](https://github.com/appwrite/appwrite/pull/11017)
|
||||
* Fix migration error handling in [#11457](https://github.com/appwrite/appwrite/pull/11457)
|
||||
* Fix deprecation warnings in [#11227](https://github.com/appwrite/appwrite/pull/11227)
|
||||
|
||||
### Installer
|
||||
|
||||
* New installer UI in [#11175](https://github.com/appwrite/appwrite/pull/11175) and [#11247](https://github.com/appwrite/appwrite/pull/11247)
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* Add CSV export functionality in [#10546](https://github.com/appwrite/appwrite/pull/10546), [#10750](https://github.com/appwrite/appwrite/pull/10750), [#10813](https://github.com/appwrite/appwrite/pull/10813), and [#10847](https://github.com/appwrite/appwrite/pull/10847)
|
||||
* Add JWT disposition in [#10867](https://github.com/appwrite/appwrite/pull/10867)
|
||||
* Add screenshots endpoint in [#10675](https://github.com/appwrite/appwrite/pull/10675)
|
||||
* Add screenshot endpoint stats in [#10706](https://github.com/appwrite/appwrite/pull/10706)
|
||||
* Add users attributes in [#10688](https://github.com/appwrite/appwrite/pull/10688)
|
||||
* Add max build duration environment variable in [#10674](https://github.com/appwrite/appwrite/pull/10674)
|
||||
* Add custom realtime logger in [#10871](https://github.com/appwrite/appwrite/pull/10871)
|
||||
* Add logs in [#10869](https://github.com/appwrite/appwrite/pull/10869)
|
||||
* Improve MFA docs endpoint order in [#10793](https://github.com/appwrite/appwrite/pull/10793)
|
||||
* Auth refactor in [#10758](https://github.com/appwrite/appwrite/pull/10758), [#10837](https://github.com/appwrite/appwrite/pull/10837), [#10682](https://github.com/appwrite/appwrite/pull/10682), and [#10667](https://github.com/appwrite/appwrite/pull/10667)
|
||||
* Bump assistant to 0.8.4 in [#10887](https://github.com/appwrite/appwrite/pull/10887)
|
||||
* Bump database to 3.1.5 in [#10766](https://github.com/appwrite/appwrite/pull/10766)
|
||||
* Bump Utopia DNS in [#10761](https://github.com/appwrite/appwrite/pull/10761)
|
||||
* Update domains to 0.8.3 in [#10658](https://github.com/appwrite/appwrite/pull/10658)
|
||||
* Update domains to 0.9.1 in [#10678](https://github.com/appwrite/appwrite/pull/10678)
|
||||
* Update Apple Swift to 13.3.0 in [#10679](https://github.com/appwrite/appwrite/pull/10679)
|
||||
* Update Apple Swift in [#10663](https://github.com/appwrite/appwrite/pull/10663)
|
||||
* Update CLI to 10.2.2 in [#10672](https://github.com/appwrite/appwrite/pull/10672)
|
||||
* Update to CLI 12.0.0 in [#10853](https://github.com/appwrite/appwrite/pull/10853)
|
||||
* Update docs examples to use Permission class in [#10707](https://github.com/appwrite/appwrite/pull/10707)
|
||||
* Update SDK examples docs in [#10855](https://github.com/appwrite/appwrite/pull/10855)
|
||||
* Release Python SDK in [#10762](https://github.com/appwrite/appwrite/pull/10762)
|
||||
* Release Flutter 20.3.2 in [#10838](https://github.com/appwrite/appwrite/pull/10838)
|
||||
* Release Flutter/Dart add screenshot examples in [#10811](https://github.com/appwrite/appwrite/pull/10811)
|
||||
* Release PHP CLI in [#10791](https://github.com/appwrite/appwrite/pull/10791)
|
||||
* Release SDKs in [#10817](https://github.com/appwrite/appwrite/pull/10817)
|
||||
* Update SDKs in [#10694](https://github.com/appwrite/appwrite/pull/10694), [#10729](https://github.com/appwrite/appwrite/pull/10729), and [#10744](https://github.com/appwrite/appwrite/pull/10744)
|
||||
* Update SDK generator in [#10743](https://github.com/appwrite/appwrite/pull/10743)
|
||||
* Update database in [#10664](https://github.com/appwrite/appwrite/pull/10664)
|
||||
* Update README file in [#10763](https://github.com/appwrite/appwrite/pull/10763)
|
||||
* SDK release documentation in [#10745](https://github.com/appwrite/appwrite/pull/10745)
|
||||
* SDK release runtime config in [#10765](https://github.com/appwrite/appwrite/pull/10765)
|
||||
* Sync specs in [#10789](https://github.com/appwrite/appwrite/pull/10789)
|
||||
* Sync 1.8.0 in [#10677](https://github.com/appwrite/appwrite/pull/10677)
|
||||
* Add workflow for issue triage in [#10718](https://github.com/appwrite/appwrite/pull/10718)
|
||||
* Add issue auto-labeler in [#10700](https://github.com/appwrite/appwrite/pull/10700)
|
||||
* Add AI moderator repo in [#10717](https://github.com/appwrite/appwrite/pull/10717)
|
||||
* Browser bump in [#10850](https://github.com/appwrite/appwrite/pull/10850)
|
||||
* Template type enum override in [#10848](https://github.com/appwrite/appwrite/pull/10848)
|
||||
* VCS reference type in [#10852](https://github.com/appwrite/appwrite/pull/10852)
|
||||
* Index scope description in [#10851](https://github.com/appwrite/appwrite/pull/10851)
|
||||
* Config for environment in [#10833](https://github.com/appwrite/appwrite/pull/10833)
|
||||
* Format instance in [#10830](https://github.com/appwrite/appwrite/pull/10830)
|
||||
* Replace sleep in webhooks service in [#10656](https://github.com/appwrite/appwrite/pull/10656)
|
||||
* Update email composer in [#10720](https://github.com/appwrite/appwrite/pull/10720)
|
||||
* Update facts on GitHub sites and functions in [#10593](https://github.com/appwrite/appwrite/pull/10593) and [#10771](https://github.com/appwrite/appwrite/pull/10771)
|
||||
* Fix wrong user type in [#10875](https://github.com/appwrite/appwrite/pull/10875)
|
||||
* Fix limit and offset computation in [#10880](https://github.com/appwrite/appwrite/pull/10880)
|
||||
* Fix enum examples in [#10828](https://github.com/appwrite/appwrite/pull/10828)
|
||||
* Fix response models multi-methods in [#10815](https://github.com/appwrite/appwrite/pull/10815)
|
||||
* Fix undefined variable in [#10654](https://github.com/appwrite/appwrite/pull/10654)
|
||||
* Fix undefined sequence in [#10652](https://github.com/appwrite/appwrite/pull/10652)
|
||||
* Fix description in [#10702](https://github.com/appwrite/appwrite/pull/10702)
|
||||
* Fix warning in builds worker in [#10705](https://github.com/appwrite/appwrite/pull/10705)
|
||||
* Fix sites create deployment docs in [#10566](https://github.com/appwrite/appwrite/pull/10566)
|
||||
* Fix test dependencies projects in [#10655](https://github.com/appwrite/appwrite/pull/10655)
|
||||
* Fix list sites test in [#10726](https://github.com/appwrite/appwrite/pull/10726)
|
||||
* Add audits upgrade in [#10953](https://github.com/appwrite/appwrite/pull/10953)
|
||||
* Add graceful workers shutdown in [#11104](https://github.com/appwrite/appwrite/pull/11104)
|
||||
* Add pool resilience in [#11139](https://github.com/appwrite/appwrite/pull/11139)
|
||||
* Add function queue job TTL in [#11226](https://github.com/appwrite/appwrite/pull/11226)
|
||||
* Add cleanup stale executions in [#11146](https://github.com/appwrite/appwrite/pull/11146)
|
||||
* Add success abuse reset in [#11085](https://github.com/appwrite/appwrite/pull/11085)
|
||||
* Add SMTP connection validation in [#11079](https://github.com/appwrite/appwrite/pull/11079)
|
||||
* Add allow custom email sender in [#10945](https://github.com/appwrite/appwrite/pull/10945)
|
||||
* Add array domains env support in [#11213](https://github.com/appwrite/appwrite/pull/11213)
|
||||
* Add file create after success hook in [#11054](https://github.com/appwrite/appwrite/pull/11054)
|
||||
* Add delete subscribers in [#11115](https://github.com/appwrite/appwrite/pull/11115)
|
||||
* Add cursor plugin in [#11371](https://github.com/appwrite/appwrite/pull/11371)
|
||||
* Add observability spans in [#11320](https://github.com/appwrite/appwrite/pull/11320), [#11306](https://github.com/appwrite/appwrite/pull/11306), and [#11228](https://github.com/appwrite/appwrite/pull/11228)
|
||||
* Upgrade PHPStan to v2 with full codebase coverage in [#11550](https://github.com/appwrite/appwrite/pull/11550)
|
||||
* Upgrade Traefik in [#11265](https://github.com/appwrite/appwrite/pull/11265)
|
||||
* Upgrade utopia-php/queue in [#11239](https://github.com/appwrite/appwrite/pull/11239)
|
||||
* Upgrade spomky-labs/otphp in [#11263](https://github.com/appwrite/appwrite/pull/11263)
|
||||
* Bump utopia-php/database to stable 5.3.15 in [#11573](https://github.com/appwrite/appwrite/pull/11573)
|
||||
* Bump utopia-php/migration to 1.6.3 in [#11443](https://github.com/appwrite/appwrite/pull/11443)
|
||||
* Consolidate CI workflows in [#11531](https://github.com/appwrite/appwrite/pull/11531) and [#11551](https://github.com/appwrite/appwrite/pull/11551)
|
||||
* Hide deprecated methods from docs in [#10933](https://github.com/appwrite/appwrite/pull/10933)
|
||||
* Deprecate project-level attributes in [#11203](https://github.com/appwrite/appwrite/pull/11203)
|
||||
|
||||
# Version 1.8.0
|
||||
|
||||
|
||||
+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.8.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.8.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.8.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.8.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
### Windows
|
||||
@@ -87,7 +87,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.8.1
|
||||
appwrite/appwrite:1.9.0
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
@@ -97,7 +97,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.8.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.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
$common = include __DIR__ . '/collections/common.php';
|
||||
$projects = include __DIR__ . '/collections/projects.php';
|
||||
$databases = include __DIR__ . '/collections/databases.php';
|
||||
$vectorsdb = include __DIR__ . '/collections/vectorsdb.php';
|
||||
$platform = include __DIR__ . '/collections/platform.php';
|
||||
$logs = include __DIR__ . '/collections/logs.php';
|
||||
|
||||
@@ -26,6 +27,7 @@ unset($common['files']);
|
||||
$collections = [
|
||||
'buckets' => $buckets,
|
||||
'databases' => $databases,
|
||||
'vectorsdb' => $vectorsdb,
|
||||
'projects' => array_merge_recursive($projects, $common),
|
||||
'console' => array_merge_recursive($platform, $common),
|
||||
'logs' => $logs,
|
||||
|
||||
@@ -419,6 +419,17 @@ return [
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('impersonator'),
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'signed' => true,
|
||||
'size' => 0,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => false,
|
||||
'default' => false,
|
||||
'array' => false,
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
@@ -491,6 +502,13 @@ return [
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('impersonator'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => [ID::custom('impersonator')],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -61,6 +61,15 @@ return [
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('database'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'size' => 128,
|
||||
'required' => false,
|
||||
'signed' => true,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
]
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
|
||||
return [
|
||||
'collections' => [
|
||||
'$collection' => ID::custom('databases'),
|
||||
'$id' => ID::custom('collections'),
|
||||
'name' => 'Collections',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => ID::custom('databaseInternalId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('databaseId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'signed' => true,
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('name'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'size' => 256,
|
||||
'required' => true,
|
||||
'signed' => true,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('dimension'),
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'size' => 0,
|
||||
'required' => true,
|
||||
'signed' => false,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('enabled'),
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'signed' => true,
|
||||
'size' => 0,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('documentSecurity'),
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'signed' => true,
|
||||
'size' => 0,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('attributes'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'size' => 1000000,
|
||||
'required' => false,
|
||||
'signed' => true,
|
||||
'array' => false,
|
||||
'filters' => ['subQueryAttributes'],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('indexes'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'size' => 1000000,
|
||||
'required' => false,
|
||||
'signed' => true,
|
||||
'array' => false,
|
||||
'filters' => ['subQueryIndexes'],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('search'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'defaultAttributes' => [
|
||||
[
|
||||
'$id' => ID::custom('embeddings'),
|
||||
'type' => Database::VAR_VECTOR,
|
||||
'required' => true,
|
||||
'signed' => false,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('metadata'),
|
||||
'type' => Database::VAR_OBJECT,
|
||||
'default' => [],
|
||||
'required' => false,
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => ID::custom('_fulltext_search'),
|
||||
'type' => Database::INDEX_FULLTEXT,
|
||||
'attributes' => ['search'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_name'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['name'],
|
||||
'lengths' => [256],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_enabled'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['enabled'],
|
||||
'lengths' => [],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_documentSecurity'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['documentSecurity'],
|
||||
'lengths' => [],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
],
|
||||
'defaultIndexes' => [
|
||||
// not creating default indexes on the embeddings as it depends on the type of query users using the most
|
||||
[
|
||||
'$id' => ID::custom('_key_metadata'),
|
||||
'type' => Database::INDEX_OBJECT,
|
||||
'attributes' => ['metadata'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
]
|
||||
]
|
||||
];
|
||||
@@ -28,6 +28,9 @@ return [
|
||||
'X-Appwrite-Timestamp',
|
||||
'X-Appwrite-Session',
|
||||
'X-Appwrite-Platform',
|
||||
'X-Appwrite-Impersonate-User-Id',
|
||||
'X-Appwrite-Impersonate-User-Email',
|
||||
'X-Appwrite-Impersonate-User-Phone',
|
||||
// SDK generator
|
||||
'X-SDK-Version',
|
||||
'X-SDK-Name',
|
||||
|
||||
@@ -1206,6 +1206,11 @@ return [
|
||||
'description' => 'Migration is already in progress. You can check the status of the migration in your Appwrite Console\'s "Settings" > "Migrations".',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED => [
|
||||
'name' => Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED,
|
||||
'description' => 'The specified database type is not supported for CSV import or export operations.',
|
||||
'code' => 400,
|
||||
],
|
||||
|
||||
/** Realtime */
|
||||
Exception::REALTIME_MESSAGE_FORMAT_INVALID => [
|
||||
|
||||
@@ -62,6 +62,8 @@ $admins = [
|
||||
'devKeys.write',
|
||||
'webhooks.read',
|
||||
'webhooks.write',
|
||||
'project.read',
|
||||
'project.write',
|
||||
'locale.read',
|
||||
'avatars.read',
|
||||
'health.read',
|
||||
|
||||
@@ -31,12 +31,4 @@ return [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s development keys",
|
||||
],
|
||||
"webhooks.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s webhooks",
|
||||
],
|
||||
"webhooks.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s webhooks",
|
||||
],
|
||||
];
|
||||
|
||||
@@ -180,4 +180,12 @@ return [ // List of publicly visible scopes
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s webhooks",
|
||||
],
|
||||
"project.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s information",
|
||||
],
|
||||
"project.write" => [
|
||||
"description" =>
|
||||
"Access to update project\'s information",
|
||||
],
|
||||
];
|
||||
|
||||
@@ -16,7 +16,6 @@ use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Hooks\Hooks;
|
||||
use Appwrite\Network\Validator\Email as EmailValidator;
|
||||
use Appwrite\Network\Validator\Redirect;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -60,6 +59,7 @@ use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Emails\Email;
|
||||
use Utopia\Emails\Validator\Email as EmailValidator;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\Storage\Validator\FileName;
|
||||
@@ -209,6 +209,22 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
|
||||
|
||||
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
|
||||
// Attempt to decode secret as a JWT (used by OAuth2 token flow to carry provider info)
|
||||
$oauthProvider = null;
|
||||
try {
|
||||
$jwtDecoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 60, 0);
|
||||
$payload = $jwtDecoder->decode($secret);
|
||||
|
||||
if (empty($payload['provider'])) {
|
||||
throw new Exception(Exception::USER_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
$oauthProvider = $payload['provider'];
|
||||
$secret = $payload['secret'];
|
||||
} catch (\Ahc\Jwt\JWTException) {
|
||||
// Not a JWT — use secret as-is (non-OAuth flows)
|
||||
}
|
||||
|
||||
/** @var Appwrite\Utopia\Database\Documents\User $userFromRequest */
|
||||
$userFromRequest = $authorization->skip(fn () => $dbForProject->getDocument('users', $userId));
|
||||
|
||||
@@ -220,6 +236,12 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
?: $userFromRequest->tokenVerify(null, $secret, $proofForCode);
|
||||
|
||||
if (!$verifiedToken) {
|
||||
// Could mean invalid/expired JWT, or expired secret
|
||||
throw new Exception(Exception::USER_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
// OAuth2 tokens must have a provider from the JWT
|
||||
if ($verifiedToken->getAttribute('type') === TOKEN_TYPE_OAUTH2 && $oauthProvider === null) {
|
||||
throw new Exception(Exception::USER_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
@@ -245,7 +267,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
TOKEN_TYPE_INVITE => SESSION_PROVIDER_EMAIL,
|
||||
TOKEN_TYPE_MAGIC_URL => SESSION_PROVIDER_MAGIC_URL,
|
||||
TOKEN_TYPE_PHONE => SESSION_PROVIDER_PHONE,
|
||||
TOKEN_TYPE_OAUTH2 => SESSION_PROVIDER_OAUTH2,
|
||||
TOKEN_TYPE_OAUTH2 => $oauthProvider,
|
||||
default => SESSION_PROVIDER_TOKEN,
|
||||
};
|
||||
$session = new Document(array_merge(
|
||||
@@ -1899,7 +1921,12 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->setParam('tokenId', $token->getId())
|
||||
;
|
||||
|
||||
$query['secret'] = $secret;
|
||||
// Wrap secret in a JWT that also carries the provider name
|
||||
$jwtEncoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 60, 0);
|
||||
$query['secret'] = $jwtEncoder->encode([
|
||||
'secret' => $secret,
|
||||
'provider' => $provider,
|
||||
]);
|
||||
$query['userId'] = $user->getId();
|
||||
|
||||
// If the `token` param is not set, we persist the session in a cookie
|
||||
|
||||
@@ -8,7 +8,6 @@ use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Messaging\Status as MessageStatus;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Appwrite\Permission;
|
||||
use Appwrite\Role;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -43,6 +42,7 @@ use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\System\System;
|
||||
|
||||
@@ -43,6 +43,16 @@ use Utopia\Validator\WhiteList;
|
||||
|
||||
include_once __DIR__ . '/../shared/api.php';
|
||||
|
||||
function getDatabaseTransferResourceServices(string $databaseType)
|
||||
{
|
||||
return match($databaseType) {
|
||||
DATABASE_TYPE_LEGACY,
|
||||
DATABASE_TYPE_TABLESDB => Transfer::GROUP_DATABASES_TABLES_DB,
|
||||
DATABASE_TYPE_VECTORSDB => Transfer::GROUP_DATABASES_VECTOR_DB,
|
||||
DATABASE_TYPE_DOCUMENTSDB => Transfer::GROUP_DATABASES_DOCUMENTS_DB
|
||||
};
|
||||
}
|
||||
|
||||
Http::post('/v1/migrations/appwrite')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Create Appwrite migration')
|
||||
@@ -427,8 +437,16 @@ Http::post('/v1/migrations/csv/imports')
|
||||
throw new \Exception('Unable to copy file');
|
||||
}
|
||||
|
||||
// getting databasetype
|
||||
$resources = explode(':', $resourceId);
|
||||
$databaseId = $resources[0];
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
$databaseType = $database->getAttribute('type');
|
||||
if (!in_array($databaseType, CSV_ALLOWED_DATABASE_TYPES)) {
|
||||
throw new Exception(Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED, 'Database type not supported for csv');
|
||||
}
|
||||
$fileSize = $deviceForMigrations->getFileSize($newPath);
|
||||
$resources = Transfer::extractServices([Transfer::GROUP_DATABASES]);
|
||||
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
|
||||
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => $migrationId,
|
||||
@@ -557,13 +575,23 @@ Http::post('/v1/migrations/csv/exports')
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
// getting databasetype
|
||||
$resources = explode(':', $resourceId);
|
||||
$databaseId = $resources[0];
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
$databaseType = $database->getAttribute('type');
|
||||
if (!in_array($databaseType, CSV_ALLOWED_DATABASE_TYPES)) {
|
||||
throw new Exception(Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED, 'Database type not supported for csv');
|
||||
}
|
||||
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
|
||||
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => Appwrite::getName(),
|
||||
'destination' => CSV::getName(),
|
||||
'resources' => Transfer::extractServices([Transfer::GROUP_DATABASES]),
|
||||
'resources' => $resources,
|
||||
'resourceId' => $resourceId,
|
||||
'resourceType' => Resource::TYPE_DATABASE,
|
||||
'statusCounters' => '{}',
|
||||
@@ -713,12 +741,11 @@ Http::get('/v1/migrations/appwrite/report')
|
||||
->param('projectID', '', new Text(512), "Source's Project ID")
|
||||
->param('key', '', new Text(512), "Source's API Key")
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response) {
|
||||
->inject('getDatabasesDB')
|
||||
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response, callable $getDatabasesDB) {
|
||||
|
||||
try {
|
||||
$appwrite = new Appwrite($projectID, $endpoint, $key);
|
||||
$appwrite = new Appwrite($projectID, $endpoint, $key, $getDatabasesDB);
|
||||
$report = $appwrite->report($resources);
|
||||
} catch (\Throwable $e) {
|
||||
throw new Exception(
|
||||
|
||||
+60
-235
@@ -1,25 +1,15 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Datetime as DateTimeValidator;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
Http::get('/v1/project/usage')
|
||||
@@ -62,16 +52,33 @@ Http::get('/v1/project/usage')
|
||||
METRIC_EXECUTIONS_MB_SECONDS,
|
||||
METRIC_BUILDS_MB_SECONDS,
|
||||
METRIC_DOCUMENTS,
|
||||
METRIC_DOCUMENTS_DOCUMENTSDB,
|
||||
METRIC_DATABASES,
|
||||
METRIC_DATABASES_DOCUMENTSDB,
|
||||
METRIC_USERS,
|
||||
METRIC_BUCKETS,
|
||||
METRIC_FILES_STORAGE,
|
||||
METRIC_DATABASES_STORAGE,
|
||||
METRIC_DATABASES_STORAGE_DOCUMENTSDB,
|
||||
METRIC_DEPLOYMENTS_STORAGE,
|
||||
METRIC_BUILDS_STORAGE,
|
||||
METRIC_DATABASES_OPERATIONS_READS,
|
||||
METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB,
|
||||
METRIC_DATABASES_OPERATIONS_WRITES,
|
||||
METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB,
|
||||
METRIC_FILES_IMAGES_TRANSFORMED,
|
||||
// VectorsDB totals
|
||||
METRIC_DATABASES_VECTORSDB,
|
||||
METRIC_COLLECTIONS_VECTORSDB,
|
||||
METRIC_DOCUMENTS_VECTORSDB,
|
||||
METRIC_DATABASES_STORAGE_VECTORSDB,
|
||||
METRIC_DATABASES_OPERATIONS_READS_VECTORSDB,
|
||||
METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB,
|
||||
// Embeddings totals
|
||||
METRIC_EMBEDDINGS_TEXT,
|
||||
METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS,
|
||||
METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION,
|
||||
METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR
|
||||
],
|
||||
'period' => [
|
||||
METRIC_NETWORK_REQUESTS,
|
||||
@@ -80,11 +87,26 @@ Http::get('/v1/project/usage')
|
||||
METRIC_USERS,
|
||||
METRIC_EXECUTIONS,
|
||||
METRIC_DATABASES_STORAGE,
|
||||
METRIC_DATABASES_STORAGE_DOCUMENTSDB,
|
||||
METRIC_EXECUTIONS_MB_SECONDS,
|
||||
METRIC_BUILDS_MB_SECONDS,
|
||||
METRIC_DATABASES_OPERATIONS_READS,
|
||||
METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB,
|
||||
METRIC_DATABASES_OPERATIONS_WRITES,
|
||||
METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB,
|
||||
METRIC_FILES_IMAGES_TRANSFORMED,
|
||||
// VectorsDB time series
|
||||
METRIC_DATABASES_VECTORSDB,
|
||||
METRIC_COLLECTIONS_VECTORSDB,
|
||||
METRIC_DOCUMENTS_VECTORSDB,
|
||||
METRIC_DATABASES_STORAGE_VECTORSDB,
|
||||
METRIC_DATABASES_OPERATIONS_READS_VECTORSDB,
|
||||
METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB,
|
||||
// Embeddings time series
|
||||
METRIC_EMBEDDINGS_TEXT,
|
||||
METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS,
|
||||
METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION,
|
||||
METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR
|
||||
]
|
||||
];
|
||||
|
||||
@@ -357,8 +379,11 @@ Http::get('/v1/project/usage')
|
||||
'buildsMbSecondsTotal' => $total[METRIC_BUILDS_MB_SECONDS],
|
||||
'documentsTotal' => $total[METRIC_DOCUMENTS],
|
||||
'rowsTotal' => $total[METRIC_DOCUMENTS],
|
||||
'documentsdbDocumentsTotal' => $total[METRIC_DOCUMENTS_DOCUMENTSDB],
|
||||
'databasesTotal' => $total[METRIC_DATABASES],
|
||||
'documentsdbTotal' => $total[METRIC_DATABASES_DOCUMENTSDB],
|
||||
'databasesStorageTotal' => $total[METRIC_DATABASES_STORAGE],
|
||||
'documentsdbDatabasesStorageTotal' => $total[METRIC_DATABASES_STORAGE_DOCUMENTSDB],
|
||||
'usersTotal' => $total[METRIC_USERS],
|
||||
'bucketsTotal' => $total[METRIC_BUCKETS],
|
||||
'filesStorageTotal' => $total[METRIC_FILES_STORAGE],
|
||||
@@ -367,10 +392,27 @@ Http::get('/v1/project/usage')
|
||||
'deploymentsStorageTotal' => $total[METRIC_DEPLOYMENTS_STORAGE],
|
||||
'databasesReadsTotal' => $total[METRIC_DATABASES_OPERATIONS_READS],
|
||||
'databasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES],
|
||||
'documentsdbDatabasesReadsTotal' => $total[METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB],
|
||||
'documentsdbDatabasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB],
|
||||
'vectorsdbDatabasesTotal' => $total[METRIC_DATABASES_VECTORSDB] ?? 0,
|
||||
'vectorsdbCollectionsTotal' => $total[METRIC_COLLECTIONS_VECTORSDB] ?? 0,
|
||||
'vectorsdbDocumentsTotal' => $total[METRIC_DOCUMENTS_VECTORSDB] ?? 0,
|
||||
'vectorsdbDatabasesStorageTotal' => $total[METRIC_DATABASES_STORAGE_VECTORSDB] ?? 0,
|
||||
'vectorsdbDatabasesReadsTotal' => $total[METRIC_DATABASES_OPERATIONS_READS_VECTORSDB] ?? 0,
|
||||
'vectorsdbDatabasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB] ?? 0,
|
||||
'executionsBreakdown' => $executionsBreakdown,
|
||||
'bucketsBreakdown' => $bucketsBreakdown,
|
||||
'databasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS],
|
||||
'databasesWrites' => $usage[METRIC_DATABASES_OPERATIONS_WRITES],
|
||||
'documentsdbDatabasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB],
|
||||
'documentsdbDatabasesWrites' => $usage[METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB],
|
||||
'documentsdbDatabasesStorage' => $usage[METRIC_DATABASES_STORAGE_DOCUMENTSDB],
|
||||
'vectorsdbDatabases' => $usage[METRIC_DATABASES_VECTORSDB] ?? [],
|
||||
'vectorsdbCollections' => $usage[METRIC_COLLECTIONS_VECTORSDB] ?? [],
|
||||
'vectorsdbDocuments' => $usage[METRIC_DOCUMENTS_VECTORSDB] ?? [],
|
||||
'vectorsdbDatabasesStorage' => $usage[METRIC_DATABASES_STORAGE_VECTORSDB] ?? [],
|
||||
'vectorsdbDatabasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS_VECTORSDB] ?? [],
|
||||
'vectorsdbDatabasesWrites' => $usage[METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB] ?? [],
|
||||
'databasesStorageBreakdown' => $databasesStorageBreakdown,
|
||||
'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown,
|
||||
'buildsMbSecondsBreakdown' => $buildsMbSecondsBreakdown,
|
||||
@@ -380,230 +422,13 @@ Http::get('/v1/project/usage')
|
||||
'authPhoneCountryBreakdown' => $authPhoneCountryBreakdown,
|
||||
'imageTransformations' => $usage[METRIC_FILES_IMAGES_TRANSFORMED],
|
||||
'imageTransformationsTotal' => $total[METRIC_FILES_IMAGES_TRANSFORMED],
|
||||
'embeddingsText' => $usage[METRIC_EMBEDDINGS_TEXT] ?? [],
|
||||
'embeddingsTextTokens' => $usage[METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS] ?? [],
|
||||
'embeddingsTextDuration' => $usage[METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION] ?? [],
|
||||
'embeddingsTextErrors' => $usage[METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR] ?? [],
|
||||
'embeddingsTextTotal' => $total[METRIC_EMBEDDINGS_TEXT] ?? 0,
|
||||
'embeddingsTextTokensTotal' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS] ?? 0,
|
||||
'embeddingsTextDurationTotal' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION] ?? 0,
|
||||
'embeddingsTextErrorsTotal' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR] ?? 0,
|
||||
]), Response::MODEL_USAGE_PROJECT);
|
||||
});
|
||||
|
||||
|
||||
// Variables
|
||||
Http::post('/v1/project/variables')
|
||||
->desc('Create variable')
|
||||
->groups(['api'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('audits.event', 'variable.create')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: null,
|
||||
name: 'createVariable',
|
||||
description: '/docs/references/project/create-variable.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_VARIABLE,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
|
||||
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
|
||||
->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $key, string $value, bool $secret, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) {
|
||||
$variableId = ID::unique();
|
||||
|
||||
$variable = new Document([
|
||||
'$id' => $variableId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceInternalId' => '',
|
||||
'resourceId' => '',
|
||||
'resourceType' => 'project',
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'secret' => $secret,
|
||||
'search' => implode(' ', [$variableId, $key, 'project']),
|
||||
]);
|
||||
|
||||
try {
|
||||
$variable = $dbForProject->createDocument('variables', $variable);
|
||||
} catch (DuplicateException $th) {
|
||||
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$functions = $dbForProject->find('functions', [
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
|
||||
foreach ($functions as $function) {
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
});
|
||||
|
||||
Http::get('/v1/project/variables')
|
||||
->desc('List variables')
|
||||
->groups(['api'])
|
||||
->label('scope', 'projects.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: null,
|
||||
name: 'listVariables',
|
||||
description: '/docs/references/project/list-variables.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_VARIABLE_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (Response $response, Database $dbForProject) {
|
||||
$variables = $dbForProject->find('variables', [
|
||||
Query::equal('resourceType', ['project']),
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'variables' => $variables,
|
||||
'total' => \count($variables),
|
||||
]), Response::MODEL_VARIABLE_LIST);
|
||||
});
|
||||
|
||||
Http::get('/v1/project/variables/:variableId')
|
||||
->desc('Get variable')
|
||||
->groups(['api'])
|
||||
->label('scope', 'projects.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: null,
|
||||
name: 'getVariable',
|
||||
description: '/docs/references/project/get-variable.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_VARIABLE,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $variableId, Response $response, Document $project, Database $dbForProject) {
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
});
|
||||
|
||||
Http::put('/v1/project/variables/:variableId')
|
||||
->desc('Update variable')
|
||||
->groups(['api'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: null,
|
||||
name: 'updateVariable',
|
||||
description: '/docs/references/project/update-variable.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_VARIABLE,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
|
||||
->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
|
||||
->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true)
|
||||
->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $variableId, string $key, ?string $value, ?bool $secret, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) {
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($variable->getAttribute('secret') === true && $secret === false) {
|
||||
throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET);
|
||||
}
|
||||
|
||||
$variable
|
||||
->setAttribute('key', $key)
|
||||
->setAttribute('value', $value ?? $variable->getAttribute('value'))
|
||||
->setAttribute('secret', $secret ?? $variable->getAttribute('secret'))
|
||||
->setAttribute('search', implode(' ', [$variableId, $key, 'project']));
|
||||
|
||||
try {
|
||||
$dbForProject->updateDocument('variables', $variable->getId(), $variable);
|
||||
} catch (DuplicateException $th) {
|
||||
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$functions = $dbForProject->find('functions', [
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
|
||||
foreach ($functions as $function) {
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
|
||||
}
|
||||
|
||||
$response->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
});
|
||||
|
||||
Http::delete('/v1/project/variables/:variableId')
|
||||
->desc('Delete variable')
|
||||
->groups(['api'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: null,
|
||||
name: 'deleteVariable',
|
||||
description: '/docs/references/project/delete-variable.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (string $variableId, Document $project, Response $response, Database $dbForProject) {
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dbForProject->deleteDocument('variables', $variable->getId());
|
||||
|
||||
$functions = $dbForProject->find('functions', [
|
||||
Query::limit(APP_LIMIT_SUBQUERY)
|
||||
]);
|
||||
|
||||
foreach ($functions as $function) {
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
|
||||
}
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
@@ -4,10 +4,8 @@ use Ahc\Jwt\JWT;
|
||||
use Appwrite\Auth\Validator\MockNumber;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Validator\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
@@ -30,7 +28,7 @@ use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Domains\Validator\PublicDomain;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\System\System;
|
||||
@@ -38,11 +36,9 @@ use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Hostname;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Multiple;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\URL;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
Http::init()
|
||||
@@ -773,312 +769,6 @@ Http::delete('/v1/projects/:projectId')
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
// Webhooks
|
||||
|
||||
Http::post('/v1/projects/:projectId/webhooks')
|
||||
->desc('Create webhook')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'webhooks',
|
||||
name: 'createWebhook',
|
||||
description: '/docs/references/projects/create-webhook.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_WEBHOOK,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
|
||||
->param('enabled', true, new Boolean(true), 'Enable or disable a webhook.', true)
|
||||
->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
|
||||
->param('url', '', fn ($request) => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.', false, ['request'])
|
||||
->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.')
|
||||
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
|
||||
->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$security = (bool) filter_var($security, FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
$webhook = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'projectId' => $project->getId(),
|
||||
'name' => $name,
|
||||
'events' => $events,
|
||||
'url' => $url,
|
||||
'security' => $security,
|
||||
'httpUser' => $httpUser,
|
||||
'httpPass' => $httpPass,
|
||||
'signatureKey' => \bin2hex(\random_bytes(64)),
|
||||
'enabled' => $enabled,
|
||||
]);
|
||||
|
||||
$webhook = $dbForPlatform->createDocument('webhooks', $webhook);
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($webhook, Response::MODEL_WEBHOOK);
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/webhooks')
|
||||
->desc('List webhooks')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'webhooks',
|
||||
name: 'listWebhooks',
|
||||
description: '/docs/references/projects/list-webhooks.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_WEBHOOK_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$webhooks = $dbForPlatform->find('webhooks', [
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
Query::limit(5000),
|
||||
]);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'webhooks' => $webhooks,
|
||||
'total' => $includeTotal ? count($webhooks) : 0,
|
||||
]), Response::MODEL_WEBHOOK_LIST);
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/webhooks/:webhookId')
|
||||
->desc('Get webhook')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'webhooks',
|
||||
name: 'getWebhook',
|
||||
description: '/docs/references/projects/get-webhook.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_WEBHOOK,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$webhook = $dbForPlatform->findOne('webhooks', [
|
||||
Query::equal('$id', [$webhookId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($webhook->isEmpty()) {
|
||||
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
|
||||
});
|
||||
|
||||
Http::put('/v1/projects/:projectId/webhooks/:webhookId')
|
||||
->desc('Update webhook')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'webhooks',
|
||||
name: 'updateWebhook',
|
||||
description: '/docs/references/projects/update-webhook.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_WEBHOOK,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform'])
|
||||
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
|
||||
->param('enabled', true, new Boolean(true), 'Enable or disable a webhook.', true)
|
||||
->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
|
||||
->param('url', '', fn ($request) => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.', false, ['request'])
|
||||
->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.')
|
||||
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
|
||||
->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $webhookId, string $name, bool $enabled, array $events, string $url, bool $security, string $httpUser, string $httpPass, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$security = ($security === '1' || $security === 'true' || $security === 1 || $security === true);
|
||||
|
||||
$webhook = $dbForPlatform->findOne('webhooks', [
|
||||
Query::equal('$id', [$webhookId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($webhook->isEmpty()) {
|
||||
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
|
||||
}
|
||||
|
||||
$webhook
|
||||
->setAttribute('name', $name)
|
||||
->setAttribute('events', $events)
|
||||
->setAttribute('url', $url)
|
||||
->setAttribute('security', $security)
|
||||
->setAttribute('httpUser', $httpUser)
|
||||
->setAttribute('httpPass', $httpPass)
|
||||
->setAttribute('enabled', $enabled);
|
||||
|
||||
if ($enabled) {
|
||||
$webhook->setAttribute('attempts', 0);
|
||||
}
|
||||
|
||||
$dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook);
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/webhooks/:webhookId/signature')
|
||||
->desc('Update webhook signature key')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'webhooks',
|
||||
name: 'updateWebhookSignature',
|
||||
description: '/docs/references/projects/update-webhook-signature.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_WEBHOOK,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$webhook = $dbForPlatform->findOne('webhooks', [
|
||||
Query::equal('$id', [$webhookId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($webhook->isEmpty()) {
|
||||
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
|
||||
}
|
||||
|
||||
$webhook->setAttribute('signatureKey', \bin2hex(\random_bytes(64)));
|
||||
|
||||
$dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook);
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
|
||||
});
|
||||
|
||||
Http::delete('/v1/projects/:projectId/webhooks/:webhookId')
|
||||
->desc('Delete webhook')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'webhooks',
|
||||
name: 'deleteWebhook',
|
||||
description: '/docs/references/projects/delete-webhook.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook unique ID.', false, ['dbForPlatform'])
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $webhookId, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$webhook = $dbForPlatform->findOne('webhooks', [
|
||||
Query::equal('$id', [$webhookId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
|
||||
if ($webhook->isEmpty()) {
|
||||
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dbForPlatform->deleteDocument('webhooks', $webhook->getId());
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
// Keys
|
||||
|
||||
Http::post('/v1/projects/:projectId/keys')
|
||||
|
||||
@@ -15,7 +15,6 @@ use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Hooks\Hooks;
|
||||
use Appwrite\Network\Validator\Email as EmailValidator;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
@@ -60,6 +59,7 @@ use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Emails\Email;
|
||||
use Utopia\Emails\Validator\Email as EmailValidator;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\System\System;
|
||||
@@ -1212,6 +1212,47 @@ Http::put('/v1/users/:userId/labels')
|
||||
$response->dynamic($user, Response::MODEL_USER);
|
||||
});
|
||||
|
||||
Http::patch('/v1/users/:userId/impersonator')
|
||||
->desc('Update user impersonator capability')
|
||||
->groups(['api', 'users'])
|
||||
->label('event', 'users.[userId].update.impersonator')
|
||||
->label('scope', 'users.write')
|
||||
->label('audits.event', 'user.update')
|
||||
->label('audits.resource', 'user/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'users',
|
||||
group: 'users',
|
||||
name: 'updateImpersonator',
|
||||
description: '/docs/references/users/update-user-impersonator.md',
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_USER,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject'])
|
||||
->param('impersonator', false, new Boolean(true), 'Whether the user can impersonate other users. When true, the user can browse project users to choose a target and can pass impersonation headers to act as that user. Internal audit logs still attribute impersonated actions to the original impersonator and store the target user details only in internal audit payload data.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, bool $impersonator, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['impersonator' => $impersonator]));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId());
|
||||
|
||||
$response->dynamic($user, Response::MODEL_USER);
|
||||
});
|
||||
|
||||
Http::patch('/v1/users/:userId/verification/phone')
|
||||
->desc('Update phone verification')
|
||||
->groups(['api', 'users'])
|
||||
|
||||
@@ -1190,6 +1190,15 @@ Http::error()
|
||||
->inject('devKey')
|
||||
->inject('authorization')
|
||||
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) {
|
||||
$trace = $error->getTrace();
|
||||
|
||||
foreach (array_slice($trace, 0, 100) as $index => $traceEntry) {
|
||||
$file = isset($traceEntry['file']) ? $traceEntry['file'] : '[internal function]';
|
||||
$line = isset($traceEntry['line']) ? $traceEntry['line'] : '';
|
||||
$function = isset($traceEntry['function']) ? $traceEntry['function'] : '';
|
||||
Console::error("[$index] $file : $line -> $function()");
|
||||
}
|
||||
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$route = $utopia->getRoute();
|
||||
$class = \get_class($error);
|
||||
|
||||
@@ -338,6 +338,19 @@ Http::init()
|
||||
|
||||
$scopes = \array_unique($scopes);
|
||||
|
||||
// Intentional: impersonators get users.read so they can discover a target user
|
||||
// before impersonation starts, and keep that access while impersonating.
|
||||
if (
|
||||
!$user->isEmpty()
|
||||
&& (
|
||||
$user->getAttribute('impersonator', false)
|
||||
|| $user->getAttribute('impersonatorUserId')
|
||||
)
|
||||
) {
|
||||
$scopes[] = 'users.read';
|
||||
$scopes = \array_unique($scopes);
|
||||
}
|
||||
|
||||
$authorization->addRole($role);
|
||||
foreach ($user->getRoles($authorization) as $authRole) {
|
||||
$authorization->addRole($authRole);
|
||||
@@ -369,8 +382,11 @@ Http::init()
|
||||
}
|
||||
|
||||
if (! empty($user->getId())) {
|
||||
$impersonatorUserId = $user->getAttribute('impersonatorUserId');
|
||||
$accessedAt = $user->getAttribute('accessedAt', 0);
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
|
||||
|
||||
// Skip updating accessedAt for impersonated requests so we don't attribute activity to the target user.
|
||||
if (! $impersonatorUserId && DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
|
||||
$user->setAttribute('accessedAt', DateTime::now());
|
||||
|
||||
if ($project->getId() !== 'console' && $mode !== APP_MODE_ADMIN) {
|
||||
@@ -470,6 +486,12 @@ Http::init()
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, Document $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) {
|
||||
|
||||
$route = $utopia->getRoute();
|
||||
$path = $route->getMatchedPath();
|
||||
$databaseType = match (true) {
|
||||
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
|
||||
str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB,
|
||||
default => '',
|
||||
};
|
||||
|
||||
if (
|
||||
array_key_exists('rest', $project->getAttribute('apis', []))
|
||||
|
||||
+19
-1
@@ -196,6 +196,8 @@ include __DIR__ . '/controllers/general.php';
|
||||
|
||||
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
|
||||
{
|
||||
$max = 15;
|
||||
$sleep = 2;
|
||||
$max = 15;
|
||||
$sleep = 2;
|
||||
$attempts = 0;
|
||||
@@ -409,13 +411,29 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot
|
||||
});
|
||||
|
||||
$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');
|
||||
|
||||
foreach ($sharedTablesV2 as $hostname) {
|
||||
// All shared tables V2 pools that need project metadata collections
|
||||
$sharedTablesV2All = \array_values(\array_unique(\array_filter([
|
||||
...$sharedTablesV2,
|
||||
...$documentsSharedTablesV2,
|
||||
...$vectorSharedTablesV2,
|
||||
])));
|
||||
|
||||
foreach ($sharedTablesV2All as $hostname) {
|
||||
Span::init('database.setup');
|
||||
Span::add('database.hostname', $hostname);
|
||||
|
||||
|
||||
+54
-1
@@ -47,7 +47,7 @@ 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 = 4321;
|
||||
const APP_VERSION_STABLE = '1.8.1';
|
||||
const APP_VERSION_STABLE = '1.9.0';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
|
||||
@@ -288,6 +288,45 @@ const METRIC_DATABASES_OPERATIONS_READS = 'databases.operations.reads';
|
||||
const METRIC_DATABASE_ID_OPERATIONS_READS = '{databaseInternalId}.databases.operations.reads';
|
||||
const METRIC_DATABASES_OPERATIONS_WRITES = 'databases.operations.writes';
|
||||
const METRIC_DATABASE_ID_OPERATIONS_WRITES = '{databaseInternalId}.databases.operations.writes';
|
||||
|
||||
// documentsdb
|
||||
const METRIC_DATABASES_DOCUMENTSDB = 'documentsdb.databases';
|
||||
const METRIC_COLLECTIONS_DOCUMENTSDB = 'documentsdb.collections';
|
||||
const METRIC_DATABASES_STORAGE_DOCUMENTSDB = 'documentsdb.databases.storage';
|
||||
const METRIC_DATABASE_ID_COLLECTIONS_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.collections';
|
||||
const METRIC_DATABASE_ID_STORAGE_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.databases.storage';
|
||||
const METRIC_DOCUMENTS_DOCUMENTSDB = 'documentsdb.documents';
|
||||
const METRIC_DATABASE_ID_DOCUMENTS_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.documents';
|
||||
const METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.{collectionInternalId}.documents';
|
||||
const METRIC_DATABASE_ID_COLLECTION_ID_STORAGE_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.{collectionInternalId}.databases.storage';
|
||||
const METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB = 'documentsdb.databases.operations.reads';
|
||||
const METRIC_DATABASE_ID_OPERATIONS_READS_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.databases.operations.reads';
|
||||
const METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB = 'documentsdb.databases.operations.writes';
|
||||
const METRIC_DATABASE_ID_OPERATIONS_WRITES_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.databases.operations.writes';
|
||||
|
||||
// vectorsdb
|
||||
const METRIC_DATABASES_VECTORSDB = 'vectorsdb.databases';
|
||||
const METRIC_COLLECTIONS_VECTORSDB = 'vectorsdb.collections';
|
||||
const METRIC_DATABASES_STORAGE_VECTORSDB = 'vectorsdb.databases.storage';
|
||||
const METRIC_DATABASE_ID_COLLECTIONS_VECTORSDB = 'vectorsdb.{databaseInternalId}.collections';
|
||||
const METRIC_DATABASE_ID_STORAGE_VECTORSDB = 'vectorsdb.{databaseInternalId}.databases.storage';
|
||||
const METRIC_DOCUMENTS_VECTORSDB = 'vectorsdb.documents';
|
||||
const METRIC_DATABASE_ID_DOCUMENTS_VECTORSDB = 'vectorsdb.{databaseInternalId}.documents';
|
||||
const METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS_VECTORSDB = 'vectorsdb.{databaseInternalId}.{collectionInternalId}.documents';
|
||||
const METRIC_DATABASE_ID_COLLECTION_ID_STORAGE_VECTORSDB = 'vectorsdb.{databaseInternalId}.{collectionInternalId}.databases.storage';
|
||||
const METRIC_DATABASES_OPERATIONS_READS_VECTORSDB = 'vectorsdb.databases.operations.reads';
|
||||
const METRIC_DATABASE_ID_OPERATIONS_READS_VECTORSDB = 'vectorsdb.{databaseInternalId}.databases.operations.reads';
|
||||
const METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB = 'vectorsdb.databases.operations.writes';
|
||||
const METRIC_DATABASE_ID_OPERATIONS_WRITES_VECTORSDB = 'vectorsdb.{databaseInternalId}.databases.operations.writes';
|
||||
const METRIC_EMBEDDINGS_TEXT = 'embeddings.text';
|
||||
const METRIC_EMBEDDINGS_MODEL_TEXT = 'embeddings.text.{embeddingModel}';
|
||||
const METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR = 'embeddings.text.totalErrors';
|
||||
const METRIC_EMBEDDINGS_MODEL_TEXT_TOTAL_ERROR = 'embeddings.text.{embeddingModel}.totalErrors';
|
||||
const METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION = 'embeddings.text.totalDuration';
|
||||
const METRIC_EMBEDDINGS_MODEL_TEXT_TOTAL_DURATION = 'embeddings.text.{embeddingModel}.totalDuration';
|
||||
const METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS = 'embeddings.text.totalTokens';
|
||||
const METRIC_EMBEDDINGS_MODEL_TEXT_TOTAL_TOKENS = 'embeddings.text.{embeddingModel}.totalTokens';
|
||||
|
||||
const METRIC_BUCKETS = 'buckets';
|
||||
const METRIC_FILES = 'files';
|
||||
const METRIC_FILES_STORAGE = 'files.storage';
|
||||
@@ -380,6 +419,7 @@ const RESOURCE_TYPE_SUBSCRIBERS = 'subscribers';
|
||||
const RESOURCE_TYPE_MESSAGES = 'messages';
|
||||
const RESOURCE_TYPE_EXECUTIONS = 'executions';
|
||||
const RESOURCE_TYPE_VCS = 'vcs';
|
||||
const RESOURCE_TYPE_EMBEDDINGS_TEXT = 'embeddingsText';
|
||||
|
||||
// Resource types for Tokens
|
||||
const TOKENS_RESOURCE_TYPE_FILES = 'files';
|
||||
@@ -401,3 +441,16 @@ const CACHE_RECONNECT_RETRY_DELAY = 1000;
|
||||
|
||||
// Project status
|
||||
const PROJECT_STATUS_ACTIVE = 'active';
|
||||
|
||||
// Database types
|
||||
const DATABASE_TYPE_LEGACY = 'legacy';
|
||||
const DATABASE_TYPE_TABLESDB = 'tablesdb';
|
||||
const DATABASE_TYPE_DOCUMENTSDB = 'documentsdb';
|
||||
const DATABASE_TYPE_VECTORSDB = 'vectorsdb';
|
||||
|
||||
// CSV import/export allowed database types
|
||||
const CSV_ALLOWED_DATABASE_TYPES = [
|
||||
DATABASE_TYPE_LEGACY,
|
||||
DATABASE_TYPE_TABLESDB,
|
||||
DATABASE_TYPE_VECTORSDB
|
||||
];
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Network\Validator\Email;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\Structure;
|
||||
use Utopia\Emails\Validator\Email;
|
||||
use Utopia\Validator\IP;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\URL;
|
||||
|
||||
+23
-1
@@ -22,6 +22,7 @@ use Appwrite\Utopia\Response\Model\AttributeLine;
|
||||
use Appwrite\Utopia\Response\Model\AttributeList;
|
||||
use Appwrite\Utopia\Response\Model\AttributeLongtext;
|
||||
use Appwrite\Utopia\Response\Model\AttributeMediumtext;
|
||||
use Appwrite\Utopia\Response\Model\AttributeObject;
|
||||
use Appwrite\Utopia\Response\Model\AttributePoint;
|
||||
use Appwrite\Utopia\Response\Model\AttributePolygon;
|
||||
use Appwrite\Utopia\Response\Model\AttributeRelationship;
|
||||
@@ -29,6 +30,7 @@ use Appwrite\Utopia\Response\Model\AttributeString;
|
||||
use Appwrite\Utopia\Response\Model\AttributeText;
|
||||
use Appwrite\Utopia\Response\Model\AttributeURL;
|
||||
use Appwrite\Utopia\Response\Model\AttributeVarchar;
|
||||
use Appwrite\Utopia\Response\Model\AttributeVector;
|
||||
use Appwrite\Utopia\Response\Model\AuthProvider;
|
||||
use Appwrite\Utopia\Response\Model\BaseList;
|
||||
use Appwrite\Utopia\Response\Model\Branch;
|
||||
@@ -65,6 +67,7 @@ use Appwrite\Utopia\Response\Model\DetectionRuntime;
|
||||
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\Error;
|
||||
use Appwrite\Utopia\Response\Model\ErrorDev;
|
||||
use Appwrite\Utopia\Response\Model\Execution;
|
||||
@@ -136,6 +139,8 @@ use Appwrite\Utopia\Response\Model\UsageBuckets;
|
||||
use Appwrite\Utopia\Response\Model\UsageCollection;
|
||||
use Appwrite\Utopia\Response\Model\UsageDatabase;
|
||||
use Appwrite\Utopia\Response\Model\UsageDatabases;
|
||||
use Appwrite\Utopia\Response\Model\UsageDocumentsDB;
|
||||
use Appwrite\Utopia\Response\Model\UsageDocumentsDBs;
|
||||
use Appwrite\Utopia\Response\Model\UsageFunction;
|
||||
use Appwrite\Utopia\Response\Model\UsageFunctions;
|
||||
use Appwrite\Utopia\Response\Model\UsageProject;
|
||||
@@ -144,9 +149,12 @@ use Appwrite\Utopia\Response\Model\UsageSites;
|
||||
use Appwrite\Utopia\Response\Model\UsageStorage;
|
||||
use Appwrite\Utopia\Response\Model\UsageTable;
|
||||
use Appwrite\Utopia\Response\Model\UsageUsers;
|
||||
use Appwrite\Utopia\Response\Model\UsageVectorsDB;
|
||||
use Appwrite\Utopia\Response\Model\UsageVectorsDBs;
|
||||
use Appwrite\Utopia\Response\Model\User;
|
||||
use Appwrite\Utopia\Response\Model\Variable;
|
||||
use Appwrite\Utopia\Response\Model\VcsContent;
|
||||
use Appwrite\Utopia\Response\Model\VectorsDBCollection;
|
||||
use Appwrite\Utopia\Response\Model\Webhook;
|
||||
|
||||
// General
|
||||
@@ -185,7 +193,7 @@ Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, '
|
||||
Response::setModel(new BaseList('Deployments List', Response::MODEL_DEPLOYMENT_LIST, 'deployments', Response::MODEL_DEPLOYMENT));
|
||||
Response::setModel(new BaseList('Executions List', Response::MODEL_EXECUTION_LIST, 'executions', Response::MODEL_EXECUTION));
|
||||
Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, false));
|
||||
Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, false));
|
||||
Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, true));
|
||||
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true));
|
||||
Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false));
|
||||
Response::setModel(new BaseList('Auth Providers List', Response::MODEL_AUTH_PROVIDER_LIST, 'platforms', Response::MODEL_AUTH_PROVIDER, true, false));
|
||||
@@ -211,9 +219,12 @@ Response::setModel(new BaseList('Migrations List', Response::MODEL_MIGRATION_LIS
|
||||
Response::setModel(new BaseList('Migrations Firebase Projects List', Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', Response::MODEL_MIGRATION_FIREBASE_PROJECT));
|
||||
Response::setModel(new BaseList('Specifications List', Response::MODEL_SPECIFICATION_LIST, 'specifications', Response::MODEL_SPECIFICATION));
|
||||
Response::setModel(new BaseList('VCS Content List', Response::MODEL_VCS_CONTENT_LIST, 'contents', Response::MODEL_VCS_CONTENT));
|
||||
Response::setModel(new BaseList('VectorsDB Collections List', Response::MODEL_VECTORSDB_COLLECTION_LIST, 'collections', Response::MODEL_VECTORSDB_COLLECTION));
|
||||
Response::setModel(new BaseList('Embedding list', Response::MODEL_EMBEDDING_LIST, 'embeddings', Response::MODEL_EMBEDDING));
|
||||
|
||||
// Entities
|
||||
Response::setModel(new Database());
|
||||
Response::setModel(new Embedding());
|
||||
|
||||
// Collection API Models
|
||||
Response::setModel(new Collection());
|
||||
@@ -237,6 +248,17 @@ Response::setModel(new AttributeText());
|
||||
Response::setModel(new AttributeMediumtext());
|
||||
Response::setModel(new AttributeLongtext());
|
||||
|
||||
// DocumentsDB API Models
|
||||
Response::setModel(new UsageDocumentsDBs());
|
||||
Response::setModel(new UsageDocumentsDB());
|
||||
|
||||
// VectorsDB API Models
|
||||
Response::setModel(new VectorsDBCollection());
|
||||
Response::setModel(new AttributeObject());
|
||||
Response::setModel(new AttributeVector());
|
||||
Response::setModel(new UsageVectorsDBs());
|
||||
Response::setModel(new UsageVectorsDB());
|
||||
|
||||
// Table API Models
|
||||
Response::setModel(new Table());
|
||||
Response::setModel(new Column());
|
||||
|
||||
+31
-3
@@ -160,7 +160,6 @@ $register->set('pools', function () {
|
||||
'pass' => System::getEnv('_APP_DB_PASS', ''),
|
||||
'path' => System::getEnv('_APP_DB_SCHEMA', ''),
|
||||
]);
|
||||
|
||||
$fallbackForRedis = 'redis_main=' . AppwriteURL::unparse([
|
||||
'scheme' => 'redis',
|
||||
'host' => System::getEnv('_APP_REDIS_HOST', 'redis'),
|
||||
@@ -169,6 +168,23 @@ $register->set('pools', function () {
|
||||
'pass' => System::getEnv('_APP_REDIS_PASS', ''),
|
||||
]);
|
||||
|
||||
$fallbackForDocumentsDB = 'db_main=' . AppwriteURL::unparse([
|
||||
'scheme' => System::getEnv('_APP_DB_ADAPTER_DOCUMENTSDB', 'mongodb'),
|
||||
'host' => System::getEnv('_APP_DB_HOST_DOCUMENTSDB', 'mongodb'),
|
||||
'port' => System::getEnv('_APP_DB_PORT_DOCUMENTSDB', '27017'),
|
||||
'user' => System::getEnv('_APP_DB_USER', ''),
|
||||
'pass' => System::getEnv('_APP_DB_PASS', ''),
|
||||
'path' => System::getEnv('_APP_DB_SCHEMA', ''),
|
||||
]);
|
||||
$fallbackForVectorsDB = 'db_main=' . AppwriteURL::unparse([
|
||||
'scheme' => System::getEnv('_APP_DB_ADAPTER_VECTORSDB', 'postgresql'),
|
||||
'host' => System::getEnv('_APP_DB_HOST_VECTORSDB', 'postgresql'),
|
||||
'port' => System::getEnv('_APP_DB_PORT_VECTORSDB', '5432'),
|
||||
'user' => System::getEnv('_APP_DB_USER', ''),
|
||||
'pass' => System::getEnv('_APP_DB_PASS', ''),
|
||||
'path' => System::getEnv('_APP_DB_SCHEMA', ''),
|
||||
]);
|
||||
|
||||
$connections = [
|
||||
'console' => [
|
||||
'type' => 'database',
|
||||
@@ -180,13 +196,25 @@ $register->set('pools', function () {
|
||||
'type' => 'database',
|
||||
'dsns' => $fallbackForDB,
|
||||
'multiple' => true,
|
||||
'schemes' => ['mariadb', 'mongodb', 'mysql', 'postgresql'],
|
||||
'schemes' => ['mongodb','mariadb', 'mysql','postgresql'],
|
||||
],
|
||||
'documentsdb' => [
|
||||
'type' => 'database',
|
||||
'dsns' => System::getEnv('_APP_CONNECTIONS_DATABASE_DOCUMENTSDB', $fallbackForDocumentsDB),
|
||||
'multiple' => true,
|
||||
'schemes' => ['mongodb'],
|
||||
],
|
||||
'vectorsdb' => [
|
||||
'type' => 'database',
|
||||
'dsns' => System::getEnv('_APP_CONNECTIONS_DATABASE_VECTORSDB', $fallbackForVectorsDB),
|
||||
'multiple' => true,
|
||||
'schemes' => ['postgresql'],
|
||||
],
|
||||
'logs' => [
|
||||
'type' => 'database',
|
||||
'dsns' => System::getEnv('_APP_CONNECTIONS_DB_LOGS', $fallbackForDB),
|
||||
'multiple' => false,
|
||||
'schemes' => ['mariadb', 'mongodb', 'mysql', 'postgresql'],
|
||||
'schemes' => ['mongodb','mariadb', 'mysql','postgresql'],
|
||||
],
|
||||
'publisher' => [
|
||||
'type' => 'publisher',
|
||||
|
||||
+234
-19
@@ -32,6 +32,8 @@ use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Executor\Executor;
|
||||
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
|
||||
use Utopia\Agents\Adapters\Ollama;
|
||||
use Utopia\Agents\Agent;
|
||||
use Utopia\Audit\Adapter\Database as AdapterDatabase;
|
||||
use Utopia\Audit\Audit;
|
||||
use Utopia\Auth\Hashes\Argon2;
|
||||
@@ -470,13 +472,38 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
}
|
||||
}
|
||||
|
||||
// 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', '');
|
||||
if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) {
|
||||
$userDb = (APP_MODE_ADMIN === $mode || $project->getId() === 'console') ? $dbForPlatform : $dbForProject;
|
||||
$targetUser = null;
|
||||
if (!empty($impersonateUserId)) {
|
||||
$targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->getDocument('users', $impersonateUserId));
|
||||
} elseif (!empty($impersonateEmail)) {
|
||||
$targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->findOne('users', [Query::equal('email', [\strtolower($impersonateEmail)])]));
|
||||
} elseif (!empty($impersonatePhone)) {
|
||||
$targetUser = $userDb->getAuthorization()->skip(fn () => $userDb->findOne('users', [Query::equal('phone', [$impersonatePhone])]));
|
||||
}
|
||||
if ($targetUser !== null && !$targetUser->isEmpty()) {
|
||||
$impersonator = clone $user;
|
||||
$user = clone $targetUser;
|
||||
$user->setAttribute('impersonatorUserId', $impersonator->getId());
|
||||
$user->setAttribute('impersonatorUserInternalId', $impersonator->getSequence());
|
||||
$user->setAttribute('impersonatorUserName', $impersonator->getAttribute('name', ''));
|
||||
$user->setAttribute('impersonatorUserEmail', $impersonator->getAttribute('email', ''));
|
||||
$user->setAttribute('impersonatorAccessedAt', $impersonator->getAttribute('accessedAt', 0));
|
||||
}
|
||||
}
|
||||
|
||||
$dbForProject->setMetadata('user', $user->getId());
|
||||
$dbForPlatform->setMetadata('user', $user->getId());
|
||||
|
||||
return $user;
|
||||
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']);
|
||||
|
||||
Http::setResource('project', function ($dbForPlatform, $request, $console, $authorization) {
|
||||
Http::setResource('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) {
|
||||
/** @var Appwrite\Utopia\Request $request */
|
||||
/** @var Utopia\Database\Database $dbForPlatform */
|
||||
/** @var Utopia\Database\Document $console */
|
||||
@@ -486,6 +513,20 @@ Http::setResource('project', function ($dbForPlatform, $request, $console, $auth
|
||||
$projectId = $request->getHeader('x-appwrite-project', '');
|
||||
}
|
||||
|
||||
// Backwards compatibility for new services, originally project resources
|
||||
// These endpoints moved from /v1/projects/:projectId/<resource> to /v1/<resource>
|
||||
// When accessed via the old alias path, extract projectId from the URI
|
||||
$deprecatedProjectPathPrefix = '/v1/projects/';
|
||||
$route = $utopia->match($request);
|
||||
if (!empty($route)) {
|
||||
$isDeprecatedAlias = \str_starts_with($request->getURI(), $deprecatedProjectPathPrefix) &&
|
||||
!\str_starts_with($route->getPath(), $deprecatedProjectPathPrefix);
|
||||
|
||||
if ($isDeprecatedAlias) {
|
||||
$projectId = \explode('/', $request->getURI(), 5)[3] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($projectId) || $projectId === 'console') {
|
||||
return $console;
|
||||
}
|
||||
@@ -493,7 +534,7 @@ Http::setResource('project', function ($dbForPlatform, $request, $console, $auth
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
|
||||
return $project;
|
||||
}, ['dbForPlatform', 'request', 'console', 'authorization']);
|
||||
}, ['dbForPlatform', 'request', 'console', 'authorization', 'utopia']);
|
||||
|
||||
Http::setResource('session', function (User $user, Store $store, Token $proofForToken) {
|
||||
if ($user->isEmpty()) {
|
||||
@@ -555,7 +596,7 @@ Http::setResource('authorization', function () {
|
||||
return new Authorization();
|
||||
}, []);
|
||||
|
||||
Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization) {
|
||||
Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
@@ -661,7 +702,31 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
$dbForProject->getCache()->purge($cacheKey);
|
||||
};
|
||||
|
||||
$usageDatabaseListener = function (string $event, Document $document, UsageContext $usage) {
|
||||
/**
|
||||
* Prefix metrics with database type when applicable.
|
||||
* Avoids prefixing for legacy and tablesdb types to preserve historical metrics.
|
||||
*/
|
||||
$getDatabaseTypePrefixedMetric = function (string $databaseType, string $metric): string {
|
||||
if (
|
||||
$databaseType === '' ||
|
||||
$databaseType === DATABASE_TYPE_LEGACY ||
|
||||
$databaseType === DATABASE_TYPE_TABLESDB
|
||||
) {
|
||||
return $metric;
|
||||
}
|
||||
|
||||
return $databaseType . '.' . $metric;
|
||||
};
|
||||
|
||||
// Determine database type from request path, similar to api.php
|
||||
$path = $request->getURI();
|
||||
$databaseType = match (true) {
|
||||
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
|
||||
str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB,
|
||||
default => '',
|
||||
};
|
||||
|
||||
$usageDatabaseListener = function (string $event, Document $document, UsageContext $usage) use ($getDatabaseTypePrefixedMetric, $databaseType) {
|
||||
$value = 1;
|
||||
|
||||
switch ($event) {
|
||||
@@ -693,7 +758,8 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
$usage->addMetric(METRIC_SESSIONS, $value); // per project
|
||||
break;
|
||||
case $document->getCollection() === 'databases': // databases
|
||||
$usage->addMetric(METRIC_DATABASES, $value); // per project
|
||||
$metric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASES);
|
||||
$usage->addMetric($metric, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$usage->addReduce($document);
|
||||
@@ -702,9 +768,11 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
case str_starts_with($document->getCollection(), 'database_') && ! str_contains($document->getCollection(), 'collection'): // collections
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_COLLECTIONS);
|
||||
$databaseIdCollectionMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_COLLECTIONS);
|
||||
$usage
|
||||
->addMetric(METRIC_COLLECTIONS, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value);
|
||||
->addMetric($collectionMetric, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdCollectionMetric), $value);
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$usage->addReduce($document);
|
||||
@@ -714,10 +782,13 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionInternalId = $parts[3] ?? 0;
|
||||
$documentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DOCUMENTS);
|
||||
$databaseIdDocumentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_DOCUMENTS);
|
||||
$databaseIdCollectionIdDocumentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS);
|
||||
$usage
|
||||
->addMetric(METRIC_DOCUMENTS, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection
|
||||
->addMetric($documentsMetric, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
|
||||
break;
|
||||
case $document->getCollection() === 'buckets': // buckets
|
||||
$usage->addMetric(METRIC_BUCKETS, $value); // per project
|
||||
@@ -792,7 +863,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database));
|
||||
|
||||
return $database;
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization']);
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization', 'request']);
|
||||
|
||||
Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
|
||||
@@ -813,6 +884,138 @@ Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authori
|
||||
return $database;
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
|
||||
|
||||
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database {
|
||||
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
|
||||
$databaseType = $database->getAttribute('type', '');
|
||||
|
||||
try {
|
||||
$databaseDSN = new DSN($databaseDSN);
|
||||
} catch (\InvalidArgumentException) {
|
||||
// for old databases migrated through patch script
|
||||
// databaseDSN determines the adapter
|
||||
$databaseDSN = new DSN('mysql://'.$databaseDSN);
|
||||
}
|
||||
try {
|
||||
$dsn = new DSN($project->getAttribute('database'));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// TODO: Temporary until all projects are using shared tables
|
||||
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
|
||||
}
|
||||
|
||||
$pool = $pools->get($databaseDSN->getHost());
|
||||
|
||||
$adapter = new DatabasePool($pool);
|
||||
$database = new Database($adapter, $cache);
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setMetadata('host', \gethostname())
|
||||
->setMetadata('project', $project->getId())
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
// inside pools authorization needs to be set first
|
||||
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int)$project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
$timeout = \intval($request->getHeader('x-appwrite-timeout'));
|
||||
if (!empty($timeout) && Http::isDevelopment()) {
|
||||
$database->setTimeout($timeout);
|
||||
}
|
||||
|
||||
// Register database event listeners for usage stats collection
|
||||
$documentsMetric = METRIC_DOCUMENTS;
|
||||
$databaseIdDocumentsMetric = METRIC_DATABASE_ID_DOCUMENTS;
|
||||
$databaseIdCollectionIdDocumentsMetric = METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS;
|
||||
if ($databaseType !== DATABASE_TYPE_LEGACY && $databaseType !== DATABASE_TYPE_TABLESDB) {
|
||||
$documentsMetric = $databaseType. '.' .$documentsMetric;
|
||||
$databaseIdDocumentsMetric = $databaseType. '.' .$databaseIdDocumentsMetric;
|
||||
$databaseIdCollectionIdDocumentsMetric = $databaseType . '.' .$databaseIdCollectionIdDocumentsMetric;
|
||||
}
|
||||
$database
|
||||
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
|
||||
$value = 1;
|
||||
|
||||
if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionInternalId = $parts[3] ?? 0;
|
||||
$usage
|
||||
->addMetric($documentsMetric, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
|
||||
}
|
||||
})
|
||||
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
|
||||
$value = -1;
|
||||
|
||||
if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionInternalId = $parts[3] ?? 0;
|
||||
$usage
|
||||
->addMetric($documentsMetric, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
|
||||
}
|
||||
})
|
||||
->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
|
||||
$value = $document->getAttribute('modified', 0);
|
||||
|
||||
if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionInternalId = $parts[3] ?? 0;
|
||||
$usage
|
||||
->addMetric($documentsMetric, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
|
||||
}
|
||||
})
|
||||
->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
|
||||
$value = -1 * $document->getAttribute('modified', 0);
|
||||
|
||||
if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionInternalId = $parts[3] ?? 0;
|
||||
$usage
|
||||
->addMetric($documentsMetric, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
|
||||
}
|
||||
})
|
||||
->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
|
||||
$value = $document->getAttribute('created', 0);
|
||||
|
||||
if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionInternalId = $parts[3] ?? 0;
|
||||
$usage
|
||||
->addMetric($documentsMetric, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
|
||||
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
|
||||
}
|
||||
});
|
||||
|
||||
return $database;
|
||||
};
|
||||
|
||||
}, ['pools','cache','project','request','usage','authorization']);
|
||||
|
||||
Http::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
|
||||
$databases = [];
|
||||
|
||||
@@ -1072,16 +1275,21 @@ function getDevice(string $root, string $connection = ''): Device
|
||||
}
|
||||
}
|
||||
|
||||
Http::setResource('mode', function ($request) {
|
||||
/** @var Appwrite\Utopia\Request $request */
|
||||
|
||||
Http::setResource('mode', function (Request $request, Document $project) {
|
||||
/**
|
||||
* Defines the mode for the request:
|
||||
* - 'default' => Requests for Client and Server Side
|
||||
* - 'admin' => Request from the Console on non-console projects
|
||||
*/
|
||||
return $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT));
|
||||
}, ['request']);
|
||||
$mode = $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT));
|
||||
|
||||
$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', ''));
|
||||
if (!empty($projectId) && $project->getId() !== $projectId) {
|
||||
$mode = APP_MODE_ADMIN;
|
||||
}
|
||||
|
||||
return $mode;
|
||||
}, ['request', 'project']);
|
||||
|
||||
Http::setResource('geodb', function ($register) {
|
||||
/** @var Utopia\Registry\Registry $register */
|
||||
@@ -1441,9 +1649,9 @@ Http::setResource('resourceToken', function ($project, $dbForProject, $request,
|
||||
return new Document([]);
|
||||
}, ['project', 'dbForProject', 'request', 'authorization']);
|
||||
|
||||
Http::setResource('transactionState', function (Database $dbForProject, Authorization $authorization) {
|
||||
return new TransactionState($dbForProject, $authorization);
|
||||
}, ['dbForProject', 'authorization']);
|
||||
Http::setResource('transactionState', function (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) {
|
||||
return new TransactionState($dbForProject, $authorization, $getDatabasesDB);
|
||||
}, ['dbForProject', 'authorization', 'getDatabasesDB']);
|
||||
|
||||
Http::setResource('executionsRetentionCount', function (Document $project, array $plan) {
|
||||
if ($project->getId() === 'console' || empty($plan)) {
|
||||
@@ -1452,3 +1660,10 @@ Http::setResource('executionsRetentionCount', function (Document $project, array
|
||||
|
||||
return (int) ($plan['executionsRetentionCount'] ?? 100);
|
||||
}, ['project', 'plan']);
|
||||
|
||||
Http::setResource('embeddingAgent', function ($register) {
|
||||
$adapter = new Ollama();
|
||||
$adapter->setEndpoint(System::getEnv('_APP_EMBEDDING_ENDPOINT', 'http://ollama:11434/api/embed'));
|
||||
$adapter->setTimeout((int) System::getEnv('_APP_EMBEDDING_TIMEOUT', '30000'));
|
||||
return new Agent($adapter);
|
||||
}, ['register']);
|
||||
|
||||
@@ -13,7 +13,7 @@ $organization = $this->getParam('organization', '');
|
||||
$image = $this->getParam('image', '');
|
||||
$enableAssistant = $this->getParam('enableAssistant', false);
|
||||
$dbService = $this->getParam('database', 'mongodb');
|
||||
$allowedDbServices = ['mariadb', 'mongodb', 'postgresql'];
|
||||
$allowedDbServices = ['mariadb', 'mongodb'];
|
||||
if (!\in_array($dbService, $allowedDbServices, true)) {
|
||||
$dbService = 'mongodb';
|
||||
}
|
||||
@@ -194,7 +194,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
appwrite-console:
|
||||
<<: *x-logging
|
||||
container_name: appwrite-console
|
||||
image: <?php echo $organization; ?>/console:7.6.4
|
||||
image: <?php echo $organization; ?>/console:7.8.26
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
|
||||
@@ -9,6 +9,7 @@ $defaultAppDomain = ($defaultAppDomain === 'traefik') ? 'localhost' : $defaultAp
|
||||
$defaultEmailCertificates = $defaultEmailCertificates ?? '';
|
||||
$defaultDatabase = $vars['_APP_DB_ADAPTER']['default'] ?? 'mongodb';
|
||||
$lockedDatabase = $isUpgrade && empty($lockedDatabase) ? $defaultDatabase : $lockedDatabase;
|
||||
$enabledDatabases = $enabledDatabases ?? ['mongodb', 'mariadb', 'postgresql'];
|
||||
$isLocalInstall = $isLocalInstall ?? false;
|
||||
|
||||
|
||||
@@ -64,6 +65,7 @@ $installerVersion = @filemtime(__DIR__ . '/installer/js/installer.js') ?: time()
|
||||
data-default-secret-key=""
|
||||
data-default-assistant-openai-key=""
|
||||
data-default-database="<?php echo htmlspecialchars((string) $defaultDatabase, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-enabled-databases="<?php echo htmlspecialchars(json_encode(array_values($enabledDatabases)), ENT_QUOTES, 'UTF-8'); ?>"
|
||||
<?php if ($isLocalInstall) { ?>
|
||||
data-dev-mode="true"
|
||||
<?php } ?>
|
||||
|
||||
@@ -11,9 +11,13 @@
|
||||
--neutral-300: rgba(173, 173, 176, 1);
|
||||
--neutral-400: rgba(151, 151, 155, 1);
|
||||
--neutral-500: rgba(129, 129, 134, 1);
|
||||
--neutral-600: rgba(108, 108, 113, 1);
|
||||
--neutral-700: rgba(86, 86, 92, 1);
|
||||
--neutral-750: rgba(65, 65, 70, 1);
|
||||
--neutral-800: rgba(45, 45, 49, 1);
|
||||
--neutral-850: rgba(29, 29, 33, 1);
|
||||
--neutral-900: rgba(25, 25, 28, 1);
|
||||
--neutral-250: rgba(195, 195, 198, 1);
|
||||
|
||||
/* Warning colors */
|
||||
--web-orange-200: rgba(255, 213, 194, 1);
|
||||
@@ -172,6 +176,44 @@
|
||||
--fgColor-neutral-weak: var(--fgcolor-neutral-weak);
|
||||
--fgColor-accent: var(--fgcolor-accent);
|
||||
--fgColor-on-accent: var(--fgcolor-on-accent);
|
||||
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bgcolor-neutral-default: var(--neutral-900);
|
||||
--bgcolor-neutral-primary: var(--neutral-850);
|
||||
--bgcolor-neutral-secondary: var(--neutral-800);
|
||||
--bgcolor-neutral-tertiary: var(--neutral-800);
|
||||
--bgcolor-neutral-invert-weaker: var(--neutral-400);
|
||||
--bgcolor-neutral-invert-weak: var(--neutral-300);
|
||||
--bgcolor-success-weak: rgba(16, 185, 129, 0.12);
|
||||
--bgcolor-warning-weaker: rgba(254, 124, 67, 0.08);
|
||||
--bgcolor-warning-weak: rgba(254, 124, 67, 0.12);
|
||||
--bgcolor-error-weaker: rgba(255, 69, 58, 0.08);
|
||||
|
||||
--fgcolor-neutral-primary: var(--neutral-50);
|
||||
--fgcolor-neutral-secondary: var(--neutral-250);
|
||||
--fgcolor-neutral-tertiary: var(--neutral-500);
|
||||
--fgcolor-neutral-weak: var(--neutral-600);
|
||||
--fgcolor-on-accent: var(--neutral-0);
|
||||
--fgcolor-on-invert: var(--neutral-900);
|
||||
--fgcolor-on-success-weak: rgba(52, 211, 153, 1);
|
||||
--fgcolor-warning: var(--web-orange-500);
|
||||
--fgcolor-on-warning-weak: var(--web-orange-500);
|
||||
--fgcolor-error: var(--web-red-500);
|
||||
--fgcolor-on-error: var(--neutral-0);
|
||||
|
||||
--border-neutral: var(--neutral-800);
|
||||
--border-neutral-strong: var(--neutral-750);
|
||||
--border-neutral-stronger: var(--neutral-600);
|
||||
--border-focus: var(--neutral-600);
|
||||
|
||||
--overlay-neutral-hover: rgba(255, 255, 255, 0.04);
|
||||
--overlay-neutral-pressed: rgba(255, 255, 255, 0.08);
|
||||
--overlay-scrim: rgba(0, 0, 0, 0.48);
|
||||
}
|
||||
}
|
||||
|
||||
.installer-toast-stack {
|
||||
@@ -649,6 +691,19 @@ body {
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.install-counter {
|
||||
margin-left: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
color: var(--fgcolor-neutral-secondary);
|
||||
}
|
||||
|
||||
.install-row[data-status='in-progress'] .install-counter:not(:empty) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.install-row-toggle {
|
||||
margin-left: auto;
|
||||
width: 32px;
|
||||
@@ -855,6 +910,17 @@ body {
|
||||
gap: var(--gap-m);
|
||||
}
|
||||
|
||||
.install-global-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--gap-m);
|
||||
padding: var(--space-4) 0;
|
||||
}
|
||||
|
||||
.install-global-actions.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.install-error-details .button {
|
||||
align-self: center;
|
||||
margin-top: 0;
|
||||
@@ -1354,6 +1420,7 @@ body {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 0;
|
||||
color: var(--fgcolor-neutral-weak);
|
||||
}
|
||||
|
||||
.step-indicator svg {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<svg width="131" height="25" viewBox="0 0 131 25" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M38.2643 19.5087C40.4069 19.5087 41.49 18.3947 41.9609 17.6362H42.1728C42.267 18.4421 42.832 19.2006 43.9386 19.2006H46.0341V16.8304H45.4926C45.1159 16.8304 44.9275 16.617 44.9275 16.2852V6.78055H42.1492V8.29751H41.9373C41.3958 7.53903 40.2656 6.47242 38.1937 6.47242C34.8974 6.47242 32.4487 9.2219 32.4487 12.9906C32.4487 16.7593 34.9445 19.5087 38.2643 19.5087ZM38.7588 16.8067C36.8045 16.8067 35.2741 15.3608 35.2741 13.0143C35.2741 10.7151 36.7574 9.15079 38.7352 9.15079C40.6188 9.15079 42.1963 10.5492 42.1963 13.0143C42.1963 15.1238 40.8543 16.8067 38.7588 16.8067Z" fill="#19191D"/>
|
||||
<path d="M47.6745 24.0166H50.4528V17.6362H50.6647C51.1826 18.3947 52.2893 19.5087 54.4789 19.5087C57.7752 19.5087 60.1768 16.7118 60.1768 12.9906C60.1768 9.2456 57.6104 6.47242 54.2906 6.47242C52.1715 6.47242 51.1356 7.63384 50.6411 8.2738H50.4292V6.78055H47.6745V24.0166ZM53.8903 16.8778C51.9832 16.8778 50.4057 15.4556 50.4057 12.9906C50.4057 10.8811 51.7477 9.10339 53.8432 9.10339C55.7974 9.10339 57.3279 10.644 57.3279 12.9906C57.3279 15.2897 55.8445 16.8778 53.8903 16.8778Z" fill="#19191D"/>
|
||||
<path d="M61.6104 24.0166H64.3887V17.6362H64.6006C65.1186 18.3947 66.2252 19.5087 68.4149 19.5087C71.7112 19.5087 73.8839 16.7118 73.8839 12.9906C73.8839 9.2456 71.5464 6.47242 68.2265 6.47242C66.1075 6.47242 65.0715 7.63384 64.5771 8.2738H64.3652V6.78055H61.6104V24.0166ZM67.8263 16.8778C65.9191 16.8778 64.3416 15.4556 64.3416 12.9906C64.3416 10.8811 65.6837 9.10339 67.7792 9.10339C69.7334 9.10339 71.2638 10.644 71.2638 12.9906C71.2638 15.2897 69.7805 16.8778 67.8263 16.8778Z" fill="#19191D"/>
|
||||
<path d="M77.5565 19.489H81.4885L83.7252 9.74733H83.8665L86.1033 19.489H90.0117L93.1415 7.06896H90.3414L88.1046 16.8343H87.8927L85.6559 7.06896H81.9594L79.6991 16.8343H79.4872L77.2739 7.06896H74.3073L77.5565 19.489Z" fill="#19191D"/>
|
||||
<path d="M94.549 19.489H97.3273V13.3501C97.3273 11.0036 98.4104 9.55771 100.435 9.55771H101.66V6.76083H100.741C99.1638 6.76083 97.963 7.85114 97.4921 8.89405H97.3038V7.06896H94.549V19.489Z" fill="#19191D"/>
|
||||
<path d="M115.447 19.489H117.613V17.0003H115.47C114.623 17.0003 114.27 16.621 114.27 15.744V9.53401H117.754V7.06896H114.27V3.58472H111.633V7.06896H109.325V9.53401H111.468V15.7677C111.468 18.3987 113.045 19.489 115.447 19.489Z" fill="#19191D"/>
|
||||
<path d="M125.067 19.5087C127.633 19.5087 129.893 18.2288 130.694 15.6452L128.151 15.029C127.704 16.4037 126.409 17.1148 125.043 17.1148C123.018 17.1148 121.676 15.7875 121.653 13.7016H131V12.9195C131 9.2219 128.716 6.47242 124.949 6.47242C121.629 6.47242 118.78 9.10339 118.78 13.0143C118.78 16.8067 121.3 19.5087 125.067 19.5087ZM121.676 11.6632C121.841 10.17 123.183 8.91377 124.949 8.91377C126.644 8.91377 128.033 9.98037 128.175 11.6632H121.676Z" fill="#19191D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.09 19.489H105.312V9.53401H103.145V7.06896H108.09V19.489Z" fill="#19191D"/>
|
||||
<path d="M106.494 5.34533C107.507 5.34533 108.26 4.58686 108.26 3.59136C108.26 2.61956 107.507 1.86108 106.494 1.86108C105.482 1.86108 104.729 2.61956 104.729 3.59136C104.729 4.58686 105.482 5.34533 106.494 5.34533Z" fill="#19191D"/>
|
||||
<path d="M38.2643 19.5087C40.4069 19.5087 41.49 18.3947 41.9609 17.6362H42.1728C42.267 18.4421 42.832 19.2006 43.9386 19.2006H46.0341V16.8304H45.4926C45.1159 16.8304 44.9275 16.617 44.9275 16.2852V6.78055H42.1492V8.29751H41.9373C41.3958 7.53903 40.2656 6.47242 38.1937 6.47242C34.8974 6.47242 32.4487 9.2219 32.4487 12.9906C32.4487 16.7593 34.9445 19.5087 38.2643 19.5087ZM38.7588 16.8067C36.8045 16.8067 35.2741 15.3608 35.2741 13.0143C35.2741 10.7151 36.7574 9.15079 38.7352 9.15079C40.6188 9.15079 42.1963 10.5492 42.1963 13.0143C42.1963 15.1238 40.8543 16.8067 38.7588 16.8067Z" fill="currentColor"/>
|
||||
<path d="M47.6745 24.0166H50.4528V17.6362H50.6647C51.1826 18.3947 52.2893 19.5087 54.4789 19.5087C57.7752 19.5087 60.1768 16.7118 60.1768 12.9906C60.1768 9.2456 57.6104 6.47242 54.2906 6.47242C52.1715 6.47242 51.1356 7.63384 50.6411 8.2738H50.4292V6.78055H47.6745V24.0166ZM53.8903 16.8778C51.9832 16.8778 50.4057 15.4556 50.4057 12.9906C50.4057 10.8811 51.7477 9.10339 53.8432 9.10339C55.7974 9.10339 57.3279 10.644 57.3279 12.9906C57.3279 15.2897 55.8445 16.8778 53.8903 16.8778Z" fill="currentColor"/>
|
||||
<path d="M61.6104 24.0166H64.3887V17.6362H64.6006C65.1186 18.3947 66.2252 19.5087 68.4149 19.5087C71.7112 19.5087 73.8839 16.7118 73.8839 12.9906C73.8839 9.2456 71.5464 6.47242 68.2265 6.47242C66.1075 6.47242 65.0715 7.63384 64.5771 8.2738H64.3652V6.78055H61.6104V24.0166ZM67.8263 16.8778C65.9191 16.8778 64.3416 15.4556 64.3416 12.9906C64.3416 10.8811 65.6837 9.10339 67.7792 9.10339C69.7334 9.10339 71.2638 10.644 71.2638 12.9906C71.2638 15.2897 69.7805 16.8778 67.8263 16.8778Z" fill="currentColor"/>
|
||||
<path d="M77.5565 19.489H81.4885L83.7252 9.74733H83.8665L86.1033 19.489H90.0117L93.1415 7.06896H90.3414L88.1046 16.8343H87.8927L85.6559 7.06896H81.9594L79.6991 16.8343H79.4872L77.2739 7.06896H74.3073L77.5565 19.489Z" fill="currentColor"/>
|
||||
<path d="M94.549 19.489H97.3273V13.3501C97.3273 11.0036 98.4104 9.55771 100.435 9.55771H101.66V6.76083H100.741C99.1638 6.76083 97.963 7.85114 97.4921 8.89405H97.3038V7.06896H94.549V19.489Z" fill="currentColor"/>
|
||||
<path d="M115.447 19.489H117.613V17.0003H115.47C114.623 17.0003 114.27 16.621 114.27 15.744V9.53401H117.754V7.06896H114.27V3.58472H111.633V7.06896H109.325V9.53401H111.468V15.7677C111.468 18.3987 113.045 19.489 115.447 19.489Z" fill="currentColor"/>
|
||||
<path d="M125.067 19.5087C127.633 19.5087 129.893 18.2288 130.694 15.6452L128.151 15.029C127.704 16.4037 126.409 17.1148 125.043 17.1148C123.018 17.1148 121.676 15.7875 121.653 13.7016H131V12.9195C131 9.2219 128.716 6.47242 124.949 6.47242C121.629 6.47242 118.78 9.10339 118.78 13.0143C118.78 16.8067 121.3 19.5087 125.067 19.5087ZM121.676 11.6632C121.841 10.17 123.183 8.91377 124.949 8.91377C126.644 8.91377 128.033 9.98037 128.175 11.6632H121.676Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.09 19.489H105.312V9.53401H103.145V7.06896H108.09V19.489Z" fill="currentColor"/>
|
||||
<path d="M106.494 5.34533C107.507 5.34533 108.26 4.58686 108.26 3.59136C108.26 2.61956 107.507 1.86108 106.494 1.86108C105.482 1.86108 104.729 2.61956 104.729 3.59136C104.729 4.58686 105.482 5.34533 106.494 5.34533Z" fill="currentColor"/>
|
||||
<path d="M24.2577 16.4436V21.9248H10.6705C6.71194 21.9248 3.25559 19.7204 1.40636 16.4436C1.13754 15.9672 0.90225 15.4674 0.704883 14.9487C0.31744 13.9322 0.0738912 12.8415 0 11.7034V10.2214C0.0160422 9.96781 0.0413207 9.71617 0.0743773 9.46752C0.141949 8.95727 0.244035 8.45799 0.378206 7.97265C1.64748 3.37143 5.77469 0 10.6705 0C15.5662 0 19.693 3.37143 20.9622 7.97265H15.1526C14.1988 6.47279 12.5479 5.4812 10.6705 5.4812C8.79305 5.4812 7.14216 6.47279 6.18839 7.97265C5.89768 8.42859 5.67212 8.93136 5.52434 9.46752C5.39308 9.94289 5.32308 10.4442 5.32308 10.9624C5.32308 12.5335 5.96768 13.9497 7.00119 14.9487C7.95886 15.876 9.25 16.4436 10.6705 16.4436H24.2577Z" fill="#FD366E"/>
|
||||
<path d="M24.2578 9.46753V14.9487H14.3398C15.3733 13.9497 16.018 12.5335 16.018 10.9624C16.018 10.4442 15.9479 9.9429 15.8167 9.46753H24.2578Z" fill="#FD366E"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.1 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8Z" fill="#D8D8DB"/>
|
||||
<path d="M5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 261 B After Width: | Height: | Size: 266 B |
@@ -2,13 +2,20 @@
|
||||
const getBodyDataset = () => document.body?.dataset ?? {};
|
||||
const isUpgradeMode = () => getBodyDataset().upgrade === 'true';
|
||||
const getLockedDatabase = () => getBodyDataset().lockedDatabase || '';
|
||||
const getEnabledDatabases = () => {
|
||||
const raw = getBodyDataset().enabledDatabases;
|
||||
if (!raw) return ['mongodb', 'mariadb', 'postgresql'];
|
||||
try { return JSON.parse(raw); } catch (e) { return ['mongodb', 'mariadb', 'postgresql']; }
|
||||
};
|
||||
|
||||
const STEP_IDS = Object.freeze({
|
||||
CONFIG_FILES: 'config-files',
|
||||
DOCKER_COMPOSE: 'docker-compose',
|
||||
ENV_VARS: 'env-vars',
|
||||
DOCKER_CONTAINERS: 'docker-containers',
|
||||
ACCOUNT_SETUP: 'account-setup'
|
||||
ACCOUNT_SETUP: 'account-setup',
|
||||
SSL_CERTIFICATE: 'ssl-certificate',
|
||||
REDIRECT: 'redirect'
|
||||
});
|
||||
|
||||
const STATUS = Object.freeze({
|
||||
@@ -70,7 +77,7 @@
|
||||
{
|
||||
id: STEP_IDS.ACCOUNT_SETUP,
|
||||
inProgress: 'Creating Appwrite account...',
|
||||
done: 'Appwrite account created (redirecting...)'
|
||||
done: 'Appwrite account created'
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -95,6 +102,7 @@
|
||||
getBodyDataset,
|
||||
isUpgradeMode,
|
||||
getLockedDatabase,
|
||||
getEnabledDatabases,
|
||||
STEP_IDS,
|
||||
STATUS,
|
||||
SSE_EVENTS,
|
||||
|
||||
@@ -21,12 +21,13 @@
|
||||
storeInstallId,
|
||||
clearInstallId
|
||||
} = window.InstallerStepsState || {};
|
||||
const { extractHostname, isLocalHost } = window.InstallerStepsValidation || {};
|
||||
const { extractHostname, isLocalHost, isIPAddress } = window.InstallerStepsValidation || {};
|
||||
const { generateSecretKey } = window.InstallerStepsUI || {};
|
||||
const { showToast } = window.InstallerToast || {};
|
||||
|
||||
let activeInstall = null;
|
||||
let unloadGuard = null;
|
||||
let sseSessionDetails = null;
|
||||
const csrfToken = document.querySelector('meta[name="appwrite-installer-csrf"]')?.getAttribute('content') || '';
|
||||
|
||||
const withCsrfHeader = (headers = {}) => {
|
||||
@@ -110,10 +111,10 @@
|
||||
return normalized.summary || 'Installation failed.';
|
||||
}
|
||||
if (status === STATUS.COMPLETED) return step.done;
|
||||
return step.inProgress;
|
||||
return message || step.inProgress;
|
||||
};
|
||||
|
||||
const updateInstallRow = (row, step, status, message) => {
|
||||
const updateInstallRow = (row, step, status, message, details) => {
|
||||
if (!row || !step) return;
|
||||
row.dataset.status = status;
|
||||
row.dataset.step = step.id;
|
||||
@@ -137,6 +138,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
const counter = row.querySelector('[data-install-counter]');
|
||||
if (counter) {
|
||||
const started = details?.containerStarted ?? 0;
|
||||
const total = details?.containerTotal;
|
||||
counter.textContent = (status === STATUS.IN_PROGRESS && total > 0 && started < total)
|
||||
? `${started}/${total}`
|
||||
: '';
|
||||
}
|
||||
|
||||
// Show/hide "Navigate to Console" button for account setup errors
|
||||
const consoleBtn = row.querySelector('[data-install-console]');
|
||||
if (consoleBtn) {
|
||||
@@ -250,7 +260,7 @@
|
||||
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
||||
};
|
||||
|
||||
const buildRedirectUrl = () => {
|
||||
const buildRedirectUrl = (protocol) => {
|
||||
const dataset = getBodyDataset?.() ?? {};
|
||||
const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
|
||||
if (!rawDomain) return '';
|
||||
@@ -265,22 +275,53 @@
|
||||
} else if (normalizedHost === 'traefik') {
|
||||
host = rawDomain.replace(hostForProtocol, 'localhost');
|
||||
}
|
||||
let protocol = 'http';
|
||||
let port = httpPort;
|
||||
if (httpsPort && httpsPort !== '0' && !isLocalHost?.(normalizedHost)) {
|
||||
protocol = 'https';
|
||||
port = httpsPort;
|
||||
}
|
||||
if (!hasPort && port && ((protocol === 'http' && port !== '80') || (protocol === 'https' && port !== '443'))) {
|
||||
const port = protocol === 'https' ? httpsPort : httpPort;
|
||||
const defaultPort = protocol === 'https' ? '443' : '80';
|
||||
if (!hasPort && port && port !== defaultPort) {
|
||||
host = `${host}:${port}`;
|
||||
}
|
||||
return `${protocol}://${host}`;
|
||||
};
|
||||
|
||||
const redirectToApp = () => {
|
||||
const url = buildRedirectUrl();
|
||||
const normalizeHostname = (rawDomain) => {
|
||||
const hostname = extractHostname?.(rawDomain)?.toLowerCase?.() ?? '';
|
||||
if (hostname === '0.0.0.0' || hostname === 'traefik') return 'localhost';
|
||||
return hostname;
|
||||
};
|
||||
|
||||
const canUseHttps = () => {
|
||||
const dataset = getBodyDataset?.() ?? {};
|
||||
const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
|
||||
const httpsPort = (formState?.httpsPort || dataset.defaultHttpsPort || '').trim();
|
||||
if (!httpsPort || httpsPort === '0') return false;
|
||||
const hostname = normalizeHostname(rawDomain);
|
||||
return !isLocalHost?.(hostname) && !isIPAddress?.(hostname);
|
||||
};
|
||||
|
||||
const pollCertificate = async (domain, port, maxAttempts, intervalMs) => {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/install/certificate?domain=${encodeURIComponent(domain)}&port=${encodeURIComponent(port)}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.ready) return true;
|
||||
}
|
||||
} catch {
|
||||
// Installer server may have shut down
|
||||
}
|
||||
if (i < maxAttempts - 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const redirectToApp = (protocol) => {
|
||||
const url = buildRedirectUrl(protocol);
|
||||
if (!url) return;
|
||||
// Fire-and-forget: tell the installer server it can shut down
|
||||
fetch('/install/shutdown', { method: 'POST', headers: withCsrfHeader() }).catch(() => {});
|
||||
window.location.href = url;
|
||||
};
|
||||
@@ -317,7 +358,7 @@
|
||||
const normalizedDomain = (formState?.appDomain || '').trim() || 'localhost';
|
||||
const normalizedHttpPort = (formState?.httpPort || '').trim() || '80';
|
||||
const normalizedHttpsPort = (formState?.httpsPort || '').trim() || '443';
|
||||
const normalizedEmail = (formState?.emailCertificates || '').trim();
|
||||
const normalizedEmail = (formState?.emailCertificates || '').trim() || (formState?.accountEmail || '').trim();
|
||||
const normalizedAssistantKey = (formState?.assistantOpenAIKey || '').trim();
|
||||
const normalizedAccountEmail = (formState?.accountEmail || '').trim();
|
||||
const normalizedAccountPassword = (formState?.accountPassword || '').trim();
|
||||
@@ -405,6 +446,7 @@
|
||||
|
||||
const initStep5 = (root) => {
|
||||
if (!root) return;
|
||||
let resolvedProtocol = 'http';
|
||||
|
||||
if (activeInstall?.controller) {
|
||||
activeInstall.controller.abort();
|
||||
@@ -496,7 +538,7 @@
|
||||
if (!state) return;
|
||||
const row = ensureRow(step);
|
||||
if (row) {
|
||||
updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message);
|
||||
updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message, state.details);
|
||||
if (state.status === STATUS?.ERROR) {
|
||||
updateInstallErrorDetails(row, {
|
||||
message: state.message,
|
||||
@@ -524,6 +566,9 @@
|
||||
inProgress: payload.message || payload.step,
|
||||
done: payload.message || payload.step
|
||||
};
|
||||
if (step.id === STEP_IDS.ACCOUNT_SETUP && payload.details?.sessionSecret) {
|
||||
sseSessionDetails = payload.details;
|
||||
}
|
||||
progressState.set(step.id, {
|
||||
status: payload.status || STATUS.IN_PROGRESS,
|
||||
message: payload.message,
|
||||
@@ -543,6 +588,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
if (payload.status === STATUS.ERROR) {
|
||||
showGlobalActions();
|
||||
}
|
||||
scheduleFallback();
|
||||
};
|
||||
|
||||
@@ -580,6 +628,7 @@
|
||||
|
||||
const applySnapshot = (snapshot) => {
|
||||
if (!snapshot || !snapshot.steps) return;
|
||||
let hasErrors = false;
|
||||
INSTALLATION_STEPS.forEach((step) => {
|
||||
const detail = snapshot.steps[step.id];
|
||||
if (!detail) return;
|
||||
@@ -588,8 +637,14 @@
|
||||
message: detail.message,
|
||||
details: snapshot.details?.[step.id]
|
||||
});
|
||||
if (detail.status === STATUS.ERROR) {
|
||||
hasErrors = true;
|
||||
}
|
||||
});
|
||||
renderProgress();
|
||||
if (hasErrors) {
|
||||
showGlobalActions();
|
||||
}
|
||||
};
|
||||
|
||||
const checkAllCompleted = () => {
|
||||
@@ -599,11 +654,9 @@
|
||||
});
|
||||
if (!allDone) return;
|
||||
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
|
||||
const sessionDetails = accountState?.details;
|
||||
const sessionDetails = sseSessionDetails || accountState?.details;
|
||||
finalizeInstall();
|
||||
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
|
||||
setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
|
||||
});
|
||||
startSslCheck(sessionDetails);
|
||||
};
|
||||
|
||||
const startPolling = () => {
|
||||
@@ -640,6 +693,77 @@
|
||||
}
|
||||
stopSyncedSpinnerRotation();
|
||||
setUnloadGuard(false);
|
||||
clearInstallLock?.();
|
||||
};
|
||||
|
||||
const SSL_STEP = {
|
||||
id: STEP_IDS.SSL_CERTIFICATE,
|
||||
inProgress: 'Generating SSL certificate...',
|
||||
done: 'SSL certificate verified'
|
||||
};
|
||||
|
||||
const REDIRECT_STEP = {
|
||||
id: STEP_IDS.REDIRECT,
|
||||
inProgress: 'Redirecting to console...',
|
||||
done: 'Redirecting to console...'
|
||||
};
|
||||
|
||||
const showRedirectStep = (sessionDetails, protocol) => {
|
||||
animatePanelHeight(() => {
|
||||
progressState.set(REDIRECT_STEP.id, {
|
||||
status: STATUS.IN_PROGRESS,
|
||||
message: REDIRECT_STEP.inProgress
|
||||
});
|
||||
const row = ensureRow(REDIRECT_STEP);
|
||||
if (row) {
|
||||
updateInstallRow(row, REDIRECT_STEP, STATUS.IN_PROGRESS, REDIRECT_STEP.inProgress);
|
||||
}
|
||||
});
|
||||
startSyncedSpinnerRotation(list);
|
||||
|
||||
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
|
||||
setTimeout(() => redirectToApp(protocol), TIMINGS?.redirectDelay ?? 0);
|
||||
});
|
||||
};
|
||||
|
||||
const startSslCheck = (sessionDetails) => {
|
||||
if (!canUseHttps()) {
|
||||
showRedirectStep(sessionDetails, 'http');
|
||||
return;
|
||||
}
|
||||
|
||||
animatePanelHeight(() => {
|
||||
progressState.set(SSL_STEP.id, {
|
||||
status: STATUS.IN_PROGRESS,
|
||||
message: SSL_STEP.inProgress
|
||||
});
|
||||
const row = ensureRow(SSL_STEP);
|
||||
if (row) {
|
||||
updateInstallRow(row, SSL_STEP, STATUS.IN_PROGRESS, SSL_STEP.inProgress);
|
||||
}
|
||||
});
|
||||
startSyncedSpinnerRotation(list);
|
||||
|
||||
const dataset = getBodyDataset?.() ?? {};
|
||||
const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
|
||||
const httpsPort = (formState?.httpsPort || dataset.defaultHttpsPort || '443').trim();
|
||||
const domain = normalizeHostname(rawDomain);
|
||||
pollCertificate(domain, httpsPort, 15, 2000).then((ready) => {
|
||||
stopSyncedSpinnerRotation();
|
||||
const certMessage = ready ? SSL_STEP.done : 'Certificate not ready, continuing over HTTP';
|
||||
animatePanelHeight(() => {
|
||||
progressState.set(SSL_STEP.id, {
|
||||
status: STATUS.COMPLETED,
|
||||
message: certMessage
|
||||
});
|
||||
const row = ensureRow(SSL_STEP);
|
||||
if (row) {
|
||||
updateInstallRow(row, SSL_STEP, STATUS.COMPLETED, certMessage);
|
||||
}
|
||||
});
|
||||
resolvedProtocol = ready ? 'https' : 'http';
|
||||
showRedirectStep(sessionDetails, resolvedProtocol);
|
||||
});
|
||||
};
|
||||
|
||||
const startInstallStream = async (installId, options = {}) => {
|
||||
@@ -740,11 +864,9 @@
|
||||
}
|
||||
|
||||
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
|
||||
const sessionDetails = accountState?.details;
|
||||
const sessionDetails = sseSessionDetails || accountState?.details;
|
||||
finalizeInstall();
|
||||
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
|
||||
setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
|
||||
});
|
||||
startSslCheck(sessionDetails);
|
||||
return;
|
||||
}
|
||||
if (event === SSE_EVENTS.ERROR) {
|
||||
@@ -788,9 +910,22 @@
|
||||
}
|
||||
};
|
||||
|
||||
const isSnapshotTerminal = (snapshot) => {
|
||||
if (!snapshot?.steps) return true;
|
||||
const stepEntries = Object.values(snapshot.steps);
|
||||
if (stepEntries.length === 0) return true;
|
||||
const hasError = stepEntries.some((s) => s.status === STATUS.ERROR);
|
||||
if (hasError) return true;
|
||||
const allCompleted = INSTALLATION_STEPS.every((step) => {
|
||||
const detail = snapshot.steps[step.id];
|
||||
return detail && detail.status === STATUS.COMPLETED;
|
||||
});
|
||||
return allCompleted;
|
||||
};
|
||||
|
||||
const resumeInstall = async (installId) => {
|
||||
const snapshot = await fetchInstallStatus(installId);
|
||||
if (!snapshot) return false;
|
||||
if (!snapshot || isSnapshotTerminal(snapshot)) return false;
|
||||
activeInstall = {
|
||||
installId,
|
||||
controller: new AbortController(),
|
||||
@@ -853,7 +988,7 @@
|
||||
const retryButton = event.target.closest('[data-install-retry]');
|
||||
|
||||
if (consoleButton) {
|
||||
redirectToApp();
|
||||
redirectToApp(resolvedProtocol);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -864,6 +999,60 @@
|
||||
}
|
||||
});
|
||||
|
||||
const globalActions = root.querySelector('[data-install-global-actions]');
|
||||
|
||||
const showGlobalActions = () => {
|
||||
if (globalActions) {
|
||||
globalActions.classList.remove('is-hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const performReset = async (hard) => {
|
||||
const installId = activeInstall?.installId || getInstallLock?.()?.installId || getStoredInstallId?.();
|
||||
|
||||
try {
|
||||
const res = await fetch('/install/reset', {
|
||||
method: 'POST',
|
||||
headers: withCsrfHeader({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ installId: installId || '', hard })
|
||||
});
|
||||
if (hard && !res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
showToast?.({
|
||||
status: 'error',
|
||||
title: 'Reset failed',
|
||||
description: data?.message || 'Could not stop containers. Try running "docker compose down -v" manually.',
|
||||
dismissible: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Reset request failed:', e);
|
||||
}
|
||||
|
||||
clearInstallLock?.();
|
||||
clearInstallId?.();
|
||||
cleanupInstallFlow();
|
||||
window.location.href = '/?step=1';
|
||||
};
|
||||
|
||||
const startOverButton = root.querySelector('[data-install-start-over]');
|
||||
if (startOverButton) {
|
||||
startOverButton.addEventListener('click', () => performReset(false));
|
||||
}
|
||||
|
||||
const hardResetButton = root.querySelector('[data-install-hard-reset]');
|
||||
if (hardResetButton) {
|
||||
hardResetButton.addEventListener('click', () => {
|
||||
const confirmed = window.confirm(
|
||||
'This will stop all containers, remove all volumes (including database data, uploads, and certificates), and delete configuration files.\n\nThis action cannot be undone. Continue?'
|
||||
);
|
||||
if (confirmed) {
|
||||
performReset(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// When the user switches back to this tab, check if installation
|
||||
// finished while the tab was in the background.
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
@@ -872,22 +1061,24 @@
|
||||
}
|
||||
});
|
||||
|
||||
const startFreshInstall = () => {
|
||||
clearInstallId?.();
|
||||
clearInstallLock?.();
|
||||
const newInstallId = generateInstallId();
|
||||
storeInstallId?.(newInstallId);
|
||||
startInstallStream(newInstallId);
|
||||
};
|
||||
|
||||
const lock = getInstallLock?.();
|
||||
const existingInstallId = lock?.installId || getStoredInstallId?.();
|
||||
if (existingInstallId) {
|
||||
resumeInstall(existingInstallId).then((resumed) => {
|
||||
if (!resumed) {
|
||||
clearInstallId?.();
|
||||
clearInstallLock?.();
|
||||
const newInstallId = generateInstallId();
|
||||
storeInstallId?.(newInstallId);
|
||||
startInstallStream(newInstallId);
|
||||
startFreshInstall();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const newInstallId = generateInstallId();
|
||||
storeInstallId?.(newInstallId);
|
||||
startInstallStream(newInstallId);
|
||||
startFreshInstall();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
const INSTALL_LOCK_KEY = 'appwrite-install-lock';
|
||||
const INSTALL_ID_KEY = 'appwrite-install-id';
|
||||
const INSTALL_LOCK_LOCAL_KEY = 'appwrite-install-lock-backup';
|
||||
const INSTALL_ID_LOCAL_KEY = 'appwrite-install-id-backup';
|
||||
|
||||
const formState = {
|
||||
appDomain: null,
|
||||
@@ -55,13 +57,24 @@
|
||||
const getInstallLock = () => {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(INSTALL_LOCK_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== 'object') return null;
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object') return parsed;
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(INSTALL_LOCK_LOCAL_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
sessionStorage.setItem(INSTALL_LOCK_KEY, raw);
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const setInstallLock = (installId, payload) => {
|
||||
@@ -79,6 +92,9 @@
|
||||
try {
|
||||
sessionStorage.setItem(INSTALL_LOCK_KEY, JSON.stringify(lock));
|
||||
} catch (error) {}
|
||||
try {
|
||||
localStorage.setItem(INSTALL_LOCK_LOCAL_KEY, JSON.stringify(lock));
|
||||
} catch (error) {}
|
||||
if (document.body) {
|
||||
document.body.dataset.installLocked = 'true';
|
||||
}
|
||||
@@ -89,6 +105,9 @@
|
||||
try {
|
||||
sessionStorage.removeItem(INSTALL_LOCK_KEY);
|
||||
} catch (error) {}
|
||||
try {
|
||||
localStorage.removeItem(INSTALL_LOCK_LOCAL_KEY);
|
||||
} catch (error) {}
|
||||
if (document.body) {
|
||||
delete document.body.dataset.installLocked;
|
||||
}
|
||||
@@ -121,22 +140,31 @@
|
||||
|
||||
const getStoredInstallId = () => {
|
||||
try {
|
||||
return sessionStorage.getItem(INSTALL_ID_KEY);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
const val = sessionStorage.getItem(INSTALL_ID_KEY);
|
||||
if (val) return val;
|
||||
} catch (error) {}
|
||||
try {
|
||||
return localStorage.getItem(INSTALL_ID_LOCAL_KEY);
|
||||
} catch (error) {}
|
||||
return null;
|
||||
};
|
||||
|
||||
const storeInstallId = (installId) => {
|
||||
try {
|
||||
sessionStorage.setItem(INSTALL_ID_KEY, installId);
|
||||
} catch (error) {}
|
||||
try {
|
||||
localStorage.setItem(INSTALL_ID_LOCAL_KEY, installId);
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
const clearInstallId = () => {
|
||||
try {
|
||||
sessionStorage.removeItem(INSTALL_ID_KEY);
|
||||
} catch (error) {}
|
||||
try {
|
||||
localStorage.removeItem(INSTALL_ID_LOCAL_KEY);
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
window.InstallerStepsState = {
|
||||
|
||||
@@ -240,6 +240,9 @@
|
||||
if (key === 'database') {
|
||||
value = toDatabaseLabel(formState?.database);
|
||||
}
|
||||
if (key === 'emailCertificates' && !value) {
|
||||
value = formState?.accountEmail;
|
||||
}
|
||||
if (value) {
|
||||
node.textContent = value;
|
||||
}
|
||||
|
||||
@@ -106,12 +106,18 @@
|
||||
return LOCAL_HOSTS.has(normalized);
|
||||
};
|
||||
|
||||
const isIPAddress = (host) => {
|
||||
if (!host) return false;
|
||||
return isValidIPv4(host) || isValidIPv6(host);
|
||||
};
|
||||
|
||||
window.InstallerStepsValidation = {
|
||||
isValidEmail,
|
||||
isValidPort,
|
||||
isValidPassword,
|
||||
isValidHostnameInput,
|
||||
extractHostname,
|
||||
isLocalHost
|
||||
isLocalHost,
|
||||
isIPAddress
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
const {
|
||||
INSTALLATION_STEPS,
|
||||
clampStep,
|
||||
isUpgradeMode
|
||||
isUpgradeMode,
|
||||
getEnabledDatabases
|
||||
} = Context;
|
||||
|
||||
const {
|
||||
@@ -79,6 +80,19 @@
|
||||
}
|
||||
};
|
||||
|
||||
const applyEnabledDatabases = (root) => {
|
||||
const enabled = getEnabledDatabases?.() || [];
|
||||
const radios = root.querySelectorAll('input[name="database"]');
|
||||
radios.forEach((radio) => {
|
||||
if (!enabled.includes(radio.value)) {
|
||||
const card = radio.closest('.selector-card');
|
||||
if (card) {
|
||||
card.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const bindDatabaseSelection = (root) => {
|
||||
const radios = root.querySelectorAll('input[name="database"]');
|
||||
radios.forEach((radio) => {
|
||||
@@ -139,6 +153,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
applyEnabledDatabases(root);
|
||||
|
||||
const lockedDatabase = getLockedDatabase?.() || '';
|
||||
if (lockedDatabase) {
|
||||
lockDatabaseSelection(root, lockedDatabase);
|
||||
@@ -374,10 +390,7 @@
|
||||
if (!parsePort(httpPort, 'HTTP')) valid = false;
|
||||
if (!parsePort(httpsPort, 'HTTPS')) valid = false;
|
||||
|
||||
if (!sslEmail || !sslEmail.value.trim()) {
|
||||
setFieldError?.(sslEmail, 'Please enter an email address for SSL certificates');
|
||||
valid = false;
|
||||
} else if (!isValidEmail?.(sslEmail.value.trim())) {
|
||||
if (sslEmail && sslEmail.value.trim() && !isValidEmail?.(sslEmail.value.trim())) {
|
||||
setFieldError?.(sslEmail, 'Please enter a valid email address');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,12 @@ $defaultHttpsPort = $defaultHttpsPort ?? '443';
|
||||
$defaultEmailCertificates = $defaultEmailCertificates ?? '';
|
||||
$defaultAssistantOpenAIKey = $defaultAssistantOpenAIKey ?? '';
|
||||
$defaultDatabase = $defaultDatabase ?? 'mongodb';
|
||||
$enabledDatabases = $enabledDatabases ?? ['mongodb', 'mariadb', 'postgresql'];
|
||||
$selectedDatabase = $lockedDatabase ?: $defaultDatabase;
|
||||
$isDatabaseLocked = !empty($lockedDatabase);
|
||||
$mongoEnabled = in_array('mongodb', $enabledDatabases, true);
|
||||
$mariaEnabled = in_array('mariadb', $enabledDatabases, true);
|
||||
$postgresEnabled = in_array('postgresql', $enabledDatabases, true);
|
||||
$mongoDisabled = $isDatabaseLocked && $selectedDatabase !== 'mongodb';
|
||||
$mariaDisabled = $isDatabaseLocked && $selectedDatabase !== 'mariadb';
|
||||
$postgresDisabled = $isDatabaseLocked && $selectedDatabase !== 'postgresql';
|
||||
@@ -43,6 +47,7 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
|
||||
<div class="input-group stack-xs">
|
||||
<label class="label-text typography-text-m-500 text-neutral-secondary">Database</label>
|
||||
<div class="selector-group<?php echo $isDatabaseLocked ? ' is-locked' : ''; ?>">
|
||||
<?php if ($mongoEnabled) { ?>
|
||||
<label class="selector-card <?php echo ($selectedDatabase === 'mongodb') ? 'selected' : ''; ?><?php echo $mongoDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $mongoDisabled ? ' aria-disabled="true"' : ''; ?>>
|
||||
<input type="radio" name="database" value="mongodb" <?php echo ($selectedDatabase === 'mongodb') ? 'checked' : ''; ?> class="sr-only" <?php echo $mongoDisabled ? 'disabled' : ''; ?>>
|
||||
<div class="selector-content">
|
||||
@@ -54,7 +59,9 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
|
||||
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
|
||||
<?php } ?>
|
||||
</label>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($mariaEnabled) { ?>
|
||||
<label class="selector-card <?php echo ($selectedDatabase === 'mariadb') ? 'selected' : ''; ?><?php echo $mariaDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $mariaDisabled ? ' aria-disabled="true"' : ''; ?>>
|
||||
<input type="radio" name="database" value="mariadb" <?php echo ($selectedDatabase === 'mariadb') ? 'checked' : ''; ?> class="sr-only" <?php echo $mariaDisabled ? 'disabled' : ''; ?>>
|
||||
<div class="selector-content">
|
||||
@@ -66,7 +73,9 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
|
||||
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
|
||||
<?php } ?>
|
||||
</label>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($postgresEnabled) { ?>
|
||||
<label class="selector-card <?php echo ($selectedDatabase === 'postgresql') ? 'selected' : ''; ?><?php echo $postgresDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $postgresDisabled ? ' aria-disabled="true"' : ''; ?>>
|
||||
<input type="radio" name="database" value="postgresql" <?php echo ($selectedDatabase === 'postgresql') ? 'checked' : ''; ?> class="sr-only" <?php echo $postgresDisabled ? 'disabled' : ''; ?>>
|
||||
<div class="selector-content">
|
||||
@@ -78,6 +87,7 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
|
||||
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
|
||||
<?php } ?>
|
||||
</label>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ $isUpgrade = $isUpgrade ?? false;
|
||||
</span>
|
||||
<span class="install-text typography-text-m-400 text-neutral-primary" data-install-text></span>
|
||||
</div>
|
||||
<span class="install-counter typography-text-xs-400" data-install-counter></span>
|
||||
<button type="button" class="install-row-toggle" aria-expanded="false" data-install-toggle>
|
||||
<?php include __DIR__ . '/../../icons/chevron-down.svg'; ?>
|
||||
</button>
|
||||
@@ -50,4 +51,13 @@ $isUpgrade = $isUpgrade ?? false;
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="install-global-actions is-hidden" data-install-global-actions>
|
||||
<button type="button" class="button secondary" data-install-start-over>
|
||||
<span class="button-text typography-text-m-500">Start Over</span>
|
||||
</button>
|
||||
<button type="button" class="button secondary" data-install-hard-reset>
|
||||
<span class="button-text typography-text-m-500">Reset Everything</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -220,6 +220,60 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authoriza
|
||||
};
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
|
||||
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
|
||||
$projectDocument ??= $project;
|
||||
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
|
||||
$databaseType = $database->getAttribute('type', '');
|
||||
|
||||
// Backwards‑compatibility: older or seeded legacy databases may not have a DSN stored
|
||||
// in the "database" attribute. In that case, fall back to the project's database DSN.
|
||||
if ($databaseDSN === '') {
|
||||
$databaseDSN = $projectDocument->getAttribute('database', '');
|
||||
}
|
||||
|
||||
try {
|
||||
$databaseDSN = new DSN($databaseDSN);
|
||||
} catch (\InvalidArgumentException) {
|
||||
$databaseDSN = new DSN('mysql://'.$databaseDSN);
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = new DSN($projectDocument->getAttribute('database'));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// Temporary fallback until all projects use shared tables
|
||||
$dsn = new DSN('mysql://' . $projectDocument->getAttribute('database'));
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$pool = $pools->get($databaseDSN->getHost());
|
||||
|
||||
$adapter = new DatabasePool($pool);
|
||||
$database = new Database($adapter, $cache);
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization);
|
||||
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables, true)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int) $projectDocument->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $projectDocument->getSequence());
|
||||
}
|
||||
|
||||
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
|
||||
return $database;
|
||||
};
|
||||
}, ['cache', 'register', 'project', 'authorization']);
|
||||
|
||||
Server::setResource('abuseRetention', function () {
|
||||
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
|
||||
});
|
||||
|
||||
+5
-12
@@ -13,9 +13,9 @@
|
||||
"test": "vendor/bin/phpunit",
|
||||
"lint": "vendor/bin/pint --test --config pint.json",
|
||||
"format": "vendor/bin/pint --config pint.json",
|
||||
"analyze": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
|
||||
"analyze": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
|
||||
"bench": "vendor/bin/phpbench run --report=benchmark",
|
||||
"check": "./vendor/bin/phpstan analyse -c phpstan.neon",
|
||||
"check": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
|
||||
"installer:clean": "php src/Appwrite/Platform/Installer/Server.php --clean",
|
||||
"installer:dev": "docker compose build && composer installer:clean && php src/Appwrite/Platform/Installer/Server.php --docker"
|
||||
},
|
||||
@@ -52,7 +52,6 @@
|
||||
"appwrite/php-runtimes": "0.19.*",
|
||||
"appwrite/php-clamav": "2.0.*",
|
||||
"utopia-php/abuse": "1.2.*",
|
||||
"utopia-php/agents": "1.2.*",
|
||||
"utopia-php/analytics": "0.15.*",
|
||||
"utopia-php/audit": "2.2.*",
|
||||
"utopia-php/auth": "0.5.*",
|
||||
@@ -62,6 +61,7 @@
|
||||
"utopia-php/config": "1.*",
|
||||
"utopia-php/console": "0.1.*",
|
||||
"utopia-php/database": "5.*",
|
||||
"utopia-php/agents": "1.*",
|
||||
"utopia-php/detector": "0.2.*",
|
||||
"utopia-php/domains": "1.*",
|
||||
"utopia-php/emails": "0.6.*",
|
||||
@@ -73,7 +73,7 @@
|
||||
"utopia-php/locale": "0.8.*",
|
||||
"utopia-php/logger": "0.6.*",
|
||||
"utopia-php/messaging": "0.20.*",
|
||||
"utopia-php/migration": "1.7.*",
|
||||
"utopia-php/migration": "1.8.*",
|
||||
"utopia-php/platform": "0.7.*",
|
||||
"utopia-php/pools": "1.*",
|
||||
"utopia-php/span": "1.1.*",
|
||||
@@ -84,7 +84,7 @@
|
||||
"utopia-php/storage": "1.0.*",
|
||||
"utopia-php/system": "0.10.*",
|
||||
"utopia-php/telemetry": "0.2.*",
|
||||
"utopia-php/vcs": "2.*",
|
||||
"utopia-php/vcs": "3.*",
|
||||
"utopia-php/websocket": "1.0.*",
|
||||
"matomo/device-detector": "6.4.*",
|
||||
"dragonmantank/cron-expression": "3.4.*",
|
||||
@@ -97,12 +97,6 @@
|
||||
"enshrined/svg-sanitize": "0.22.*",
|
||||
"utopia-php/di": "0.1.0"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/utopia-php/database"
|
||||
}
|
||||
],
|
||||
"require-dev": {
|
||||
"ext-fileinfo": "*",
|
||||
"appwrite/sdk-generator": "*",
|
||||
@@ -119,7 +113,6 @@
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.3"
|
||||
},
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true,
|
||||
|
||||
Generated
+29
-62
@@ -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": "1404c8821e43b3fe92e06a8ed658ed26",
|
||||
"content-hash": "f9225f2b580de0ccb796b2fb8c881384",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -3850,16 +3850,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "5.3.16",
|
||||
"version": "5.3.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "f121418c4dc7169576735620fd8c17093c3b851d"
|
||||
"reference": "cff2b6ed63d3291b74110d086e16ff089fe05993"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/f121418c4dc7169576735620fd8c17093c3b851d",
|
||||
"reference": "f121418c4dc7169576735620fd8c17093c3b851d",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/cff2b6ed63d3291b74110d086e16ff089fe05993",
|
||||
"reference": "cff2b6ed63d3291b74110d086e16ff089fe05993",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3889,38 +3889,7 @@
|
||||
"Utopia\\Database\\": "src/Database"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\E2E\\": "tests/e2e",
|
||||
"Tests\\Unit\\": "tests/unit"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"docker compose build"
|
||||
],
|
||||
"start": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"docker compose up -d"
|
||||
],
|
||||
"test": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"docker compose exec tests vendor/bin/phpunit --configuration phpunit.xml"
|
||||
],
|
||||
"lint": [
|
||||
"php -d memory_limit=2G ./vendor/bin/pint --test"
|
||||
],
|
||||
"format": [
|
||||
"php -d memory_limit=2G ./vendor/bin/pint"
|
||||
],
|
||||
"check": [
|
||||
"./vendor/bin/phpstan analyse --level 7 src tests --memory-limit 2G"
|
||||
],
|
||||
"coverage": [
|
||||
"./vendor/bin/coverage-check ./tmp/clover.xml 90"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
@@ -3933,10 +3902,10 @@
|
||||
"utopia"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.16",
|
||||
"issues": "https://github.com/utopia-php/database/issues"
|
||||
"issues": "https://github.com/utopia-php/database/issues",
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.17"
|
||||
},
|
||||
"time": "2026-03-19T10:10:02+00:00"
|
||||
"time": "2026-03-20T01:18:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -4549,16 +4518,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/migration.git",
|
||||
"reference": "97583ae502e40621ea91a71de19d053c5ae2e558"
|
||||
"reference": "8633523b3343d492427331b6eec53f020f6ab7a7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/97583ae502e40621ea91a71de19d053c5ae2e558",
|
||||
"reference": "97583ae502e40621ea91a71de19d053c5ae2e558",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/8633523b3343d492427331b6eec53f020f6ab7a7",
|
||||
"reference": "8633523b3343d492427331b6eec53f020f6ab7a7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4598,9 +4567,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/migration/issues",
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.7.0"
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.8.3"
|
||||
},
|
||||
"time": "2026-03-10T06:36:27+00:00"
|
||||
"time": "2026-03-19T09:18:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/mongo",
|
||||
@@ -5247,22 +5216,23 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/vcs",
|
||||
"version": "2.0.2",
|
||||
"version": "3.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/vcs.git",
|
||||
"reference": "5769679308bad498f2777547d48ab332166c4c0b"
|
||||
"reference": "03b76ad5fd01bc50f809915bca6ff0745ea913af"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/5769679308bad498f2777547d48ab332166c4c0b",
|
||||
"reference": "5769679308bad498f2777547d48ab332166c4c0b",
|
||||
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/03b76ad5fd01bc50f809915bca6ff0745ea913af",
|
||||
"reference": "03b76ad5fd01bc50f809915bca6ff0745ea913af",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"adhocore/jwt": "^1.1",
|
||||
"php": ">=8.0",
|
||||
"utopia-php/cache": "1.0.*"
|
||||
"utopia-php/cache": "1.0.*",
|
||||
"utopia-php/fetch": "0.5.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.*.*",
|
||||
@@ -5289,9 +5259,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/vcs/issues",
|
||||
"source": "https://github.com/utopia-php/vcs/tree/2.0.2"
|
||||
"source": "https://github.com/utopia-php/vcs/tree/3.1.0"
|
||||
},
|
||||
"time": "2026-03-13T15:25:16+00:00"
|
||||
"time": "2026-03-24T08:49:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/websocket",
|
||||
@@ -5469,16 +5439,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.11.10",
|
||||
"version": "1.12.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "96e6b79a241fc615627a820107ca64bbd1b550a6"
|
||||
"reference": "a724aa8db52f83ea35854a004837fa5ce990b736"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/96e6b79a241fc615627a820107ca64bbd1b550a6",
|
||||
"reference": "96e6b79a241fc615627a820107ca64bbd1b550a6",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/a724aa8db52f83ea35854a004837fa5ce990b736",
|
||||
"reference": "a724aa8db52f83ea35854a004837fa5ce990b736",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5514,9 +5484,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.11.10"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.12.1"
|
||||
},
|
||||
"time": "2026-03-19T05:27:36+00:00"
|
||||
"time": "2026-03-24T05:18:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
@@ -8486,8 +8456,5 @@
|
||||
"platform-dev": {
|
||||
"ext-fileinfo": "*"
|
||||
},
|
||||
"platform-overrides": {
|
||||
"php": "8.3"
|
||||
},
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
+56
-14
@@ -112,6 +112,8 @@ services:
|
||||
condition: service_healthy
|
||||
coredns:
|
||||
condition: service_started
|
||||
ollama:
|
||||
condition: service_started
|
||||
entrypoint:
|
||||
- php
|
||||
- -e
|
||||
@@ -159,6 +161,12 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER_VECTORSDB
|
||||
- _APP_DB_HOST_VECTORSDB
|
||||
- _APP_DB_PORT_VECTORSDB
|
||||
- _APP_DB_SCHEMA_VECTORSDB
|
||||
- _APP_DB_USER_VECTORSDB
|
||||
- _APP_DB_PASS_VECTORSDB
|
||||
- _APP_SMTP_HOST
|
||||
- _APP_SMTP_PORT
|
||||
- _APP_SMTP_SECURE
|
||||
@@ -246,7 +254,7 @@ services:
|
||||
appwrite-console:
|
||||
<<: *x-logging
|
||||
container_name: appwrite-console
|
||||
image: appwrite/console:7.5.7
|
||||
image: appwrite/console:7.8.26
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
@@ -295,6 +303,7 @@ services:
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- redis
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -311,6 +320,12 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER_VECTORSDB
|
||||
- _APP_DB_HOST_VECTORSDB
|
||||
- _APP_DB_PORT_VECTORSDB
|
||||
- _APP_DB_SCHEMA_VECTORSDB
|
||||
- _APP_DB_USER_VECTORSDB
|
||||
- _APP_DB_PASS_VECTORSDB
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_LOGGING_CONFIG_REALTIME
|
||||
@@ -330,6 +345,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -363,6 +379,7 @@ services:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- request-catcher-sms
|
||||
- request-catcher-webhook
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -393,6 +410,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
volumes:
|
||||
- appwrite-uploads:/storage/uploads:rw
|
||||
- appwrite-cache:/storage/cache:rw
|
||||
@@ -402,6 +420,7 @@ services:
|
||||
- appwrite-certificates:/storage/certificates:rw
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -458,9 +477,11 @@ services:
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -476,6 +497,12 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER_VECTORSDB
|
||||
- _APP_DB_HOST_VECTORSDB
|
||||
- _APP_DB_PORT_VECTORSDB
|
||||
- _APP_DB_SCHEMA_VECTORSDB
|
||||
- _APP_DB_USER_VECTORSDB
|
||||
- _APP_DB_PASS_VECTORSDB
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_WORKERS_NUM
|
||||
- _APP_QUEUE_NAME
|
||||
@@ -497,6 +524,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -629,6 +657,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
volumes:
|
||||
- appwrite-config:/storage/config:rw
|
||||
- appwrite-certificates:/storage/certificates:rw
|
||||
@@ -848,6 +877,7 @@ services:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
- ./tests:/usr/src/code/tests
|
||||
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
environment:
|
||||
@@ -1044,6 +1074,7 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -1077,6 +1108,7 @@ services:
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- redis
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -1107,6 +1139,7 @@ services:
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- redis
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -1137,6 +1170,7 @@ services:
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- redis
|
||||
- ollama
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
@@ -1228,7 +1262,6 @@ services:
|
||||
start_period: 5s
|
||||
|
||||
mariadb:
|
||||
profiles: ["mariadb"]
|
||||
image: mariadb:10.11 # fix issues when upgrading using: mysql_upgrade -u root -p
|
||||
container_name: appwrite-mariadb
|
||||
<<: *x-logging
|
||||
@@ -1252,7 +1285,6 @@ services:
|
||||
retries: 12
|
||||
|
||||
mongodb:
|
||||
profiles: ["mongodb"]
|
||||
image: mongo:8.2.5
|
||||
container_name: appwrite-mongodb
|
||||
<<: *x-logging
|
||||
@@ -1288,32 +1320,41 @@ services:
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
|
||||
|
||||
postgresql:
|
||||
profiles: ["postgresql"]
|
||||
build:
|
||||
context: ./tests/resources/postgresql
|
||||
args:
|
||||
POSTGRES_VERSION: 17
|
||||
image: appwrite/postgres:0.1.0
|
||||
container_name: appwrite-postgresql
|
||||
<<: *x-logging
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-postgresql:/var/lib/postgresql:rw
|
||||
- appwrite-postgresql:/var/lib/postgresql/18/data:rw
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_DB=${_APP_DB_SCHEMA}
|
||||
- POSTGRES_USER=${_APP_DB_USER}
|
||||
- POSTGRES_PASSWORD=${_APP_DB_PASS}
|
||||
command: "postgres"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER}"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER} -d ${_APP_DB_SCHEMA}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
command: "postgres"
|
||||
|
||||
ollama:
|
||||
image: appwrite/ollama:0.1.1
|
||||
container_name: ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MODELS: ${_APP_EMBEDDING_MODELS:-embeddinggemma}
|
||||
OLLAMA_KEEP_ALIVE: 24h
|
||||
volumes:
|
||||
- appwrite-models:/root/.ollama
|
||||
networks:
|
||||
- appwrite
|
||||
|
||||
redis:
|
||||
image: redis:7.4.7-alpine
|
||||
@@ -1436,3 +1477,4 @@ volumes:
|
||||
appwrite-sites:
|
||||
appwrite-builds:
|
||||
appwrite-config:
|
||||
appwrite-models:
|
||||
@@ -0,0 +1 @@
|
||||
Create a new Collection. Before using this route, you should create a new database resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
|
||||
@@ -0,0 +1 @@
|
||||
Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
|
||||
@@ -0,0 +1 @@
|
||||
Create new Documents. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
|
||||
@@ -0,0 +1,2 @@
|
||||
Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.
|
||||
Attributes can be `key`, `fulltext`, and `unique`.
|
||||
@@ -0,0 +1 @@
|
||||
Create a new Database.
|
||||
@@ -0,0 +1 @@
|
||||
Decrement a specific column of a row by a given value.
|
||||
@@ -0,0 +1 @@
|
||||
Delete a collection by its unique ID. Only users with write permissions have access to delete this resource.
|
||||
@@ -0,0 +1 @@
|
||||
Delete a document by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Bulk delete documents using queries, if no queries are passed then all documents are deleted.
|
||||
@@ -0,0 +1 @@
|
||||
Delete an index.
|
||||
@@ -0,0 +1 @@
|
||||
Delete a database by its unique ID. Only API keys with with databases.write scope can delete a database.
|
||||
@@ -0,0 +1 @@
|
||||
Get the collection activity logs list by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get usage metrics and statistics for a collection. Returning the total number of documents. The response includes both current totals and historical data over time. 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, range defaults to 30 days.
|
||||
@@ -0,0 +1 @@
|
||||
Get a collection by its unique ID. This endpoint response returns a JSON object with the collection metadata.
|
||||
@@ -0,0 +1 @@
|
||||
Get usage metrics and statistics for a database. You can view the total number of collections, documents, and storage usage. The response includes both current totals and historical data over time. 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, range defaults to 30 days.
|
||||
@@ -0,0 +1 @@
|
||||
Get the document activity logs list by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get a document by its unique ID. This endpoint response returns a JSON object with the document data.
|
||||
@@ -0,0 +1 @@
|
||||
Get index by ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get the database activity logs list by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get a database by its unique ID. This endpoint response returns a JSON object with the database metadata.
|
||||
@@ -0,0 +1 @@
|
||||
Increment a specific column of a row by a given value.
|
||||
@@ -0,0 +1 @@
|
||||
List attributes in the collection.
|
||||
@@ -0,0 +1 @@
|
||||
Get a list of all collections that belong to the provided databaseId. You can use the search parameter to filter your results.
|
||||
@@ -0,0 +1 @@
|
||||
Get a list of all the user's documents in a given collection. You can use the query params to filter your results.
|
||||
@@ -0,0 +1 @@
|
||||
List indexes in the collection.
|
||||
@@ -0,0 +1 @@
|
||||
List usage metrics and statistics for all databases in the project. You can view the total number of databases, collections, documents, and storage usage. The response includes both current totals and historical data over time. 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, range defaults to 30 days.
|
||||
@@ -0,0 +1 @@
|
||||
Get a list of all databases from the current Appwrite project. You can use the search parameter to filter your results.
|
||||
@@ -0,0 +1 @@
|
||||
Update a collection by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Update a document by its unique ID. Using the patch method you can pass only specific fields that will get updated.
|
||||
@@ -0,0 +1 @@
|
||||
Update all documents that match your queries, if no queries are submitted then all documents are updated. You can pass only specific fields to be updated.
|
||||
@@ -0,0 +1 @@
|
||||
Update a database by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Create or update a Document. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
|
||||
@@ -0,0 +1 @@
|
||||
Create or update Documents. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
|
||||
@@ -0,0 +1 @@
|
||||
Enable or disable whether a user can impersonate other users. When impersonation headers are used, the request runs as the target user for API behavior, while internal audit logs still attribute the action to the original impersonator and store the impersonated target details only in internal audit payload data.
|
||||
Executable → Regular
+1
-1
@@ -15,4 +15,4 @@ adminDb.createUser({
|
||||
roles: [
|
||||
{ role: 'readWrite', db: database }
|
||||
]
|
||||
});
|
||||
});
|
||||
@@ -210,12 +210,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Appwrite/Auth/Validator/PersonalData.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @var above a method has no effect\.$#'
|
||||
identifier: varTag.misplaced
|
||||
count: 1
|
||||
path: src/Appwrite/Databases/TransactionState.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param has invalid value \(DeviceDetector\)\: Unexpected token "\\n ", expected variable at offset 32 on line 2$#'
|
||||
identifier: phpDoc.parseError
|
||||
@@ -924,18 +918,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Tasks/StatsResources.php
|
||||
|
||||
-
|
||||
message: '#^Call to static method error\(\) on an unknown class Utopia\\CLI\\Console\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Tasks/Upgrade.php
|
||||
|
||||
-
|
||||
message: '#^Call to static method log\(\) on an unknown class Utopia\\CLI\\Console\.$#'
|
||||
identifier: class.notFound
|
||||
count: 5
|
||||
path: src/Appwrite/Platform/Tasks/Upgrade.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$getProjectDB$#'
|
||||
identifier: parameter.notFound
|
||||
@@ -1200,18 +1182,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Database/Documents/User.php
|
||||
|
||||
-
|
||||
message: '#^Call to method isValid\(\) on an unknown class Utopia\\Validator\\Email\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Database/Validator/Attributes.php
|
||||
|
||||
-
|
||||
message: '#^Instantiated class Utopia\\Validator\\Email not found\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Database/Validator/Attributes.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe call to private method Appwrite\\Utopia\\Request\\Filters\\V17\:\:appendSymbol\(\) through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateMethod
|
||||
@@ -1241,55 +1211,11 @@ parameters:
|
||||
identifier: return.phpDocType
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Response/Model/User.php
|
||||
|
||||
-
|
||||
message: '#^Attribute class Tests\\E2E\\General\\Retry does not exist\.$#'
|
||||
identifier: attribute.notFound
|
||||
count: 1
|
||||
path: tests/e2e/General/UsageTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Databases\\Legacy\\DatabasesStringTypesTest\:\:\$setupCache through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/Databases/Legacy/DatabasesStringTypesTest.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$library might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: tests/e2e/Services/Databases/LegacyConsoleClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$person might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 4
|
||||
path: tests/e2e/Services/Databases/LegacyConsoleClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$library might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: tests/e2e/Services/Databases/LegacyCustomClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$person might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 4
|
||||
path: tests/e2e/Services/Databases/LegacyCustomClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$library might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: tests/e2e/Services/Databases/LegacyCustomServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$person might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 4
|
||||
path: tests/e2e/Services/Databases/LegacyCustomServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Tests\\E2E\\Services\\Databases\\Permissions\\LegacyPermissionsGuestTest\:\:getIndexUrl\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
@@ -1673,43 +1599,6 @@ parameters:
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: tests/e2e/Services/TablesDB/Permissions/TablesDBPermissionsTeamTest.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$library might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: tests/e2e/Services/TablesDB/TablesDBConsoleClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$person might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 4
|
||||
path: tests/e2e/Services/TablesDB/TablesDBConsoleClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$library might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: tests/e2e/Services/TablesDB/TablesDBCustomClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$person might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 4
|
||||
path: tests/e2e/Services/TablesDB/TablesDBCustomClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$library might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 1
|
||||
path: tests/e2e/Services/TablesDB/TablesDBCustomServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$person might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
count: 4
|
||||
path: tests/e2e/Services/TablesDB/TablesDBCustomServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensConsoleClientTest\:\:\$bucketAndFileData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<directory>./tests/e2e/Services/ProjectWebhooks</directory>
|
||||
<directory>./tests/e2e/Services/Messaging</directory>
|
||||
<directory>./tests/e2e/Services/Migrations</directory>
|
||||
<directory>./tests/e2e/Services/Project</directory>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>
|
||||
|
||||
@@ -21,17 +21,23 @@ class TransactionState
|
||||
{
|
||||
private Database $dbForProject;
|
||||
private Authorization $authorization;
|
||||
/** @var Authorization $authorization */
|
||||
public function __construct(Database $dbForProject, Authorization $authorization)
|
||||
/**
|
||||
* @var callable(Document $database): Database
|
||||
*/
|
||||
private mixed $getDatabasesDB;
|
||||
|
||||
public function __construct(Database $dbForProject, Authorization $authorization, callable $getDatabasesDB)
|
||||
{
|
||||
$this->dbForProject = $dbForProject;
|
||||
$this->authorization = $authorization;
|
||||
$this->getDatabasesDB = $getDatabasesDB;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a document with transaction-aware logic
|
||||
*
|
||||
* @param Document $database Target database document
|
||||
* @param string $collectionId Collection ID
|
||||
* @param string $documentId Document ID
|
||||
* @param string|null $transactionId Optional transaction ID
|
||||
@@ -42,13 +48,15 @@ class TransactionState
|
||||
* @throws Timeout
|
||||
*/
|
||||
public function getDocument(
|
||||
Document $database,
|
||||
string $collectionId,
|
||||
string $documentId,
|
||||
?string $transactionId = null,
|
||||
array $queries = []
|
||||
): Document {
|
||||
$dbForDatabases = ($this->getDatabasesDB)($database);
|
||||
if ($transactionId === null) {
|
||||
return $this->dbForProject->getDocument($collectionId, $documentId, $queries);
|
||||
return $dbForDatabases->getDocument($collectionId, $documentId, $queries);
|
||||
}
|
||||
|
||||
$state = $this->getTransactionState($transactionId);
|
||||
@@ -66,7 +74,7 @@ class TransactionState
|
||||
|
||||
if ($docState['action'] === 'update' || $docState['action'] === 'upsert') {
|
||||
// Merge with committed version
|
||||
$committedDoc = $this->dbForProject->getDocument($collectionId, $documentId, $queries);
|
||||
$committedDoc = $dbForDatabases->getDocument($collectionId, $documentId, $queries);
|
||||
if (!$committedDoc->isEmpty()) {
|
||||
foreach ($docState['document']->getAttributes() as $key => $value) {
|
||||
if ($key !== '$id') {
|
||||
@@ -80,13 +88,13 @@ class TransactionState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->dbForProject->getDocument($collectionId, $documentId, $queries);
|
||||
return $dbForDatabases->getDocument($collectionId, $documentId, $queries);
|
||||
}
|
||||
|
||||
/**
|
||||
* List documents with transaction-aware logic
|
||||
*
|
||||
* @param Document $database Target database document
|
||||
* @param string $collectionId Collection ID
|
||||
* @param string|null $transactionId Optional transaction ID
|
||||
* @param array $queries Optional query filters
|
||||
@@ -96,17 +104,19 @@ class TransactionState
|
||||
* @throws Timeout
|
||||
*/
|
||||
public function listDocuments(
|
||||
Document $database,
|
||||
string $collectionId,
|
||||
?string $transactionId = null,
|
||||
array $queries = []
|
||||
): array {
|
||||
$dbForDatabases = ($this->getDatabasesDB)($database);
|
||||
// If no transaction, use normal database retrieval
|
||||
if ($transactionId === null) {
|
||||
return $this->dbForProject->find($collectionId, $queries);
|
||||
return $dbForDatabases->find($collectionId, $queries);
|
||||
}
|
||||
|
||||
$state = $this->getTransactionState($transactionId);
|
||||
$committedDocs = $this->dbForProject->find($collectionId, $queries);
|
||||
$committedDocs = $dbForDatabases->find($collectionId, $queries);
|
||||
$documentMap = [];
|
||||
|
||||
// Build map of committed documents
|
||||
@@ -147,6 +157,7 @@ class TransactionState
|
||||
/**
|
||||
* Count documents with transaction-aware logic
|
||||
*
|
||||
* @param Document $database Target database document
|
||||
* @param string $collectionId Collection ID
|
||||
* @param string|null $transactionId Optional transaction ID
|
||||
* @param array $queries Optional query filters
|
||||
@@ -156,23 +167,23 @@ class TransactionState
|
||||
* @throws Timeout
|
||||
*/
|
||||
public function countDocuments(
|
||||
Document $database,
|
||||
string $collectionId,
|
||||
?string $transactionId = null,
|
||||
array $queries = []
|
||||
): int {
|
||||
$dbForDatabases = ($this->getDatabasesDB)($database);
|
||||
if ($transactionId === null) {
|
||||
return $this->dbForProject->count($collectionId, $queries, APP_LIMIT_COUNT);
|
||||
return $dbForDatabases->count($collectionId, $queries, APP_LIMIT_COUNT);
|
||||
}
|
||||
|
||||
$state = $this->getTransactionState($transactionId);
|
||||
|
||||
$baseCount = $this->dbForProject->count($collectionId, $queries, APP_LIMIT_COUNT);
|
||||
$baseCount = $dbForDatabases->count($collectionId, $queries, APP_LIMIT_COUNT);
|
||||
|
||||
if (!isset($state[$collectionId])) {
|
||||
return $baseCount;
|
||||
}
|
||||
|
||||
$committedDocs = $this->dbForProject->find($collectionId, $queries);
|
||||
$committedDocs = $dbForDatabases->find($collectionId, $queries);
|
||||
$committedDocIds = [];
|
||||
foreach ($committedDocs as $doc) {
|
||||
$committedDocIds[$doc->getId()] = true;
|
||||
@@ -214,17 +225,19 @@ class TransactionState
|
||||
/**
|
||||
* Check if a document exists with transaction-aware logic
|
||||
*
|
||||
* @param Document $database Target database document
|
||||
* @param string $collectionId Collection ID
|
||||
* @param string $documentId Document ID
|
||||
* @param string|null $transactionId Optional transaction ID
|
||||
* @return bool True if document exists
|
||||
*/
|
||||
public function documentExists(
|
||||
Document $database,
|
||||
string $collectionId,
|
||||
string $documentId,
|
||||
?string $transactionId = null
|
||||
): bool {
|
||||
$doc = $this->getDocument($collectionId, $documentId, $transactionId);
|
||||
$doc = $this->getDocument($database, $collectionId, $documentId, $transactionId);
|
||||
return !$doc->isEmpty();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,16 @@ class Env
|
||||
foreach ($data as &$row) {
|
||||
$row = explode('=', $row, 2);
|
||||
$key = (isset($row[0])) ? trim($row[0]) : null;
|
||||
$value = (isset($row[1])) ? trim($row[1]) : null;
|
||||
$value = (isset($row[1])) ? (function (string $v): string {
|
||||
$v = trim($v);
|
||||
if (
|
||||
(\str_starts_with($v, '"') && \str_ends_with($v, '"')) ||
|
||||
(\str_starts_with($v, "'") && \str_ends_with($v, "'"))
|
||||
) {
|
||||
return \substr($v, 1, -1);
|
||||
}
|
||||
return $v;
|
||||
})(trim($row[1])) : null;
|
||||
|
||||
if ($key) {
|
||||
$this->vars[$key] = $value;
|
||||
|
||||
@@ -519,6 +519,7 @@ class Event
|
||||
* @param string $pattern
|
||||
* @param array $params
|
||||
* @param ?Document $database
|
||||
* @param ?Document $database
|
||||
* @return array
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
@@ -533,7 +534,7 @@ class Event
|
||||
$parsed = self::parseEventPattern($pattern);
|
||||
// to switch the resource types from databases to the required prefix
|
||||
// eg; all databases events get fired with databases. prefix which mainly depicts legacy type
|
||||
// so a projection from databases to the actual prefix
|
||||
// so a projection from databases to the actual prefix(documentsdb, vectorsdb,etc)
|
||||
if ((str_contains($pattern, 'databases.') && $database && $database->getAttribute('type') !== 'legacy')) {
|
||||
$parsed = self::getDatabaseTypeEvents($database, $parsed);
|
||||
}
|
||||
@@ -695,7 +696,6 @@ class Event
|
||||
)
|
||||
) {
|
||||
$pairedEvents = [];
|
||||
|
||||
foreach ($events as $event) {
|
||||
$pairedEvents[] = $event;
|
||||
// tablesdb needs databases event with tables and collections
|
||||
@@ -745,6 +745,13 @@ class Event
|
||||
'attributes' => 'columns',
|
||||
];
|
||||
break;
|
||||
case 'documentsdb':
|
||||
case 'vectorsdb':
|
||||
// sending the type itself(eg: documentsdb, vectorsdb)
|
||||
$eventMap = [
|
||||
'databases' => $database->getAttribute('type')
|
||||
];
|
||||
break;
|
||||
}
|
||||
foreach ($event as $eventKey => $eventValue) {
|
||||
if (isset($eventMap[$eventValue])) {
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Appwrite\Event;
|
||||
|
||||
use Appwrite\Messaging\Adapter;
|
||||
use Appwrite\Messaging\Adapter\Realtime as RealtimeAdapter;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception;
|
||||
|
||||
@@ -96,17 +97,21 @@ class Realtime extends Event
|
||||
: [$target['projectId'] ?? $this->getProject()->getId()];
|
||||
|
||||
foreach ($projectIds as $projectId) {
|
||||
$this->realtime->send(
|
||||
projectId: $projectId,
|
||||
payload: $this->getRealtimePayload(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles'],
|
||||
options: [
|
||||
'permissionsChanged' => $target['permissionsChanged'],
|
||||
'userId' => $this->getParam('userId')
|
||||
]
|
||||
);
|
||||
try {
|
||||
$this->realtime->send(
|
||||
projectId: $projectId,
|
||||
payload: $this->getRealtimePayload(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles'],
|
||||
options: [
|
||||
'permissionsChanged' => $target['permissionsChanged'],
|
||||
'userId' => $this->getParam('userId')
|
||||
]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Console::error('Realtime send failed: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -340,6 +340,7 @@ class Exception extends \Exception
|
||||
public const string MIGRATION_ALREADY_EXISTS = 'migration_already_exists';
|
||||
public const string MIGRATION_IN_PROGRESS = 'migration_in_progress';
|
||||
public const string MIGRATION_PROVIDER_ERROR = 'migration_provider_error';
|
||||
public const string MIGRATION_DATABASE_TYPE_UNSUPPORTED = 'migration_database_type_unsupported';
|
||||
|
||||
/** Realtime */
|
||||
public const string REALTIME_MESSAGE_FORMAT_INVALID = 'realtime_message_format_invalid';
|
||||
|
||||
@@ -274,7 +274,7 @@ class Mapper
|
||||
case \Appwrite\Event\Validator\Event::class:
|
||||
case \Appwrite\Event\Validator\FunctionEvent::class:
|
||||
case \Appwrite\Network\Validator\CNAME::class:
|
||||
case \Appwrite\Network\Validator\Email::class:
|
||||
case \Utopia\Emails\Validator\Email::class:
|
||||
case \Appwrite\Network\Validator\Redirect::class:
|
||||
case \Appwrite\Network\Validator\DNS::class:
|
||||
case \Appwrite\Network\Validator\Origin::class:
|
||||
|
||||
@@ -492,6 +492,8 @@ class Realtime extends MessagingAdapter
|
||||
break;
|
||||
case 'databases':
|
||||
case 'tablesdb':
|
||||
case 'documentsdb':
|
||||
case 'vectorsdb':
|
||||
$resource = $parts[4] ?? '';
|
||||
if (in_array($resource, ['columns', 'attributes', 'indexes'])) {
|
||||
$channels[] = 'console';
|
||||
@@ -511,12 +513,20 @@ class Realtime extends MessagingAdapter
|
||||
$resourceId = $tableId ?: $collectionId;
|
||||
$channels = [];
|
||||
|
||||
// sending legacy + tablesdb events to both legacy and tablesdb
|
||||
$channels = array_values(array_unique(array_merge(
|
||||
self::getDatabaseChannels('legacy', $database->getId(), $resourceId, $payload->getId(), 'databases'),
|
||||
self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId(), 'databases'),
|
||||
self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId())
|
||||
)));
|
||||
switch ($parts[0]) {
|
||||
case 'databases':
|
||||
case 'tablesdb':
|
||||
// sending legacy + tablesdb events to both legacy and tablesdb
|
||||
$channels = array_values(array_unique(array_merge(
|
||||
self::getDatabaseChannels('legacy', $database->getId(), $resourceId, $payload->getId(), 'databases'),
|
||||
self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId(), 'databases'),
|
||||
self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId())
|
||||
)));
|
||||
break;
|
||||
default:
|
||||
// only prefixed events
|
||||
$channels = array_values(self::getDatabaseChannels($parts[0], $database->getId(), $resourceId, $payload->getId()));
|
||||
}
|
||||
|
||||
$roles = $collection->getAttribute('documentSecurity', false)
|
||||
? \array_merge($collection->getRead(), $payload->getRead())
|
||||
@@ -582,6 +592,7 @@ class Realtime extends MessagingAdapter
|
||||
* @param string $resourceId The collection/table ID
|
||||
* @param string $payloadId The document/row ID
|
||||
* @param string $prefixOverride Override the channel prefix when different API types share the same terminology but need different prefixes
|
||||
* (e.g., 'databases' and 'documentsdb' use same terminology but need different prefixes)
|
||||
* @return array Array of channel names
|
||||
*/
|
||||
private static function getDatabaseChannels(
|
||||
@@ -615,6 +626,13 @@ class Realtime extends MessagingAdapter
|
||||
$channels[] = "{$basePrefix}.{$databaseId}.tables.{$resourceId}.rows.{$payloadId}";
|
||||
break;
|
||||
|
||||
case 'documentsdb':
|
||||
case 'vectorsdb':
|
||||
$channels[] = 'documents';
|
||||
$channels[] = "{$basePrefix}.{$databaseId}.collections.{$resourceId}.documents";
|
||||
$channels[] = "{$basePrefix}.{$databaseId}.collections.{$resourceId}.documents.{$payloadId}";
|
||||
break;
|
||||
|
||||
default:
|
||||
$basePrefix = 'databases';
|
||||
$channels[] = 'documents';
|
||||
@@ -623,6 +641,7 @@ class Realtime extends MessagingAdapter
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
return $channels;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ abstract class Migration
|
||||
'1.7.4' => 'V22',
|
||||
'1.8.0' => 'V23',
|
||||
'1.8.1' => 'V23',
|
||||
'1.9.0' => 'V24',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Migration\Version;
|
||||
|
||||
use Appwrite\Migration\Migration;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Conflict;
|
||||
use Utopia\Database\Exception\Structure;
|
||||
use Utopia\Database\Exception\Timeout;
|
||||
|
||||
class V24 extends Migration
|
||||
{
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function execute(): void
|
||||
{
|
||||
/**
|
||||
* Disable SubQueries for Performance.
|
||||
*/
|
||||
$subQueries = [
|
||||
'subQueryAccountKeys',
|
||||
'subQueryAttributes',
|
||||
'subQueryAuthenticators',
|
||||
'subQueryChallenges',
|
||||
'subQueryDevKeys',
|
||||
'subQueryIndexes',
|
||||
'subQueryKeys',
|
||||
'subQueryMemberships',
|
||||
'subQueryOrganizationKeys',
|
||||
'subQueryPlatforms',
|
||||
'subQueryProjectVariables',
|
||||
'subQuerySessions',
|
||||
'subQueryTargets',
|
||||
'subQueryTokens',
|
||||
'subQueryTopicTargets',
|
||||
'subQueryVariables',
|
||||
'subQueryWebhooks',
|
||||
];
|
||||
foreach ($subQueries as $name) {
|
||||
Database::addFilter(
|
||||
$name,
|
||||
fn () => null,
|
||||
fn () => []
|
||||
);
|
||||
}
|
||||
|
||||
Console::info('Migrating collections');
|
||||
$this->migrateCollections();
|
||||
|
||||
if ($this->project->getSequence() != 'console') {
|
||||
Console::info('Migrating Databases');
|
||||
$this->migrateDatabases();
|
||||
}
|
||||
|
||||
Console::info('Migrating Buckets');
|
||||
$this->migrateBuckets();
|
||||
|
||||
Console::info('Migrating documents');
|
||||
$this->forEachDocument($this->migrateDocument(...));
|
||||
|
||||
Console::info('Cleaning up old attributes');
|
||||
$this->cleanupOldAttributes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate Collections.
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception|Throwable
|
||||
*/
|
||||
private function migrateCollections(): void
|
||||
{
|
||||
$projectInternalId = $this->project->getSequence();
|
||||
|
||||
if (empty($projectInternalId)) {
|
||||
throw new Exception('Project ID is null');
|
||||
}
|
||||
|
||||
$collectionType = match ($projectInternalId) {
|
||||
'console' => 'console',
|
||||
default => 'projects',
|
||||
};
|
||||
|
||||
$collections = $this->collections[$collectionType];
|
||||
|
||||
foreach ($collections as $collection) {
|
||||
$id = $collection['$id'];
|
||||
|
||||
if (empty($id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Console::log("Migrating collection \"{$id}\"");
|
||||
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
$this->dbForProject->purgeCachedDocument(Database::METADATA, $id);
|
||||
|
||||
switch ($id) {
|
||||
case 'projects':
|
||||
if ($collectionType === 'console') {
|
||||
$attributes = [
|
||||
'labels',
|
||||
'status',
|
||||
];
|
||||
try {
|
||||
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'keys':
|
||||
if ($collectionType === 'console') {
|
||||
$attributes = [
|
||||
'resourceType',
|
||||
'resourceId',
|
||||
'resourceInternalId',
|
||||
];
|
||||
try {
|
||||
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
try {
|
||||
$this->dbForProject->deleteIndex($id, '_key_project');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to delete index \"_key_project\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
try {
|
||||
$this->createIndexFromCollection($this->dbForProject, $id, '_key_resource');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create index \"_key_resource\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'rules':
|
||||
if ($collectionType === 'console') {
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->dbForProject, $id, 'logs');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create attribute \"logs\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
|
||||
$indexesToDelete = [
|
||||
'_key_type',
|
||||
'_key_trigger',
|
||||
'_key_deploymentResourceType',
|
||||
'_key_owner',
|
||||
'_key_region',
|
||||
'_key_piid_riid_rt',
|
||||
];
|
||||
foreach ($indexesToDelete as $index) {
|
||||
try {
|
||||
$this->dbForProject->deleteIndex($id, $index);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to delete index \"$index\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
$indexesToCreate = [
|
||||
'_key_type',
|
||||
'_key_trigger',
|
||||
'_key_deploymentResourceType',
|
||||
'_key_owner',
|
||||
'_key_piid_diid_drt',
|
||||
'_key_region_status_createdAt',
|
||||
];
|
||||
foreach ($indexesToCreate as $index) {
|
||||
try {
|
||||
$this->createIndexFromCollection($this->dbForProject, $id, $index);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create index \"$index\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'teams':
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->dbForProject, $id, 'labels');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create attribute \"labels\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'databases':
|
||||
if ($collectionType === 'projects') {
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->dbForProject, $id, 'database');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create attribute \"database\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'functions':
|
||||
$attributes = [
|
||||
'deploymentRetention',
|
||||
'startCommand',
|
||||
'buildSpecification',
|
||||
'runtimeSpecification',
|
||||
];
|
||||
try {
|
||||
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'sites':
|
||||
$attributes = [
|
||||
'startCommand',
|
||||
'deploymentRetention',
|
||||
'buildSpecification',
|
||||
'runtimeSpecification',
|
||||
];
|
||||
try {
|
||||
$this->createAttributesFromCollection($this->dbForProject, $id, $attributes);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'deployments':
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->dbForProject, $id, 'startCommand');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create attribute \"startCommand\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'executions':
|
||||
try {
|
||||
$this->dbForProject->deleteIndex($id, '_key_function_internal_id');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to delete index \"_key_function_internal_id\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
$this->createIndexFromCollection($this->dbForProject, $id, '_key_resourceType');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create index \"_key_resourceType\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'buckets':
|
||||
try {
|
||||
$this->dbForProject->deleteIndex($id, '_fulltext_name');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to delete index \"_fulltext_name\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'providers':
|
||||
try {
|
||||
$this->dbForProject->deleteIndex($id, '_key_name');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to delete index \"_key_name\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'topics':
|
||||
try {
|
||||
$this->dbForProject->deleteIndex($id, '_key_name');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to delete index \"_key_name\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate all Database tables
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
private function migrateDatabases(): void
|
||||
{
|
||||
$this->dbForProject->foreach('databases', function (Document $database) {
|
||||
Console::log("Migrating Collections of {$database->getId()} ({$database->getAttribute('name')})");
|
||||
|
||||
$databaseTable = "database_{$database->getSequence()}";
|
||||
$this->dbForProject->purgeCachedCollection($databaseTable);
|
||||
|
||||
$this->dbForProject->foreach($databaseTable, function (Document $collection) use ($databaseTable) {
|
||||
Console::log("Migrating Collection of {$collection->getId()} ({$collection->getAttribute('name')})");
|
||||
|
||||
$collectionTable = "{$databaseTable}_collection_{$collection->getSequence()}";
|
||||
$this->dbForProject->purgeCachedCollection($collectionTable);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate all Bucket tables
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
* @throws \PDOException
|
||||
*/
|
||||
protected function migrateBuckets(): void
|
||||
{
|
||||
$this->dbForProject->foreach('buckets', function (Document $bucket) {
|
||||
Console::log("Migrating Bucket {$bucket->getId()} ({$bucket->getAttribute('name')})");
|
||||
|
||||
$bucketTable = "bucket_{$bucket->getSequence()}";
|
||||
$this->dbForProject->purgeCachedCollection($bucketTable);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix run on each document
|
||||
*
|
||||
* @param Document $document
|
||||
* @return Document
|
||||
* @throws Conflict
|
||||
* @throws Structure
|
||||
* @throws Timeout
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws \Utopia\Database\Exception\Authorization
|
||||
* @throws \Utopia\Database\Exception\Query
|
||||
*/
|
||||
private function migrateDocument(Document $document): Document
|
||||
{
|
||||
switch ($document->getCollection()) {
|
||||
case 'keys':
|
||||
$projectInternalId = $document->getAttribute('projectInternalId');
|
||||
$projectId = $document->getAttribute('projectId');
|
||||
|
||||
if (!empty($projectInternalId) && empty($document->getAttribute('resourceInternalId'))) {
|
||||
$document->setAttribute('resourceType', 'projects');
|
||||
$document->setAttribute('resourceId', $projectId);
|
||||
$document->setAttribute('resourceInternalId', $projectInternalId);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return $document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old attributes after document migration is complete.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function cleanupOldAttributes(): void
|
||||
{
|
||||
$collectionType = match ($this->project->getSequence()) {
|
||||
'console' => 'console',
|
||||
default => 'projects',
|
||||
};
|
||||
|
||||
if ($collectionType === 'console') {
|
||||
$attributesToDelete = [
|
||||
'keys' => ['projectInternalId', 'projectId'],
|
||||
];
|
||||
|
||||
foreach ($attributesToDelete as $collectionId => $attributes) {
|
||||
foreach ($attributes as $attribute) {
|
||||
try {
|
||||
$this->dbForProject->deleteAttribute($collectionId, $attribute);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to delete attribute \"{$attribute}\" from {$collectionId}: {$th->getMessage()}");
|
||||
}
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($collectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ class Platform
|
||||
public const SCHEME_ANDROID = 'appwrite-android';
|
||||
public const SCHEME_WINDOWS = 'appwrite-windows';
|
||||
public const SCHEME_LINUX = 'appwrite-linux';
|
||||
public const SCHEME_TAURI = 'tauri';
|
||||
|
||||
/**
|
||||
* @var array<string, string> Map scheme types to user-friendly platform names.
|
||||
@@ -53,6 +54,7 @@ class Platform
|
||||
self::SCHEME_FIREFOX_EXTENSION => 'Web (Firefox Extension)',
|
||||
self::SCHEME_SAFARI_EXTENSION => 'Web (Safari Extension)',
|
||||
self::SCHEME_EDGE_EXTENSION => 'Web (Edge Extension)',
|
||||
self::SCHEME_TAURI => 'Web (Tauri)',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Network\Validator;
|
||||
|
||||
use Utopia\Validator;
|
||||
|
||||
/**
|
||||
* Email
|
||||
*
|
||||
* Validate that an variable is a valid email address
|
||||
*
|
||||
* @package Utopia\Validator
|
||||
*/
|
||||
class Email extends Validator
|
||||
{
|
||||
protected bool $allowEmpty;
|
||||
|
||||
public function __construct(bool $allowEmpty = false)
|
||||
{
|
||||
$this->allowEmpty = $allowEmpty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Description
|
||||
*
|
||||
* Returns validator description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Value must be a valid email address';
|
||||
}
|
||||
|
||||
/**
|
||||
* Is valid
|
||||
*
|
||||
* Validation will pass when $value is valid email address.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
if ($this->allowEmpty && \strlen($value) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!\filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is array
|
||||
*
|
||||
* Function will return true if object is array.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isArray(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* Returns validator type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return self::TYPE_STRING;
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ class Origin extends Validator
|
||||
Platform::SCHEME_FIREFOX_EXTENSION,
|
||||
Platform::SCHEME_SAFARI_EXTENSION,
|
||||
Platform::SCHEME_EDGE_EXTENSION,
|
||||
Platform::SCHEME_TAURI,
|
||||
];
|
||||
if (in_array($this->scheme, $webPlatforms, true)) {
|
||||
$validator = new Hostname($this->allowedHostnames);
|
||||
|
||||
@@ -9,6 +9,7 @@ use Appwrite\Platform\Modules\Core;
|
||||
use Appwrite\Platform\Modules\Databases;
|
||||
use Appwrite\Platform\Modules\Functions;
|
||||
use Appwrite\Platform\Modules\Health;
|
||||
use Appwrite\Platform\Modules\Project;
|
||||
use Appwrite\Platform\Modules\Projects;
|
||||
use Appwrite\Platform\Modules\Proxy;
|
||||
use Appwrite\Platform\Modules\Sites;
|
||||
@@ -38,5 +39,6 @@ class Appwrite extends Platform
|
||||
$this->addModule(new Storage\Module());
|
||||
$this->addModule(new VCS\Module());
|
||||
$this->addModule(new Webhooks\Module());
|
||||
$this->addModule(new Project\Module());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Installer\Http\Installer\Certificate;
|
||||
|
||||
use Appwrite\Platform\Installer\Validator\AppDomain;
|
||||
use Utopia\Http\Adapter\Swoole\Response;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Validator\Range;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
private const int CONNECTION_TIMEOUT_SECONDS = 5;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'installerCertificateGet';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/install/certificate')
|
||||
->desc('Check if SSL certificate is ready for a domain')
|
||||
->param('domain', '', new AppDomain(), 'Domain to check')
|
||||
->param('port', 443, new Range(1, 65535), 'HTTPS port to check', true)
|
||||
->inject('response')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $domain, int $port, Response $response): void
|
||||
{
|
||||
$domain = trim($domain);
|
||||
if ($domain === '') {
|
||||
$response->json(['ready' => false]);
|
||||
return;
|
||||
}
|
||||
|
||||
$ready = $this->checkHttps($domain, $port);
|
||||
$response->json(['ready' => $ready]);
|
||||
}
|
||||
|
||||
private function checkHttps(string $domain, int $port): bool
|
||||
{
|
||||
$gateway = $this->getDockerGateway();
|
||||
|
||||
$ch = curl_init();
|
||||
$options = [
|
||||
CURLOPT_URL => 'https://' . $domain . ':' . $port . '/',
|
||||
CURLOPT_NOBODY => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CONNECTTIMEOUT => self::CONNECTION_TIMEOUT_SECONDS,
|
||||
CURLOPT_TIMEOUT => self::CONNECTION_TIMEOUT_SECONDS,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
];
|
||||
|
||||
if ($gateway !== '') {
|
||||
$options[CURLOPT_RESOLVE] = [$domain . ':' . $port . ':' . $gateway];
|
||||
}
|
||||
|
||||
curl_setopt_array($ch, $options);
|
||||
curl_exec($ch);
|
||||
$errno = curl_errno($ch);
|
||||
curl_close($ch);
|
||||
|
||||
return $errno === 0;
|
||||
}
|
||||
|
||||
private function getDockerGateway(): string
|
||||
{
|
||||
$route = @file_get_contents('/proc/net/route');
|
||||
if ($route === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
foreach (explode("\n", $route) as $line) {
|
||||
$fields = preg_split('/\s+/', trim($line));
|
||||
if (isset($fields[1]) && $fields[1] === '00000000' && isset($fields[2])) {
|
||||
$hex = $fields[2];
|
||||
if (strlen($hex) !== 8) {
|
||||
continue;
|
||||
}
|
||||
$ip = long2ip((int) hexdec($hex[6] . $hex[7] . $hex[4] . $hex[5] . $hex[2] . $hex[3] . $hex[0] . $hex[1]));
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,17 @@ class Complete extends Action
|
||||
|
||||
@touch(Server::INSTALLER_COMPLETE_FILE);
|
||||
|
||||
$progressData = ($installId !== '') ? $state->readProgressFile($installId) : [];
|
||||
|
||||
if (!$sessionSecret) {
|
||||
$details = $progressData['details'][Server::STEP_ACCOUNT_SETUP] ?? [];
|
||||
if (!empty($details['sessionSecret'])) {
|
||||
$sessionSecret = $details['sessionSecret'];
|
||||
$sessionId = $sessionId ?: ($details['sessionId'] ?? '');
|
||||
$sessionExpire = $sessionExpire ?: ($details['sessionExpire'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
if ($sessionSecret) {
|
||||
$isHttps = $request->getProtocol() === 'https';
|
||||
$sameSite = $isHttps ? Response::COOKIE_SAMESITE_NONE : Response::COOKIE_SAMESITE_LAX;
|
||||
@@ -58,8 +69,11 @@ class Complete extends Action
|
||||
$expires = $timestamp;
|
||||
}
|
||||
}
|
||||
$response->addCookie('a_session_console', $sessionSecret, $expires, '/', '', $isHttps, true, $sameSite);
|
||||
$response->addCookie('a_session_console_legacy', $sessionSecret, $expires, '/', '', $isHttps, true, $sameSite);
|
||||
$appDomain = $progressData['payload']['appDomain'] ?? '';
|
||||
$cookieDomain = $this->buildCookieDomain($appDomain ?: $request->getHostname());
|
||||
|
||||
$response->addCookie('a_session_console', $sessionSecret, $expires, '/', $cookieDomain, $isHttps, true, $sameSite);
|
||||
$response->addCookie('a_session_console_legacy', $sessionSecret, $expires, '/', $cookieDomain, $isHttps, true, $sameSite);
|
||||
if ($sessionId) {
|
||||
$response->addHeader('X-Appwrite-Session', $sessionId);
|
||||
}
|
||||
@@ -69,4 +83,42 @@ class Complete extends Action
|
||||
|
||||
$response->json(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the cookie domain to match Appwrite's convention in general.php.
|
||||
*
|
||||
* For localhost and IP addresses the domain is left empty (host-only cookie).
|
||||
* For real hostnames, the domain is prefixed with a dot so the cookie matches
|
||||
* Appwrite's default `'.' . $request->getHostname()` behaviour and lives in
|
||||
* the same cookie-jar slot — preventing stale ghost cookies after logout.
|
||||
*/
|
||||
private function buildCookieDomain(string $raw): string
|
||||
{
|
||||
$hostname = $this->extractHostname($raw);
|
||||
if ($hostname === '' || $hostname === 'localhost' || $hostname === '0.0.0.0' || $hostname === 'traefik') {
|
||||
return '';
|
||||
}
|
||||
if (filter_var($hostname, FILTER_VALIDATE_IP) !== false) {
|
||||
return '';
|
||||
}
|
||||
return '.' . $hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the bare hostname from an appDomain value, stripping any port
|
||||
* suffix or IPv6 bracket notation.
|
||||
*/
|
||||
private function extractHostname(string $domain): string
|
||||
{
|
||||
$domain = trim($domain);
|
||||
if ($domain === '') {
|
||||
return '';
|
||||
}
|
||||
if (str_starts_with($domain, '[')) {
|
||||
$end = strpos($domain, ']');
|
||||
return $end !== false ? substr($domain, 1, $end - 1) : '';
|
||||
}
|
||||
$parts = explode(':', $domain);
|
||||
return count($parts) <= 2 ? strtolower($parts[0]) : strtolower($domain);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class Install extends Action
|
||||
->param('appDomain', '', new AppDomain(), 'Application domain (hostname, IP, or bracket IPv6 with optional port)')
|
||||
->param('httpPort', 80, new Range(1, 65535), 'HTTP port')
|
||||
->param('httpsPort', 443, new Range(1, 65535), 'HTTPS port')
|
||||
->param('emailCertificates', '', new Email(), 'Email for SSL certificates')
|
||||
->param('emailCertificates', '', new Email(allowEmpty: true), 'Email for SSL certificates', true)
|
||||
->param('opensslKey', '', new Text(64, 0), 'Secret API key', true)
|
||||
->param('assistantOpenAIKey', '', new Text(256, 0), 'OpenAI API key for assistant', true)
|
||||
->param('accountEmail', '', new Email(allowEmpty: true), 'Account email address', true)
|
||||
@@ -90,6 +90,9 @@ class Install extends Action
|
||||
|
||||
$appDomain = trim($appDomain);
|
||||
$emailCertificates = trim($emailCertificates);
|
||||
if ($emailCertificates === '') {
|
||||
$emailCertificates = trim($accountEmail);
|
||||
}
|
||||
$opensslKey = trim($opensslKey);
|
||||
$assistantOpenAIKey = trim($assistantOpenAIKey);
|
||||
|
||||
@@ -127,6 +130,10 @@ class Install extends Action
|
||||
$this->sendBadRequest($response, $swooleResponse, $wantsStream, 'Please select a supported database');
|
||||
return;
|
||||
}
|
||||
if (!$config->isDatabaseEnabled($database)) {
|
||||
$this->sendBadRequest($response, $swooleResponse, $wantsStream, 'The selected database is not available');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$installId = $state->sanitizeInstallId($installId);
|
||||
@@ -136,6 +143,8 @@ class Install extends Action
|
||||
|
||||
@unlink(Server::INSTALLER_COMPLETE_FILE);
|
||||
|
||||
$state->clearStaleLockIfNeeded();
|
||||
|
||||
try {
|
||||
$lockResult = $state->reserveGlobalLock($installId);
|
||||
} catch (\Throwable $e) {
|
||||
@@ -171,15 +180,23 @@ class Install extends Action
|
||||
if (file_exists($existingPath)) {
|
||||
$existing = $state->readProgressFile($installId);
|
||||
if (!empty($existing['steps']) && $retryStep === null) {
|
||||
$state->updateGlobalLock($installId, Server::STATUS_ERROR);
|
||||
if ($wantsStream) {
|
||||
$this->writeSseEvent($swooleResponse, Server::STATUS_ERROR, ['message' => 'Installation already started']);
|
||||
$swooleResponse->end();
|
||||
$previousHadError = isset($existing['error']);
|
||||
$allCompleted = !$previousHadError && $this->allStepsCompleted($existing['steps']);
|
||||
|
||||
if ($previousHadError || $allCompleted) {
|
||||
@unlink($existingPath);
|
||||
$existing = null;
|
||||
} else {
|
||||
$response->setStatusCode(Response::STATUS_CODE_CONFLICT);
|
||||
$response->json(['success' => false, 'message' => 'Installation already started']);
|
||||
$state->updateGlobalLock($installId, Server::STATUS_ERROR);
|
||||
if ($wantsStream) {
|
||||
$this->writeSseEvent($swooleResponse, Server::STATUS_ERROR, ['message' => 'Installation already started']);
|
||||
$swooleResponse->end();
|
||||
} else {
|
||||
$response->setStatusCode(Response::STATUS_CODE_CONFLICT);
|
||||
$response->json(['success' => false, 'message' => 'Installation already started']);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +220,8 @@ class Install extends Action
|
||||
'_APP_ASSISTANT_OPENAI_API_KEY' => $assistantOpenAIKey,
|
||||
];
|
||||
|
||||
if ($this->hasPayload($existing)) {
|
||||
$previousHadError = is_array($existing) && isset($existing['error']);
|
||||
if ($this->hasPayload($existing) && !$previousHadError) {
|
||||
$stored = $existing['payload'];
|
||||
$inputValues = [
|
||||
'httpPort' => (string) $httpPort,
|
||||
@@ -364,8 +382,6 @@ class Install extends Action
|
||||
$state->updateGlobalLock($installId, Server::STATUS_ERROR);
|
||||
}
|
||||
|
||||
@unlink(Server::INSTALLER_CONFIG_FILE);
|
||||
|
||||
if ($wantsStream) {
|
||||
$this->writeSseEvent($swooleResponse, Server::STATUS_ERROR, [
|
||||
'message' => $e->getMessage(),
|
||||
@@ -388,6 +404,16 @@ class Install extends Action
|
||||
return is_array($data) && isset($data['payload']) && is_array($data['payload']);
|
||||
}
|
||||
|
||||
private function allStepsCompleted(array $steps): bool
|
||||
{
|
||||
foreach ($steps as $step) {
|
||||
if (($step['status'] ?? '') !== Server::STATUS_COMPLETED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function deriveNameFromEmail(string $email): string
|
||||
{
|
||||
$parts = explode('@', $email);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user