mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
302
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
937d94972f | ||
|
|
aefa51f261 | ||
|
|
f9ecd28dd0 | ||
|
|
7e6db9a99b | ||
|
|
c618372d37 | ||
|
|
975a38bf43 | ||
|
|
6b21968413 | ||
|
|
286e0e5802 | ||
|
|
ac05c9e9ce | ||
|
|
153e660274 | ||
|
|
ad97feaf6e | ||
|
|
ef618b831d | ||
|
|
021c5fcad6 | ||
|
|
4309bd075e | ||
|
|
236dceae2c | ||
|
|
884b8f10bb | ||
|
|
530d681c48 | ||
|
|
0295e27b5d | ||
|
|
a804cba999 | ||
|
|
d8f733787f | ||
|
|
1abbca9318 | ||
|
|
e99f682cd6 | ||
|
|
26326d05e9 | ||
|
|
e67ed2660a | ||
|
|
8d0a4d7f92 | ||
|
|
aecca2f503 | ||
|
|
7e4ec9eae0 | ||
|
|
edd948557e | ||
|
|
09317f290a | ||
|
|
e5f0c2df12 | ||
|
|
2afec80a25 | ||
|
|
20c65bac37 | ||
|
|
2c6f37ecad | ||
|
|
f09b9a994e | ||
|
|
9f4ba3a4a2 | ||
|
|
8cb36835cc | ||
|
|
89419344c2 | ||
|
|
7dfe44cb36 | ||
|
|
4cbe50193a | ||
|
|
16929bc420 | ||
|
|
fd28ad8a66 | ||
|
|
38798c8993 | ||
|
|
33ce469ab0 | ||
|
|
0bbcc3f570 | ||
|
|
ecca0d8036 | ||
|
|
534dc55f17 | ||
|
|
d30df31b82 | ||
|
|
dc9a1c03d1 | ||
|
|
3613a645d0 | ||
|
|
6aac7ea6ad | ||
|
|
d22642590f | ||
|
|
0b416e4475 | ||
|
|
4e1b710503 | ||
|
|
e557a5fc6e | ||
|
|
7d15d7e1f5 | ||
|
|
ab1d88a44a | ||
|
|
af54ff83dc | ||
|
|
4b1c6fdf83 | ||
|
|
e42656d51a | ||
|
|
a56c2c11d3 | ||
|
|
0ebda89adf | ||
|
|
4bf3c72196 | ||
|
|
c23945ebc6 | ||
|
|
0984daf37e | ||
|
|
1113bffb78 | ||
|
|
33cafdfc0e | ||
|
|
1be142e4be | ||
|
|
f4b8992cde | ||
|
|
ee2e41fa45 | ||
|
|
94c6f6e07d | ||
|
|
f23411f549 | ||
|
|
299778cd50 | ||
|
|
b677586925 | ||
|
|
7f4cba276e | ||
|
|
eccc39a466 | ||
|
|
a10595f501 | ||
|
|
e42812283b | ||
|
|
4d7aa6d8ab | ||
|
|
95b3db0228 | ||
|
|
a0167d6c6c | ||
|
|
39f3bc7b9d | ||
|
|
ab0fa70bca | ||
|
|
74ab41f2b5 | ||
|
|
d8954f424c | ||
|
|
38493094db | ||
|
|
58e5e69fec | ||
|
|
895ec7fd20 | ||
|
|
d3442d86c1 | ||
|
|
c20dfc7063 | ||
|
|
591cc70e76 | ||
|
|
2de28cfcb7 | ||
|
|
c6d476b978 | ||
|
|
5dca3ad875 | ||
|
|
b6793dc0b5 | ||
|
|
a2ad25a00a | ||
|
|
16ad05792d | ||
|
|
e5f0bc2df6 | ||
|
|
0d3d14d9e1 | ||
|
|
f2826189c6 | ||
|
|
8b026d3459 | ||
|
|
0ea196d21c | ||
|
|
ece5b81732 | ||
|
|
2d92e56795 | ||
|
|
2306072dda | ||
|
|
91edf82060 | ||
|
|
3fc50e4d8d | ||
|
|
71dff441ed | ||
|
|
65780d75f9 | ||
|
|
12ae61b915 | ||
|
|
03d7bccde2 | ||
|
|
b5c2cc9716 | ||
|
|
ee107d30b3 | ||
|
|
499ca71c28 | ||
|
|
cfe60dd2a2 | ||
|
|
ab85942276 | ||
|
|
0214f22d20 | ||
|
|
e0269e268f | ||
|
|
a4844599c6 | ||
|
|
ce876adb3a | ||
|
|
59edcf1934 | ||
|
|
5821832da6 | ||
|
|
d4df2c51de | ||
|
|
dbbc57bc37 | ||
|
|
d772220414 | ||
|
|
9d2036024a | ||
|
|
cf9637288c | ||
|
|
9fdd7c1c6e | ||
|
|
5510231b8e | ||
|
|
152de6c584 | ||
|
|
d1ffa5daf3 | ||
|
|
161c5af66f | ||
|
|
c497a68613 | ||
|
|
50f9c67862 | ||
|
|
198f9a64a3 | ||
|
|
248b3c8b32 | ||
|
|
4574385b31 | ||
|
|
f8b31e7db7 | ||
|
|
cf16e3e2a3 | ||
|
|
ff0f132984 | ||
|
|
300aaeb251 | ||
|
|
f989ccde57 | ||
|
|
63756ddf1d | ||
|
|
1f5fd919c9 | ||
|
|
718d3377b0 | ||
|
|
c81029d8aa | ||
|
|
0291f8f943 | ||
|
|
da30142b4d | ||
|
|
f5047afec9 | ||
|
|
fc88d4b4ab | ||
|
|
df604cb2ef | ||
|
|
a6d58a847d | ||
|
|
fdd3a58d81 | ||
|
|
cf7710b580 | ||
|
|
ccaaffed48 | ||
|
|
c073743989 | ||
|
|
a0854e0591 | ||
|
|
20f248a6ae | ||
|
|
c171e0c3a2 | ||
|
|
2081c4c42c | ||
|
|
b45ff6b646 | ||
|
|
82db411517 | ||
|
|
5999db295d | ||
|
|
7644e0fe48 | ||
|
|
b6b44efdab | ||
|
|
f4125b8859 | ||
|
|
1ccd61ece5 | ||
|
|
598c71fb11 | ||
|
|
40fc4edb25 | ||
|
|
feedd0eb4a | ||
|
|
f41c19ed3e | ||
|
|
94bd9661b3 | ||
|
|
e31843be4b | ||
|
|
e7424e70aa | ||
|
|
5dadb9a998 | ||
|
|
164c372b21 | ||
|
|
cb260fa3d8 | ||
|
|
05a2f56945 | ||
|
|
82a5d138e0 | ||
|
|
3d4f37c482 | ||
|
|
b03ad6d523 | ||
|
|
a5fa09b4ce | ||
|
|
5c2b1be581 | ||
|
|
b3b2fe3ba2 | ||
|
|
9667ec2434 | ||
|
|
3c4c78d086 | ||
|
|
98aab7b34d | ||
|
|
81e828a200 | ||
|
|
218d25ba1b | ||
|
|
908a74f69d | ||
|
|
8368a28ff5 | ||
|
|
abe21d51cb | ||
|
|
339ecc418d | ||
|
|
5ff7e2af24 | ||
|
|
9aa47c267d | ||
|
|
12dca64b60 | ||
|
|
952f48c72a | ||
|
|
4e80f7eb9a | ||
|
|
9db037722c | ||
|
|
ded05ebea2 | ||
|
|
a71f3555ae | ||
|
|
4500349fda | ||
|
|
611495ec28 | ||
|
|
347436d370 | ||
|
|
dd925e335b | ||
|
|
0e8b5f1d04 | ||
|
|
8dfaf4a9a9 | ||
|
|
d5b2f97252 | ||
|
|
47a81d165e | ||
|
|
d7c8b9d43a | ||
|
|
7562946434 | ||
|
|
4d19a5e855 | ||
|
|
1c85789a6e | ||
|
|
79d219bf50 | ||
|
|
9b2143a2a5 | ||
|
|
b174a50b2a | ||
|
|
3e0ee30428 | ||
|
|
0e7c50ef71 | ||
|
|
b8ee9fbe2c | ||
|
|
d49dea9790 | ||
|
|
8891890601 | ||
|
|
db85004b12 | ||
|
|
b7e4f78c56 | ||
|
|
159da8ba31 | ||
|
|
88ce622e4a | ||
|
|
c041e57b8f | ||
|
|
4a0f00f7db | ||
|
|
de21cab22a | ||
|
|
f8929e1f2e | ||
|
|
754ddbef3c | ||
|
|
ed93b77faa | ||
|
|
5a6cfdbe4c | ||
|
|
6b90d00cc2 | ||
|
|
3ba5e12f89 | ||
|
|
dc441c3973 | ||
|
|
9745df4d37 | ||
|
|
715adf62ca | ||
|
|
90ab70dfc6 | ||
|
|
b0c5185ebd | ||
|
|
d58873ef14 | ||
|
|
105c885c3f | ||
|
|
ad625029e8 | ||
|
|
aeaa184aec | ||
|
|
9dd707cd1b | ||
|
|
10e2edac90 | ||
|
|
340081a67d | ||
|
|
9f0ca2bc53 | ||
|
|
605cf0c985 | ||
|
|
7e1838dda6 | ||
|
|
2016e9bc2f | ||
|
|
b3d1410ca9 | ||
|
|
a2d56a47cd | ||
|
|
316bbfe43d | ||
|
|
3b5351afb6 | ||
|
|
ecc4052bdc | ||
|
|
fc84012977 | ||
|
|
c882afbd2c | ||
|
|
9c3b78e18f | ||
|
|
4365e1fee5 | ||
|
|
d19e7c169e | ||
|
|
226bb6b830 | ||
|
|
da5b68490c | ||
|
|
4dc5df3923 | ||
|
|
bc7bdf040f | ||
|
|
f8968f69f0 | ||
|
|
030e89ac7d | ||
|
|
37330522a4 | ||
|
|
807bb0039a | ||
|
|
c4c260f3cf | ||
|
|
87d6c73b7b | ||
|
|
a71875eedf | ||
|
|
51e03353dd | ||
|
|
175b0d92f2 | ||
|
|
c88a77a31c | ||
|
|
a3a5e05b5c | ||
|
|
bd2db5e249 | ||
|
|
6cbc79026f | ||
|
|
0ec755911e | ||
|
|
be626ad0fc | ||
|
|
305697a5d3 | ||
|
|
d954681132 | ||
|
|
872345366a | ||
|
|
5b1aec0f0b | ||
|
|
98d063132c | ||
|
|
fcb3699e99 | ||
|
|
39658a7ee1 | ||
|
|
914723c53a | ||
|
|
db965f8144 | ||
|
|
d99a52741d | ||
|
|
96f79fac0e | ||
|
|
856c1d685e | ||
|
|
5817aea2ee | ||
|
|
6213df9fb3 | ||
|
|
4bc624ea5c | ||
|
|
7444f5cf60 | ||
|
|
cbb96f8b82 | ||
|
|
63f6840d1b | ||
|
|
62173b8f61 | ||
|
|
7f3c0c9c03 | ||
|
|
f907f76eb5 | ||
|
|
30df7a43d5 | ||
|
|
8fc9fdb7dc | ||
|
|
77905e241f |
@@ -5,8 +5,6 @@ on:
|
||||
types: [opened, edited]
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
pull_request:
|
||||
types: [opened, edited]
|
||||
pull_request_review:
|
||||
types: [submitted, edited]
|
||||
pull_request_review_comment:
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
name: Benchmark
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.ref }}'
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
COMPOSE_FILE: docker-compose.yml
|
||||
IMAGE: appwrite-dev
|
||||
CACHE_KEY: 'appwrite-dev-${{ github.event.pull_request.head.sha }}'
|
||||
'on':
|
||||
- pull_request
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup & Build Appwrite Image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build Appwrite
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: '${{ env.IMAGE }}'
|
||||
load: true
|
||||
cache-from: type=gha
|
||||
cache-to: 'type=gha,mode=max'
|
||||
outputs: 'type=docker,dest=/tmp/${{ env.IMAGE }}.tar'
|
||||
target: development
|
||||
build-args: |
|
||||
DEBUG=false
|
||||
TESTING=true
|
||||
VERSION=dev
|
||||
- name: Cache Docker Image
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: '${{ env.CACHE_KEY }}'
|
||||
path: '/tmp/${{ env.IMAGE }}.tar'
|
||||
benchmarking:
|
||||
name: Benchmark
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: '${{ env.CACHE_KEY }}'
|
||||
path: '/tmp/${{ env.IMAGE }}.tar'
|
||||
fail-on-cache-miss: true
|
||||
- name: Load and Start Appwrite
|
||||
run: |
|
||||
sed -i 's/traefik/localhost/g' .env
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose up -d
|
||||
sleep 10
|
||||
- name: Install Oha
|
||||
run: |
|
||||
echo "deb [signed-by=/usr/share/keyrings/azlux-archive-keyring.gpg] http://packages.azlux.fr/debian/ stable main" | sudo tee /etc/apt/sources.list.d/azlux.list
|
||||
sudo wget -O /usr/share/keyrings/azlux-archive-keyring.gpg https://azlux.fr/repo.gpg
|
||||
sudo apt update
|
||||
sudo apt install oha
|
||||
oha --version
|
||||
- name: Benchmark PR
|
||||
run: 'oha -z 180s http://localhost/v1/health/version --output-format json > benchmark.json'
|
||||
- name: Cleaning
|
||||
run: docker compose down -v
|
||||
- name: Installing latest version
|
||||
run: |
|
||||
rm docker-compose.yml
|
||||
rm .env
|
||||
curl https://appwrite.io/install/compose -o docker-compose.yml
|
||||
curl https://appwrite.io/install/env -o .env
|
||||
sed -i 's/_APP_OPTIONS_ABUSE=enabled/_APP_OPTIONS_ABUSE=disabled/g' .env
|
||||
docker compose up -d
|
||||
sleep 10
|
||||
- name: Benchmark Latest
|
||||
run: oha -z 180s http://localhost/v1/health/version --output-format json > benchmark-latest.json
|
||||
- name: Prepare comment
|
||||
run: |
|
||||
echo '## :sparkles: Benchmark results' > benchmark.txt
|
||||
echo ' ' >> benchmark.txt
|
||||
echo "- Requests per second: $(jq -r '.summary.requestsPerSec|tonumber?|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json)" >> benchmark.txt
|
||||
echo "- Requests with 200 status code: $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json)" >> benchmark.txt
|
||||
echo "- P99 latency: $(jq -r '.latencyPercentiles.p99' benchmark.json )" >> benchmark.txt
|
||||
echo " " >> benchmark.txt
|
||||
echo " " >> benchmark.txt
|
||||
echo "## :zap: Benchmark Comparison" >> benchmark.txt
|
||||
echo " " >> benchmark.txt
|
||||
echo "| Metric | This PR | Latest version | " >> benchmark.txt
|
||||
echo "| --- | --- | --- | " >> benchmark.txt
|
||||
echo "| RPS | $(jq -r '.summary.requestsPerSec|tonumber?|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json) | $(jq -r '.summary.requestsPerSec|tonumber|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark-latest.json) | " >> benchmark.txt
|
||||
echo "| 200 | $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json) | $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark-latest.json) | " >> benchmark.txt
|
||||
echo "| P99 | $(jq -r '.latencyPercentiles.p99' benchmark.json ) | $(jq -r '.latencyPercentiles.p99' benchmark-latest.json ) | " >> benchmark.txt
|
||||
- name: Save results
|
||||
uses: actions/upload-artifact@v6
|
||||
if: '${{ !cancelled() }}'
|
||||
with:
|
||||
name: benchmark.json
|
||||
path: benchmark.json
|
||||
retention-days: 7
|
||||
- name: Find Comment
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: fc
|
||||
with:
|
||||
issue-number: '${{ github.event.pull_request.number }}'
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: Benchmark results
|
||||
- name: Comment on PR
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
comment-id: '${{ steps.fc.outputs.comment-id }}'
|
||||
issue-number: '${{ github.event.pull_request.number }}'
|
||||
body-path: benchmark.txt
|
||||
edit-mode: replace
|
||||
@@ -1,19 +0,0 @@
|
||||
name: Check dependencies
|
||||
|
||||
# Adapted from https://google.github.io/osv-scanner/github-action/#scan-on-pull-request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, 1.*.x]
|
||||
merge_group:
|
||||
branches: [main, 1.*.x]
|
||||
|
||||
permissions:
|
||||
# Require writing security events to upload SARIF file to security tab
|
||||
security-events: write
|
||||
# Only need to read contents
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
scan-pr:
|
||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v1.7.1"
|
||||
@@ -0,0 +1,660 @@
|
||||
name: CI
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPOSE_FILE: docker-compose.yml
|
||||
IMAGE: appwrite-dev
|
||||
CACHE_KEY: appwrite-dev-${{ github.event.pull_request.head.sha }}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
response_format:
|
||||
description: 'Response format version to test (e.g., 1.5.0, 1.4.0)'
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
dependencies:
|
||||
name: Checks / Dependencies
|
||||
if: github.event_name == 'pull_request'
|
||||
permissions:
|
||||
actions: read
|
||||
security-events: write
|
||||
contents: read
|
||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@v2.3.3"
|
||||
|
||||
security:
|
||||
name: Checks / Image
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Build the Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
load: true
|
||||
tags: pr_image:${{ github.sha }}
|
||||
target: production
|
||||
|
||||
- name: Run Trivy vulnerability scanner on image
|
||||
uses: aquasecurity/trivy-action@0.35.0
|
||||
with:
|
||||
image-ref: 'pr_image:${{ github.sha }}'
|
||||
format: 'sarif'
|
||||
output: 'trivy-image-results.sarif'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
|
||||
- name: Run Trivy vulnerability scanner on source code
|
||||
uses: aquasecurity/trivy-action@0.35.0
|
||||
with:
|
||||
scan-type: 'fs'
|
||||
scan-ref: '.'
|
||||
format: 'sarif'
|
||||
output: 'trivy-fs-results.sarif'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
skip-setup-trivy: true
|
||||
|
||||
- name: Upload image scan results
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
if: always() && hashFiles('trivy-image-results.sarif') != ''
|
||||
with:
|
||||
sarif_file: 'trivy-image-results.sarif'
|
||||
category: 'trivy-image'
|
||||
|
||||
- name: Upload source code scan results
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
if: always() && hashFiles('trivy-fs-results.sarif') != ''
|
||||
with:
|
||||
sarif_file: 'trivy-fs-results.sarif'
|
||||
category: 'trivy-source'
|
||||
|
||||
format:
|
||||
name: Checks / Format
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- run: git checkout HEAD^2
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
- name: Validate composer.json and composer.lock
|
||||
run: |
|
||||
docker run --rm -v $PWD:/app composer:2.8 sh -c \
|
||||
"composer validate"
|
||||
|
||||
- name: Run Linter
|
||||
run: |
|
||||
docker run --rm -v $PWD:/app composer:2.8 sh -c \
|
||||
"composer install --profile --ignore-platform-reqs && composer lint"
|
||||
|
||||
analyze:
|
||||
name: Checks / Analyze
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Run PHPStan
|
||||
run: |
|
||||
docker run --rm -v $PWD:/app composer:2.8 sh -c \
|
||||
"composer install --profile --ignore-platform-reqs && composer analyze"
|
||||
|
||||
- name: Run Locale check
|
||||
run: |
|
||||
docker run --rm -v $PWD:/app node:24-alpine sh -c \
|
||||
"cd /app/.github/workflows/static-analysis/locale && node index.js"
|
||||
|
||||
matrix:
|
||||
name: Tests / Matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
databases: ${{ steps.generate.outputs.databases }}
|
||||
modes: ${{ steps.generate.outputs.modes }}
|
||||
steps:
|
||||
- name: Generate matrix
|
||||
id: generate
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
|
||||
const allModes = ['dedicated', 'shared_v1', 'shared_v2'];
|
||||
|
||||
const defaultDatabases = ['MongoDB'];
|
||||
const defaultModes = ['dedicated'];
|
||||
|
||||
const pr = context.payload.pull_request;
|
||||
if (!pr) {
|
||||
core.setOutput('databases', JSON.stringify(allDatabases));
|
||||
core.setOutput('modes', JSON.stringify(allModes));
|
||||
return;
|
||||
}
|
||||
|
||||
const getContent = (ref) => github.rest.repos.getContent({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
path: 'composer.lock',
|
||||
ref,
|
||||
});
|
||||
|
||||
const getDbVersion = (lock) => lock.packages?.find(p => p.name === 'utopia-php/database')?.version;
|
||||
|
||||
const [{ data: base }, { data: head }] = await Promise.all([
|
||||
getContent(pr.base.sha),
|
||||
getContent(pr.head.sha),
|
||||
]);
|
||||
|
||||
const decode = (content) => JSON.parse(Buffer.from(content, 'base64').toString());
|
||||
const databaseChanged = getDbVersion(decode(base.content)) !== getDbVersion(decode(head.content));
|
||||
|
||||
core.setOutput('databases', JSON.stringify(databaseChanged ? allDatabases : defaultDatabases));
|
||||
core.setOutput('modes', JSON.stringify(databaseChanged ? allModes : defaultModes));
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build Appwrite
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: ${{ env.IMAGE }}
|
||||
load: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
outputs: type=docker,dest=/tmp/${{ env.IMAGE }}.tar
|
||||
target: development
|
||||
build-args: |
|
||||
DEBUG=false
|
||||
TESTING=true
|
||||
VERSION=dev
|
||||
|
||||
- name: Cache Docker Image
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
|
||||
unit:
|
||||
name: Tests / Unit
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
- name: Environment Variables
|
||||
run: docker compose exec -T appwrite vars
|
||||
|
||||
- name: Run Unit Tests
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/unit
|
||||
command: >-
|
||||
docker compose exec
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/unit
|
||||
|
||||
e2e_general:
|
||||
name: Tests / E2E / General
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
- name: Wait for Open Runtimes
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do
|
||||
echo "Waiting for Executor to come online"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run General Tests
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/General
|
||||
command: >-
|
||||
docker compose exec -T
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/e2e/General
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Appwrite Logs ==="
|
||||
docker compose logs
|
||||
|
||||
e2e_service:
|
||||
name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, matrix]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
database: ${{ fromJSON(needs.matrix.outputs.databases) }}
|
||||
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
|
||||
service: [
|
||||
Account,
|
||||
Avatars,
|
||||
Console,
|
||||
Databases,
|
||||
TablesDB,
|
||||
Functions,
|
||||
FunctionsSchedule,
|
||||
GraphQL,
|
||||
Health,
|
||||
Locale,
|
||||
Projects,
|
||||
Realtime,
|
||||
Sites,
|
||||
Proxy,
|
||||
Storage,
|
||||
Tokens,
|
||||
Teams,
|
||||
Users,
|
||||
Webhooks,
|
||||
VCS,
|
||||
Messaging,
|
||||
Migrations
|
||||
]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Set database environment
|
||||
run: |
|
||||
if [ "${{ matrix.database }}" = "MariaDB" ]; then
|
||||
echo "COMPOSE_PROFILES=mariadb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_ADAPTER=mariadb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_HOST=mariadb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_PORT=3306" >> $GITHUB_ENV
|
||||
elif [ "${{ matrix.database }}" = "MongoDB" ]; then
|
||||
echo "COMPOSE_PROFILES=mongodb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_ADAPTER=mongodb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_HOST=mongodb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_PORT=27017" >> $GITHUB_ENV
|
||||
elif [ "${{ matrix.database }}" = "PostgreSQL" ]; then
|
||||
echo "COMPOSE_PROFILES=postgresql" >> $GITHUB_ENV
|
||||
echo "_APP_DB_ADAPTER=postgresql" >> $GITHUB_ENV
|
||||
echo "_APP_DB_HOST=postgresql" >> $GITHUB_ENV
|
||||
echo "_APP_DB_PORT=5432" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
_APP_BROWSER_HOST: http://invalid-browser/v1
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
- name: Wait for Open Runtimes
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do
|
||||
echo "Waiting for Executor to come online"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run tests
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
timeout_minutes: 20
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/Services/${{ matrix.service }}
|
||||
command: |
|
||||
SERVICE_PATH="/usr/src/code/tests/e2e/Services/${{ matrix.service }}"
|
||||
|
||||
# Services that rely on sequential test method execution (shared static state)
|
||||
FUNCTIONAL_FLAG="--functional"
|
||||
case "${{ matrix.service }}" in
|
||||
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
|
||||
esac
|
||||
|
||||
docker compose exec -T \
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
|
||||
appwrite vendor/bin/paratest --processes $(nproc) $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Appwrite Logs ==="
|
||||
docker compose logs
|
||||
|
||||
e2e_abuse:
|
||||
name: Tests / E2E / Abuse (${{ matrix.mode }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, matrix]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
_APP_OPTIONS_ABUSE: enabled
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
- name: Run tests
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e
|
||||
command: >-
|
||||
docker compose exec -T
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/e2e --group=abuseEnabled
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Appwrite Logs ==="
|
||||
docker compose logs
|
||||
|
||||
e2e_screenshots:
|
||||
name: Tests / E2E / Screenshots (${{ matrix.mode }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, matrix]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
- name: Wait for Open Runtimes
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do
|
||||
echo "Waiting for Executor to come online"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run tests
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/Services/Sites
|
||||
command: >-
|
||||
docker compose exec -T
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/e2e/Services/Sites --group=screenshots
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Appwrite Logs ==="
|
||||
docker compose logs
|
||||
|
||||
benchmark:
|
||||
name: Benchmark
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
run: |
|
||||
sed -i 's/traefik/localhost/g' .env
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose up -d
|
||||
sleep 10
|
||||
|
||||
- name: Install Oha
|
||||
run: |
|
||||
echo "deb [signed-by=/usr/share/keyrings/azlux-archive-keyring.gpg] http://packages.azlux.fr/debian/ stable main" | sudo tee /etc/apt/sources.list.d/azlux.list
|
||||
sudo wget -O /usr/share/keyrings/azlux-archive-keyring.gpg https://azlux.fr/repo.gpg
|
||||
sudo apt update
|
||||
sudo apt install oha
|
||||
oha --version
|
||||
|
||||
- name: Benchmark PR
|
||||
run: 'oha -z 180s http://localhost/v1/health/version --output-format json > benchmark.json'
|
||||
|
||||
- name: Cleaning
|
||||
run: docker compose down -v
|
||||
|
||||
- name: Installing latest version
|
||||
run: |
|
||||
rm docker-compose.yml
|
||||
rm .env
|
||||
curl https://appwrite.io/install/compose -o docker-compose.yml
|
||||
curl https://appwrite.io/install/env -o .env
|
||||
sed -i 's/_APP_OPTIONS_ABUSE=enabled/_APP_OPTIONS_ABUSE=disabled/g' .env
|
||||
docker compose up -d
|
||||
sleep 10
|
||||
|
||||
- name: Benchmark Latest
|
||||
run: oha -z 180s http://localhost/v1/health/version --output-format json > benchmark-latest.json
|
||||
|
||||
- name: Prepare comment
|
||||
run: |
|
||||
echo '## :sparkles: Benchmark results' > benchmark.txt
|
||||
echo ' ' >> benchmark.txt
|
||||
echo "- Requests per second: $(jq -r '.summary.requestsPerSec|tonumber?|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json)" >> benchmark.txt
|
||||
echo "- Requests with 200 status code: $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json)" >> benchmark.txt
|
||||
echo "- P99 latency: $(jq -r '.latencyPercentiles.p99' benchmark.json )" >> benchmark.txt
|
||||
echo " " >> benchmark.txt
|
||||
echo " " >> benchmark.txt
|
||||
echo "## :zap: Benchmark Comparison" >> benchmark.txt
|
||||
echo " " >> benchmark.txt
|
||||
echo "| Metric | This PR | Latest version | " >> benchmark.txt
|
||||
echo "| --- | --- | --- | " >> benchmark.txt
|
||||
echo "| RPS | $(jq -r '.summary.requestsPerSec|tonumber?|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json) | $(jq -r '.summary.requestsPerSec|tonumber|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark-latest.json) | " >> benchmark.txt
|
||||
echo "| 200 | $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json) | $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark-latest.json) | " >> benchmark.txt
|
||||
echo "| P99 | $(jq -r '.latencyPercentiles.p99' benchmark.json ) | $(jq -r '.latencyPercentiles.p99' benchmark-latest.json ) | " >> benchmark.txt
|
||||
|
||||
- name: Save results
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: benchmark.json
|
||||
path: benchmark.json
|
||||
retention-days: 7
|
||||
|
||||
- name: Find Comment
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: Benchmark results
|
||||
|
||||
- name: Comment on PR
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-path: benchmark.txt
|
||||
edit-mode: replace
|
||||
@@ -1,28 +0,0 @@
|
||||
name: "Linter"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on: [pull_request]
|
||||
jobs:
|
||||
lint:
|
||||
name: Linter
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- run: git checkout HEAD^2
|
||||
|
||||
- name: Validate composer.json and composer.lock
|
||||
run: |
|
||||
docker run --rm -v $PWD:/app composer:2.8 sh -c \
|
||||
"composer validate"
|
||||
- name: Run Linter
|
||||
run: |
|
||||
docker run --rm -v $PWD:/app composer:2.8 sh -c \
|
||||
"composer install --profile --ignore-platform-reqs && composer lint"
|
||||
@@ -1,106 +0,0 @@
|
||||
name: PR Security Scan
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
scan:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Build the Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
load: true
|
||||
tags: pr_image:${{ github.sha }}
|
||||
target: production
|
||||
|
||||
- name: Run Trivy vulnerability scanner on image
|
||||
uses: aquasecurity/trivy-action@0.20.0
|
||||
with:
|
||||
image-ref: 'pr_image:${{ github.sha }}'
|
||||
format: 'json'
|
||||
output: 'trivy-image-results.json'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
|
||||
- name: Run Trivy vulnerability scanner on source code
|
||||
uses: aquasecurity/trivy-action@0.20.0
|
||||
with:
|
||||
scan-type: 'fs'
|
||||
scan-ref: '.'
|
||||
format: 'json'
|
||||
output: 'trivy-fs-results.json'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
|
||||
- name: Process Trivy scan results
|
||||
id: process-results
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
let commentBody = '## Security Scan Results for PR\n\n';
|
||||
|
||||
function processResults(results, title) {
|
||||
let sectionBody = `### ${title}\n\n`;
|
||||
if (results.Results && results.Results.some(result => result.Vulnerabilities && result.Vulnerabilities.length > 0)) {
|
||||
sectionBody += '| Package | Version | Vulnerability | Severity |\n';
|
||||
sectionBody += '|---------|---------|----------------|----------|\n';
|
||||
|
||||
const uniqueVulns = new Set();
|
||||
results.Results.forEach(result => {
|
||||
if (result.Vulnerabilities) {
|
||||
result.Vulnerabilities.forEach(vuln => {
|
||||
const vulnKey = `${vuln.PkgName}-${vuln.InstalledVersion}-${vuln.VulnerabilityID}`;
|
||||
if (!uniqueVulns.has(vulnKey)) {
|
||||
uniqueVulns.add(vulnKey);
|
||||
sectionBody += `| ${vuln.PkgName} | ${vuln.InstalledVersion} | [${vuln.VulnerabilityID}](https://nvd.nist.gov/vuln/detail/${vuln.VulnerabilityID}) | ${vuln.Severity} |\n`;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
sectionBody += '🎉 No vulnerabilities found!\n';
|
||||
}
|
||||
return sectionBody;
|
||||
}
|
||||
|
||||
try {
|
||||
const imageResults = JSON.parse(fs.readFileSync('trivy-image-results.json', 'utf8'));
|
||||
const fsResults = JSON.parse(fs.readFileSync('trivy-fs-results.json', 'utf8'));
|
||||
|
||||
commentBody += processResults(imageResults, "Docker Image Scan Results");
|
||||
commentBody += '\n';
|
||||
commentBody += processResults(fsResults, "Source Code Scan Results");
|
||||
|
||||
} catch (error) {
|
||||
commentBody += `There was an error while running the security scan: ${error.message}\n`;
|
||||
commentBody += 'Please contact the core team for assistance.';
|
||||
}
|
||||
|
||||
core.setOutput('comment-body', commentBody);
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: Security Scan Results for PR
|
||||
|
||||
- name: Create or update comment
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
body: ${{ steps.process-results.outputs.comment-body }}
|
||||
edit-mode: replace
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: "This issue has been labeled as a 'question', indicating that it requires additional information from the requestor. It has been inactive for 7 days. If no further activity occurs, this issue will be closed in 14 days."
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
name: "Static code analysis"
|
||||
|
||||
on: [pull_request]
|
||||
jobs:
|
||||
lint:
|
||||
name: CodeQL
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Run CodeQL
|
||||
run: |
|
||||
docker run --rm -v $PWD:/app composer:2.8 sh -c \
|
||||
"composer install --profile --ignore-platform-reqs && composer check"
|
||||
|
||||
- name: Run Locale check
|
||||
run: |
|
||||
docker run --rm -v $PWD:/app node:24-alpine sh -c \
|
||||
"cd /app/.github/workflows/static-analysis/locale && node index.js"
|
||||
@@ -1,691 +0,0 @@
|
||||
name: "Tests"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPOSE_FILE: docker-compose.yml
|
||||
IMAGE: appwrite-dev
|
||||
CACHE_KEY: appwrite-dev-${{ github.event.pull_request.head.sha }}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
response_format:
|
||||
description: 'Response format version to test (e.g., 1.5.0, 1.4.0)'
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
check_database_changes:
|
||||
name: Check if utopia-php/database changed
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
database_changed: ${{ steps.check.outputs.database_changed }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Fetch base branch
|
||||
run: git fetch origin ${{ github.event.pull_request.base.ref }}
|
||||
|
||||
- name: Check for utopia-php/database changes
|
||||
id: check
|
||||
run: |
|
||||
if git diff origin/${{ github.event.pull_request.base.ref }} HEAD -- composer.lock | grep -q '"name": "utopia-php/database"'; then
|
||||
echo "Database version changed, going to run all mode tests."
|
||||
echo "database_changed=true" >> "$GITHUB_ENV"
|
||||
echo "database_changed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "database_changed=false" >> "$GITHUB_ENV"
|
||||
echo "database_changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
setup:
|
||||
name: Setup & Build Appwrite Image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build Appwrite
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: ${{ env.IMAGE }}
|
||||
load: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
outputs: type=docker,dest=/tmp/${{ env.IMAGE }}.tar
|
||||
target: development
|
||||
build-args: |
|
||||
DEBUG=false
|
||||
TESTING=true
|
||||
VERSION=dev
|
||||
|
||||
- name: Cache Docker Image
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
|
||||
unit_test:
|
||||
name: Unit Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose up -d
|
||||
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
|
||||
echo "Waiting for Appwrite to be ready..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Environment Variables
|
||||
run: docker compose exec -T appwrite vars
|
||||
|
||||
- name: Run Unit Tests
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 300
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/unit
|
||||
command: >-
|
||||
docker compose exec
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/unit
|
||||
|
||||
e2e_general_test:
|
||||
name: E2E General Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose up -d
|
||||
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
|
||||
echo "Waiting for Appwrite to be ready..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Wait for Open Runtimes
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do
|
||||
echo "Waiting for Executor to come online"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run General Tests
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 300
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/General
|
||||
command: >-
|
||||
docker compose exec -T
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/e2e/General --debug
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Appwrite Logs ==="
|
||||
docker compose logs
|
||||
|
||||
e2e_service_test:
|
||||
name: E2E Service Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
db_adapter: [
|
||||
MARIADB,
|
||||
POSTGRESQL,
|
||||
MONGODB
|
||||
]
|
||||
service: [
|
||||
Account,
|
||||
Avatars,
|
||||
Console,
|
||||
Databases,
|
||||
Functions,
|
||||
FunctionsSchedule,
|
||||
GraphQL,
|
||||
Health,
|
||||
Locale,
|
||||
Projects,
|
||||
Realtime,
|
||||
Sites,
|
||||
Proxy,
|
||||
Storage,
|
||||
Tokens,
|
||||
Teams,
|
||||
Users,
|
||||
Webhooks,
|
||||
VCS,
|
||||
Messaging,
|
||||
Migrations
|
||||
]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Set DB Adapter environment
|
||||
id: set-db-env
|
||||
run: |
|
||||
DB_ADAPTER_LOWER=$(echo "${{ matrix.db_adapter }}" | tr 'A-Z' 'a-z')
|
||||
echo "COMPOSE_PROFILES=${DB_ADAPTER_LOWER}" >> $GITHUB_ENV
|
||||
|
||||
if [ "${{ matrix.db_adapter }}" = "MARIADB" ]; then
|
||||
echo "_APP_DB_ADAPTER=mariadb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_HOST=mariadb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_PORT=3306" >> $GITHUB_ENV
|
||||
elif [ "${{ matrix.db_adapter }}" = "MONGODB" ]; then
|
||||
echo "_APP_DB_ADAPTER=mongodb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_HOST=mongodb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_PORT=27017" >> $GITHUB_ENV
|
||||
elif [ "${{ matrix.db_adapter }}" = "POSTGRESQL" ]; then
|
||||
echo "_APP_DB_ADAPTER=postgresql" >> $GITHUB_ENV
|
||||
echo "_APP_DB_HOST=postgresql" >> $GITHUB_ENV
|
||||
echo "_APP_DB_PORT=5432" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 3
|
||||
env:
|
||||
_APP_BROWSER_HOST: http://invalid-browser/v1
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose up -d
|
||||
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
|
||||
echo "Waiting for Appwrite to be ready..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Wait for Open Runtimes
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do
|
||||
echo "Waiting for Executor to come online"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run ${{ matrix.service }} tests with Project table mode
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 300
|
||||
timeout_minutes: 20
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/Services/${{ matrix.service }}
|
||||
command: |
|
||||
echo "Using project tables"
|
||||
SERVICE_PATH="/usr/src/code/tests/e2e/Services/${{ matrix.service }}"
|
||||
|
||||
# Services that rely on sequential test method execution (shared static state)
|
||||
FUNCTIONAL_FLAG="--functional"
|
||||
case "${{ matrix.service }}" in
|
||||
Databases|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
|
||||
esac
|
||||
|
||||
echo "Running with paratest (parallel) for: ${{ matrix.service }} ${FUNCTIONAL_FLAG:+(functional)}"
|
||||
docker compose exec -T \
|
||||
-e _APP_DATABASE_SHARED_TABLES="" \
|
||||
-e _APP_DATABASE_SHARED_TABLES_V1="" \
|
||||
-e _APP_DB_ADAPTER="${{ env._APP_DB_ADAPTER }}" \
|
||||
-e _APP_DB_HOST="${{ env._APP_DB_HOST }}" \
|
||||
-e _APP_DB_PORT="${{ env._APP_DB_PORT }}" \
|
||||
-e _APP_DB_SCHEMA=appwrite \
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
|
||||
appwrite vendor/bin/paratest --processes $(nproc) $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --exclude-group ciIgnore --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Appwrite Logs ==="
|
||||
docker compose logs
|
||||
|
||||
e2e_shared_mode_test:
|
||||
name: E2E Shared Mode Service Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ setup, check_database_changes ]
|
||||
if: needs.check_database_changes.outputs.database_changed == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
service:
|
||||
[
|
||||
Account,
|
||||
Avatars,
|
||||
Console,
|
||||
Databases,
|
||||
Functions,
|
||||
FunctionsSchedule,
|
||||
GraphQL,
|
||||
Health,
|
||||
Locale,
|
||||
Projects,
|
||||
Realtime,
|
||||
Sites,
|
||||
Proxy,
|
||||
Storage,
|
||||
Teams,
|
||||
Users,
|
||||
Webhooks,
|
||||
VCS,
|
||||
Messaging,
|
||||
Migrations,
|
||||
Tokens
|
||||
]
|
||||
tables-mode: [
|
||||
'Shared V1',
|
||||
'Shared V2',
|
||||
]
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose up -d
|
||||
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
|
||||
echo "Waiting for Appwrite to be ready..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Wait for Open Runtimes
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do
|
||||
echo "Waiting for Executor to come online"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run ${{ matrix.service }} tests with ${{ matrix.tables-mode }} table mode
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 300
|
||||
timeout_minutes: 20
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/Services/${{ matrix.service }}
|
||||
command: |
|
||||
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
|
||||
echo "Using shared tables V1"
|
||||
export _APP_DATABASE_SHARED_TABLES=database_db_main
|
||||
export _APP_DATABASE_SHARED_TABLES_V1=database_db_main
|
||||
elif [ "${{ matrix.tables-mode }}" == "Shared V2" ]; then
|
||||
echo "Using shared tables V2"
|
||||
export _APP_DATABASE_SHARED_TABLES=database_db_main
|
||||
export _APP_DATABASE_SHARED_TABLES_V1=
|
||||
fi
|
||||
|
||||
SERVICE_PATH="/usr/src/code/tests/e2e/Services/${{ matrix.service }}"
|
||||
|
||||
# Services that rely on sequential test method execution (shared static state)
|
||||
FUNCTIONAL_FLAG="--functional"
|
||||
case "${{ matrix.service }}" in
|
||||
Databases|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
|
||||
esac
|
||||
|
||||
docker compose exec -T \
|
||||
-e _APP_DATABASE_SHARED_TABLES \
|
||||
-e _APP_DATABASE_SHARED_TABLES_V1 \
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
|
||||
appwrite vendor/bin/paratest --processes $(nproc) $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --exclude-group ciIgnore --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Appwrite Worker Builds Logs ==="
|
||||
docker compose logs appwrite-worker-builds
|
||||
echo "=== OpenRuntimes Executor Logs ==="
|
||||
docker compose logs openruntimes-executor
|
||||
|
||||
e2e_abuse_enabled:
|
||||
name: E2E Service Test (Abuse enabled)
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
sed -i 's/_APP_OPTIONS_ABUSE=disabled/_APP_OPTIONS_ABUSE=enabled/' .env
|
||||
docker compose up -d
|
||||
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
|
||||
echo "Waiting for Appwrite to be ready..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Run Projects tests in dedicated table mode
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 300
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/Services/Projects
|
||||
command: |
|
||||
echo "Using project tables"
|
||||
export _APP_DATABASE_SHARED_TABLES=
|
||||
export _APP_DATABASE_SHARED_TABLES_V1=
|
||||
|
||||
docker compose exec -T \
|
||||
-e _APP_DATABASE_SHARED_TABLES \
|
||||
-e _APP_DATABASE_SHARED_TABLES_V1 \
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
|
||||
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=abuseEnabled
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Appwrite Worker Builds Logs ==="
|
||||
docker compose logs appwrite-worker-builds
|
||||
echo "=== OpenRuntimes Executor Logs ==="
|
||||
docker compose logs openruntimes-executor
|
||||
|
||||
e2e_abuse_enabled_shared_mode:
|
||||
name: E2E Shared Mode Service Test (Abuse enabled)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ setup, check_database_changes ]
|
||||
if: needs.check_database_changes.outputs.database_changed == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
tables-mode: [
|
||||
'Shared V1',
|
||||
'Shared V2',
|
||||
]
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
sed -i 's/_APP_OPTIONS_ABUSE=disabled/_APP_OPTIONS_ABUSE=enabled/' .env
|
||||
docker compose up -d
|
||||
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
|
||||
echo "Waiting for Appwrite to be ready..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Run Projects tests in ${{ matrix.tables-mode }} table mode
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 300
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/Services/Projects
|
||||
command: |
|
||||
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
|
||||
echo "Using shared tables V1"
|
||||
export _APP_DATABASE_SHARED_TABLES=database_db_main
|
||||
export _APP_DATABASE_SHARED_TABLES_V1=database_db_main
|
||||
elif [ "${{ matrix.tables-mode }}" == "Shared V2" ]; then
|
||||
echo "Using shared tables V2"
|
||||
export _APP_DATABASE_SHARED_TABLES=database_db_main
|
||||
export _APP_DATABASE_SHARED_TABLES_V1=
|
||||
fi
|
||||
|
||||
docker compose exec -T \
|
||||
-e _APP_DATABASE_SHARED_TABLES \
|
||||
-e _APP_DATABASE_SHARED_TABLES_V1 \
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
|
||||
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=abuseEnabled
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Appwrite Worker Builds Logs ==="
|
||||
docker compose logs appwrite-worker-builds
|
||||
echo "=== OpenRuntimes Executor Logs ==="
|
||||
docker compose logs openruntimes-executor
|
||||
|
||||
e2e_screenshots:
|
||||
name: E2E Service Test (Site Screenshots)
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
sed -i 's/_APP_OPTIONS_ABUSE=disabled/_APP_OPTIONS_ABUSE=enabled/' .env
|
||||
docker compose up -d
|
||||
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
|
||||
echo "Waiting for Appwrite to be ready..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Wait for Open Runtimes
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do
|
||||
echo "Waiting for Executor to come online"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run Site tests with browser connected in dedicated table mode
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 300
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/Services/Sites
|
||||
command: |
|
||||
echo "Keeping original value of _APP_BROWSER_HOST"
|
||||
echo "Using project tables"
|
||||
export _APP_DATABASE_SHARED_TABLES=
|
||||
export _APP_DATABASE_SHARED_TABLES_V1=
|
||||
|
||||
docker compose exec -T \
|
||||
-e _APP_DATABASE_SHARED_TABLES \
|
||||
-e _APP_DATABASE_SHARED_TABLES_V1 \
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
|
||||
appwrite test /usr/src/code/tests/e2e/Services/Sites --debug --group=screenshots
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Appwrite Worker Builds Logs ==="
|
||||
docker compose logs appwrite-worker-builds
|
||||
echo "=== OpenRuntimes Executor Logs ==="
|
||||
docker compose logs openruntimes-executor
|
||||
|
||||
e2e_screenshots_shared_mode:
|
||||
name: E2E Shared Mode Service Test (Site Screenshots)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ setup, check_database_changes ]
|
||||
if: needs.check_database_changes.outputs.database_changed == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
tables-mode: [
|
||||
'Shared V1',
|
||||
'Shared V2',
|
||||
]
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Load Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: ${{ env.CACHE_KEY }}
|
||||
path: /tmp/${{ env.IMAGE }}.tar
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
sed -i 's/_APP_OPTIONS_ABUSE=disabled/_APP_OPTIONS_ABUSE=enabled/' .env
|
||||
docker compose up -d
|
||||
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
|
||||
echo "Waiting for Appwrite to be ready..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Wait for Open Runtimes
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do
|
||||
echo "Waiting for Executor to come online"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run Site tests with browser connected in ${{ matrix.tables-mode }} table mode
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 300
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/Services/Sites
|
||||
command: |
|
||||
echo "Keeping original value of _APP_BROWSER_HOST"
|
||||
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
|
||||
echo "Using shared tables V1"
|
||||
export _APP_DATABASE_SHARED_TABLES=database_db_main
|
||||
export _APP_DATABASE_SHARED_TABLES_V1=database_db_main
|
||||
elif [ "${{ matrix.tables-mode }}" == "Shared V2" ]; then
|
||||
echo "Using shared tables V2"
|
||||
export _APP_DATABASE_SHARED_TABLES=database_db_main
|
||||
export _APP_DATABASE_SHARED_TABLES_V1=
|
||||
fi
|
||||
|
||||
docker compose exec -T \
|
||||
-e _APP_DATABASE_SHARED_TABLES \
|
||||
-e _APP_DATABASE_SHARED_TABLES_V1 \
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
|
||||
appwrite test /usr/src/code/tests/e2e/Services/Sites --debug --group=screenshots
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Appwrite Worker Builds Logs ==="
|
||||
docker compose logs appwrite-worker-builds
|
||||
echo "=== OpenRuntimes Executor Logs ==="
|
||||
docker compose logs openruntimes-executor
|
||||
@@ -47,6 +47,36 @@ Examples:
|
||||
'resourceType' => 'deployments'
|
||||
```
|
||||
|
||||
## Performance Patterns
|
||||
|
||||
### Document Update Optimization
|
||||
|
||||
When updating documents, always pass only the changed attributes as a sparse `Document` rather than the full document. This is more efficient because `updateDocument()` internally performs `array_merge($old, $new)`.
|
||||
|
||||
**Correct Pattern:**
|
||||
```php
|
||||
// Good: Pass only changed attributes directly
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
]));
|
||||
```
|
||||
|
||||
**Incorrect Pattern:**
|
||||
```php
|
||||
$user->setAttribute('name', $name);
|
||||
$user->setAttribute('email', $email);
|
||||
|
||||
// Bad: Passing full document is inefficient
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
```
|
||||
|
||||
**Exceptions:**
|
||||
- Migration files (need full document updates by design)
|
||||
- Cases already using `array_merge()` with `getArrayCopy()`
|
||||
- Updates where almost all attributes of the document change at once (sparse update provides little benefit compared to passing the full document)
|
||||
- Complex nested relationship logic where full document state is required
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Critical Security Practices
|
||||
|
||||
+36
-23
@@ -111,7 +111,7 @@ $ git push origin [name_of_your_new_branch]
|
||||
|
||||
## Setup From Source
|
||||
|
||||
To set up a working **development environment**, just fork the project git repository and install the backend and frontend dependencies using the proper package manager and create run the docker-compose stack.
|
||||
To set up a working **development environment**, just fork the project git repository and install the backend and frontend dependencies using the proper package manager and run the docker-compose stack.
|
||||
|
||||
> If you just want to install Appwrite for day-to-day use and not as a contributor, you can reference the [installation guide](https://github.com/appwrite/appwrite#installation), the [getting started guide](https://appwrite.io/docs/quick-starts), or the main [README](README.md) file.
|
||||
|
||||
@@ -173,12 +173,12 @@ Learn more at our [Technology Stack](#technology-stack) section.
|
||||
- [MVVM](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) - Appwrite console architecture
|
||||
|
||||
##### Container Namespace Conventions
|
||||
To keep our services easy to understand within Docker we follow a naming convention for all our containers depending on it's intended use.
|
||||
To keep our services easy to understand within Docker we follow a naming convention for all our containers depending on its intended use.
|
||||
|
||||
`appwrite-worker-X` - Workers (`src/Appwrite/Platform/Workers/*`)
|
||||
`appwrite-task-X` - Tasks (`src/Appwrite/Platform/Tasks/*`)
|
||||
|
||||
Other containes should be named the same as their service, for example `redis` should just be called `redis`.
|
||||
Other containers should be named the same as their service, for example `redis` should just be called `redis`.
|
||||
|
||||
##### Security
|
||||
|
||||
@@ -189,7 +189,7 @@ Other containes should be named the same as their service, for example `redis` s
|
||||
|
||||
## Modules
|
||||
|
||||
As Appwrite grows, we noticed approach of having all service endpoints in `app/controllers/api/[service].php` is not maintainable. Not only it creates massive files, it also doesnt contain all product's features such as workers or tasks. While there might still be some occurances of those controller files, we avoid it in all new development, and gradually migrate existing controllers to **HTTP modules**.
|
||||
As Appwrite grows, we noticed approach of having all service endpoints in `app/controllers/api/[service].php` is not maintainable. Not only it creates massive files, it also doesn't contain all product's features such as workers or tasks. While there might still be some occurrences of those controller files, we avoid it in all new development, and gradually migrate existing controllers to **HTTP modules**.
|
||||
|
||||
### HTTP Endpoints
|
||||
|
||||
@@ -204,7 +204,7 @@ Tips and tricks:
|
||||
1. If endpoint doesn't have resource, use service name as resource name too
|
||||
> Example: `Modules/Sites/Http/Sites/Get.php`
|
||||
|
||||
2. If there are multiple resources, use then all in folder structure
|
||||
2. If there are multiple resources, use them all in folder structure
|
||||
> Example: `Modules/Sites/Http/Deployments/Builds/Create.php`
|
||||
|
||||
3. Action can only be `Get`, `Create`, `Update`, `Delete` or `XList`
|
||||
@@ -395,7 +395,7 @@ These are the current metrics we collect usage stats for:
|
||||
|
||||
> Note: The curly brackets in the metric name represents a template and is replaced with a value when the metric is processed.
|
||||
|
||||
Metrics are collected within 3 scopes Daily, monthly, an infinity. Adding new usage metric in order to aggregate usage stats is very simple, but very much dependent on where do you want to collect
|
||||
Metrics are collected within 3 scopes Daily, monthly, and infinity. Adding new usage metric in order to aggregate usage stats is very simple, but very much dependent on where do you want to collect
|
||||
statistics ,via API or via background worker. For both cases you will need to add a `const` variable in `app/init.php` under the usage metrics list using the naming convention `METRIC_<RESOURCE_NAME>` as shown below.
|
||||
|
||||
```php
|
||||
@@ -409,14 +409,16 @@ Next follow the appropriate steps below depending on whether you're adding the m
|
||||
|
||||
**API**
|
||||
|
||||
In file `app/controllers/shared/api.php` On the database listener, add to an existing or create a new switch case. Add a call to the usage worker with your new metric const like so:
|
||||
In file `app/controllers/shared/api.php` On the database listener, add to an existing or create a new switch case. Accumulate metrics in the usage context like so:
|
||||
|
||||
```php
|
||||
case $document->getCollection() === 'teams':
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_TEAMS, $value); // per project
|
||||
$usage->addMetric(METRIC_TEAMS, $value); // per project
|
||||
break;
|
||||
```
|
||||
|
||||
The metrics will be automatically published by the shutdown hook at the end of the request. There is no need to manually trigger or publish.
|
||||
|
||||
There are cases when you need to handle metric that has a parent entity, like buckets.
|
||||
Files are linked to a parent bucket, you should verify you remove the files stats when you delete a bucket.
|
||||
|
||||
@@ -425,14 +427,13 @@ In that case you need also to handle children removal using addReduce() method c
|
||||
```php
|
||||
|
||||
case $document->getCollection() === 'buckets': //buckets
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_BUCKETS, $value); // per project
|
||||
$usage->addMetric(METRIC_BUCKETS, $value); // per project
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addReduce($document);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
```
|
||||
|
||||
In addition, you will also need to add some logic to the `reduce()` method of the Usage worker located in `/src/Appwrite/Platform/Workers/Usage.php`, like so:
|
||||
@@ -460,8 +461,12 @@ case $document->getCollection() === 'buckets':
|
||||
|
||||
**Background worker**
|
||||
|
||||
You need to inject the usage queue in the desired worker on the constructor method
|
||||
You need to inject the usage context and publisher in the desired worker on the constructor method
|
||||
```php
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Message\Usage as UsageMessage;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
@@ -474,24 +479,32 @@ public function __construct()
|
||||
->inject('dbForProject')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('publisherForUsage')
|
||||
->inject('log')
|
||||
->callback(fn (Message $message, Database $dbForProject, Func $queueForFunctions, Event $queueForEvents, StatsUsage $queueForStatsUsage, Log $log) => $this->action($message, $dbForProject, $queueForFunctions, $queueForEvents, $queueForStatsUsage, $log));
|
||||
->callback(fn (Message $message, Database $dbForProject, Func $queueForFunctions, Event $queueForEvents, Context $usage, UsagePublisher $publisherForUsage, Log $log) => $this->action($message, $dbForProject, $queueForFunctions, $queueForEvents, $usage, $publisherForUsage, $log));
|
||||
}
|
||||
```
|
||||
|
||||
and then trigger the queue with the new metric like so:
|
||||
and then accumulate metrics, create a message, and publish like so:
|
||||
|
||||
```php
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addMetric(METRIC_BUILDS, 1)
|
||||
->addMetric(METRIC_BUILDS_STORAGE, $build->getAttribute('size', 0))
|
||||
->addMetric(METRIC_BUILDS_COMPUTE, (int)$build->getAttribute('duration', 0) * 1000)
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getSequence(), METRIC_FUNCTION_ID_BUILDS), 1)
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getSequence(), METRIC_FUNCTION_ID_BUILDS), 1)
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getSequence(), METRIC_FUNCTION_ID_BUILDS_STORAGE), $build->getAttribute('size', 0))
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getSequence(), METRIC_FUNCTION_ID_BUILDS_COMPUTE), (int)$build->getAttribute('duration', 0) * 1000)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getSequence(), METRIC_FUNCTION_ID_BUILDS_COMPUTE), (int)$build->getAttribute('duration', 0) * 1000);
|
||||
|
||||
// Publish the accumulated metrics (workers don't have shutdown hooks)
|
||||
$message = new UsageMessage(
|
||||
project: $project,
|
||||
metrics: $usage->getMetrics(),
|
||||
reduce: $usage->getReduce()
|
||||
);
|
||||
$publisherForUsage->enqueue($message);
|
||||
$usage->reset();
|
||||
```
|
||||
|
||||
|
||||
@@ -661,7 +674,7 @@ docker compose exec redis redis-cli FLUSHALL
|
||||
|
||||
## Using preview domains locally
|
||||
|
||||
Appwrite Functions are automatically given a domain you can visit to execute the function. This domain has format `[SOMETHING].functions.localhost` unless you changed `_APP_DOMAIN_FUNCTIONS` environment variable. This default value works great when running Appwrite locally, but it can be impossible to use preview domains with Cloud woekspaces such as Gitpod or GitHub Codespaces.
|
||||
Appwrite Functions are automatically given a domain you can visit to execute the function. This domain has format `[SOMETHING].functions.localhost` unless you changed `_APP_DOMAIN_FUNCTIONS` environment variable. This default value works great when running Appwrite locally, but it can be impossible to use preview domains with Cloud workspaces such as Gitpod or GitHub Codespaces.
|
||||
|
||||
To use preview domains on Cloud workspaces, you can visit hostname provided by them, and supply function's preview domain as URL parameter:
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ RUN chmod +x /usr/local/bin/doctor && \
|
||||
chmod +x /usr/local/bin/sdks && \
|
||||
chmod +x /usr/local/bin/specs && \
|
||||
chmod +x /usr/local/bin/ssl && \
|
||||
chmod +x /usr/local/bin/task-time-travel && \
|
||||
chmod +x /usr/local/bin/screenshot && \
|
||||
chmod +x /usr/local/bin/test && \
|
||||
chmod +x /usr/local/bin/upgrade && \
|
||||
|
||||
+28
-18
@@ -4,11 +4,13 @@ require_once __DIR__ . '/init.php';
|
||||
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\StatsResources;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Appwrite\Runtimes\Runtimes;
|
||||
use Appwrite\Usage\Context as UsageContext;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Executor\Executor;
|
||||
use Swoole\Runtime;
|
||||
@@ -29,6 +31,7 @@ use Utopia\Platform\Service;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Broker\Pool as BrokerPool;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter\None as NoTelemetry;
|
||||
@@ -47,7 +50,7 @@ $platform = new Appwrite();
|
||||
$args = $platform->getEnv('argv');
|
||||
|
||||
\array_shift($args);
|
||||
if (!isset($args[0])) {
|
||||
if (! isset($args[0])) {
|
||||
Console::error('Missing task name');
|
||||
Console::exit(1);
|
||||
}
|
||||
@@ -85,6 +88,7 @@ $setResource('pools', function (Registry $register) {
|
||||
$setResource('authorization', function () {
|
||||
$authorization = new Authorization();
|
||||
$authorization->disable();
|
||||
|
||||
return $authorization;
|
||||
}, []);
|
||||
|
||||
@@ -113,7 +117,7 @@ $setResource('dbForPlatform', function ($pools, $cache, $authorization) {
|
||||
$collections = Config::getParam('collections', [])['console'];
|
||||
$last = \array_key_last($collections);
|
||||
|
||||
if (!($dbForPlatform->exists($dbForPlatform->getDatabase(), $last))) { /** TODO cache ready variable using registry */
|
||||
if (! ($dbForPlatform->exists($dbForPlatform->getDatabase(), $last))) { /** TODO cache ready variable using registry */
|
||||
throw new Exception('Tables not ready yet.');
|
||||
}
|
||||
|
||||
@@ -122,10 +126,10 @@ $setResource('dbForPlatform', function ($pools, $cache, $authorization) {
|
||||
Console::warning($err->getMessage());
|
||||
sleep($sleep);
|
||||
}
|
||||
} while ($attempts < $maxAttempts && !$ready);
|
||||
} while ($attempts < $maxAttempts && ! $ready);
|
||||
|
||||
if (!$ready) {
|
||||
throw new Exception("Console is not ready yet. Please try again later.");
|
||||
if (! $ready) {
|
||||
throw new Exception('Console is not ready yet. Please try again later.');
|
||||
}
|
||||
|
||||
return $dbForPlatform;
|
||||
@@ -163,7 +167,7 @@ $setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $c
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int)$project->getSequence())
|
||||
->setTenant((int) $project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
@@ -184,7 +188,7 @@ $setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $c
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int)$project->getSequence())
|
||||
->setTenant((int) $project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
@@ -207,8 +211,9 @@ $setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $a
|
||||
$database = null;
|
||||
|
||||
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
|
||||
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant((int)$project->getSequence());
|
||||
if ($database !== null && $project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant((int) $project->getSequence());
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
@@ -224,8 +229,8 @@ $setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $a
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
|
||||
// set tenant
|
||||
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant((int)$project->getSequence());
|
||||
if ($project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant((int) $project->getSequence());
|
||||
}
|
||||
|
||||
return $database;
|
||||
@@ -243,15 +248,16 @@ $setResource('publisherFunctions', function (BrokerPool $publisher) {
|
||||
$setResource('publisherMigrations', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$setResource('publisherStatsUsage', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$setResource('publisherMessaging', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
$setResource('queueForStatsUsage', function (Publisher $publisher) {
|
||||
return new StatsUsage($publisher);
|
||||
}, ['publisher']);
|
||||
$setResource('usage', function () {
|
||||
return new UsageContext();
|
||||
}, []);
|
||||
$setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
$setResource('queueForStatsResources', function (Publisher $publisher) {
|
||||
return new StatsResources($publisher);
|
||||
}, ['publisher']);
|
||||
@@ -318,6 +324,10 @@ $setResource('logError', function (Registry $register) {
|
||||
|
||||
$setResource('executor', fn () => new Executor(), []);
|
||||
|
||||
$setResource('bus', function (Registry $register) use ($cli) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $cli->getResource($name));
|
||||
}, ['register']);
|
||||
|
||||
$setResource('telemetry', fn () => new NoTelemetry(), []);
|
||||
|
||||
$cli
|
||||
|
||||
@@ -777,6 +777,7 @@ return [
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
// At the moment, always empty (no runtime supports it yet)
|
||||
'array' => false,
|
||||
'$id' => ID::custom('startCommand'),
|
||||
'type' => Database::VAR_STRING,
|
||||
@@ -2190,13 +2191,6 @@ return [
|
||||
'lengths' => [],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_function_internal_id'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['resourceInternalId'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_resourceType'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
|
||||
@@ -26,7 +26,7 @@ $console = [
|
||||
'hostname' => 'localhost',
|
||||
], // Current host is added on app init
|
||||
],
|
||||
'region' => 'fra',
|
||||
'region' => System::getEnv('_APP_REGION', 'default'),
|
||||
'legalName' => '',
|
||||
'legalCountry' => '',
|
||||
'legalState' => '',
|
||||
|
||||
+13
-3
@@ -27,7 +27,7 @@ return [
|
||||
Exception::GENERAL_RESOURCE_BLOCKED => [
|
||||
'name' => Exception::GENERAL_RESOURCE_BLOCKED,
|
||||
'description' => 'Access to this resource is blocked.',
|
||||
'code' => 401,
|
||||
'code' => 403,
|
||||
],
|
||||
Exception::GENERAL_UNKNOWN_ORIGIN => [
|
||||
'name' => Exception::GENERAL_UNKNOWN_ORIGIN,
|
||||
@@ -168,8 +168,8 @@ return [
|
||||
],
|
||||
Exception::USER_BLOCKED => [
|
||||
'name' => Exception::USER_BLOCKED,
|
||||
'description' => 'The current user has been blocked. You can unblock the user by making a request to the User API\'s "Update User Status" endpoint or in the Appwrite Console\'s Auth section.',
|
||||
'code' => 401,
|
||||
'description' => 'The current user has been blocked.',
|
||||
'code' => 403,
|
||||
],
|
||||
Exception::USER_INVALID_TOKEN => [
|
||||
'name' => Exception::USER_INVALID_TOKEN,
|
||||
@@ -630,6 +630,11 @@ return [
|
||||
'description' => 'Site with the requested ID could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::SITE_ALREADY_EXISTS => [
|
||||
'name' => Exception::SITE_ALREADY_EXISTS,
|
||||
'description' => 'Site with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::SITE_TEMPLATE_NOT_FOUND => [
|
||||
'name' => Exception::SITE_TEMPLATE_NOT_FOUND,
|
||||
'description' => 'Site Template with the requested ID could not be found.',
|
||||
@@ -1291,6 +1296,11 @@ return [
|
||||
'description' => 'Message with the requested ID could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::MESSAGE_ALREADY_EXISTS => [
|
||||
'name' => Exception::MESSAGE_ALREADY_EXISTS,
|
||||
'description' => 'Message with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::MESSAGE_MISSING_TARGET => [
|
||||
'name' => Exception::MESSAGE_MISSING_TARGET,
|
||||
'description' => 'Message with the requested ID has no recipients (topics or users or targets).',
|
||||
|
||||
+1
-1
@@ -231,7 +231,7 @@ return [
|
||||
'url' => 'https://github.com/appwrite/sdk-for-cli',
|
||||
'package' => 'https://www.npmjs.com/package/appwrite-cli',
|
||||
'enabled' => true,
|
||||
'beta' => true,
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'family' => APP_SDK_PLATFORM_CONSOLE,
|
||||
|
||||
@@ -188,7 +188,7 @@ return [
|
||||
'name' => 'VCS',
|
||||
'subtitle' => 'The VCS service allows you to interact with providers like GitHub, GitLab etc.',
|
||||
'description' => '',
|
||||
'controller' => 'api/vcs.php',
|
||||
'controller' => '', // Uses modules
|
||||
'sdk' => false,
|
||||
'docs' => false,
|
||||
'docsUrl' => '',
|
||||
|
||||
@@ -14,7 +14,6 @@ use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Hooks\Hooks;
|
||||
use Appwrite\Network\Validator\Email as EmailValidator;
|
||||
@@ -28,6 +27,7 @@ use Appwrite\SDK\MethodType;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\URL\URL as URLParser;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Identities;
|
||||
@@ -288,7 +288,10 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
}
|
||||
|
||||
try {
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'emailVerification' => $user->getAttribute('emailVerification'),
|
||||
'phoneVerification' => $user->getAttribute('phoneVerification'),
|
||||
]));
|
||||
} catch (\Throwable $th) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB');
|
||||
}
|
||||
@@ -1032,7 +1035,11 @@ Http::post('/v1/account/sessions/email')
|
||||
->setAttribute('password', $proofForPasswordUpdated->hash($password))
|
||||
->setAttribute('hash', $proofForPasswordUpdated->getHash()->getName())
|
||||
->setAttribute('hashOptions', $proofForPasswordUpdated->getHash()->getOptions());
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'password' => $user->getAttribute('password'),
|
||||
'hash' => $user->getAttribute('hash'),
|
||||
'hashOptions' => $user->getAttribute('hashOptions'),
|
||||
]));
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
@@ -1822,7 +1829,11 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->setAttribute('providerAccessToken', $accessToken)
|
||||
->setAttribute('providerRefreshToken', $refreshToken)
|
||||
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $accessTokenExpiry));
|
||||
$dbForProject->updateDocument('identities', $identity->getId(), $identity);
|
||||
$dbForProject->updateDocument('identities', $identity->getId(), new Document([
|
||||
'providerAccessToken' => $identity->getAttribute('providerAccessToken'),
|
||||
'providerRefreshToken' => $identity->getAttribute('providerRefreshToken'),
|
||||
'providerAccessTokenExpiry' => $identity->getAttribute('providerAccessTokenExpiry'),
|
||||
]));
|
||||
}
|
||||
|
||||
if (empty($user->getAttribute('email'))) {
|
||||
@@ -1960,7 +1971,10 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->setAttribute('sessionId', $session->getId())
|
||||
->setAttribute('sessionInternalId', $session->getSequence());
|
||||
|
||||
$dbForProject->updateDocument('targets', $target->getId(), $target);
|
||||
$dbForProject->updateDocument('targets', $target->getId(), new Document([
|
||||
'sessionId' => $target->getAttribute('sessionId'),
|
||||
'sessionInternalId' => $target->getAttribute('sessionInternalId'),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2787,12 +2801,12 @@ Http::post('/v1/account/tokens/phone')
|
||||
->inject('queueForMessaging')
|
||||
->inject('locale')
|
||||
->inject('timelimit')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('store')
|
||||
->inject('proofForCode')
|
||||
->inject('authorization')
|
||||
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Store $store, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, Context $usage, array $plan, Store $store, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
|
||||
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
|
||||
}
|
||||
@@ -2941,16 +2955,12 @@ Http::post('/v1/account/tokens/phone')
|
||||
$countryCode = $helper->parse($phone)->getCountryCode();
|
||||
|
||||
if (!empty($countryCode)) {
|
||||
$queueForStatsUsage
|
||||
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
|
||||
$usage->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
|
||||
}
|
||||
} catch (NumberParseException $e) {
|
||||
// Ignore invalid phone number for country code stats
|
||||
}
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
$usage->addMetric(METRIC_AUTH_METHOD_PHONE, 1);
|
||||
}
|
||||
|
||||
$token->setAttribute('secret', $secret);
|
||||
@@ -3145,7 +3155,9 @@ Http::patch('/v1/account/name')
|
||||
|
||||
$user->setAttribute('name', $name);
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'name' => $user->getAttribute('name'),
|
||||
]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
@@ -3798,13 +3810,15 @@ Http::put('/v1/account/recovery')
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
|
||||
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile
|
||||
->setAttribute('password', $newPassword)
|
||||
->setAttribute('passwordHistory', $history)
|
||||
->setAttribute('passwordUpdate', DateTime::now())
|
||||
->setAttribute('hash', $proofForPassword->getHash()->getName())
|
||||
->setAttribute('hashOptions', $proofForPassword->getHash()->getOptions())
|
||||
->setAttribute('emailVerification', true));
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(
|
||||
[
|
||||
'password' => $newPassword,
|
||||
'passwordHistory' => $history,
|
||||
'passwordUpdate' => DateTime::now(),
|
||||
'hash' => $proofForPassword->getHash()->getName(),
|
||||
'hashOptions' => $proofForPassword->getHash()->getOptions(),
|
||||
'emailVerification' => true]
|
||||
));
|
||||
|
||||
$user->setAttributes($profile->getArrayCopy());
|
||||
|
||||
@@ -4126,7 +4140,7 @@ Http::put('/v1/account/verifications/email')
|
||||
|
||||
$authorization->addRole(Role::user($profile->getId())->toString());
|
||||
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true));
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(['emailVerification' => true]));
|
||||
|
||||
$user->setAttributes($profile->getArrayCopy());
|
||||
|
||||
@@ -4181,11 +4195,11 @@ Http::post('/v1/account/verifications/phone')
|
||||
->inject('project')
|
||||
->inject('locale')
|
||||
->inject('timelimit')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('proofForCode')
|
||||
->inject('authorization')
|
||||
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, Context $usage, array $plan, ProofsCode $proofForCode, Authorization $authorization) {
|
||||
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
|
||||
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
|
||||
}
|
||||
@@ -4270,16 +4284,12 @@ Http::post('/v1/account/verifications/phone')
|
||||
$countryCode = $helper->parse($phone)->getCountryCode();
|
||||
|
||||
if (!empty($countryCode)) {
|
||||
$queueForStatsUsage
|
||||
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
|
||||
$usage->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
|
||||
}
|
||||
} catch (NumberParseException $e) {
|
||||
// Ignore invalid phone number for country code stats
|
||||
}
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
$usage->addMetric(METRIC_AUTH_METHOD_PHONE, 1);
|
||||
}
|
||||
|
||||
$verification->setAttribute('secret', $secret);
|
||||
@@ -4342,7 +4352,7 @@ Http::put('/v1/account/verifications/phone')
|
||||
|
||||
$authorization->addRole(Role::user($profile->getId())->toString());
|
||||
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true));
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(['phoneVerification' => true]));
|
||||
|
||||
$user->setAttributes($profile->getArrayCopy());
|
||||
|
||||
@@ -4500,7 +4510,11 @@ Http::put('/v1/account/targets/:targetId/push')
|
||||
|
||||
$target->setAttribute('name', "{$device['deviceBrand']} {$device['deviceModel']}");
|
||||
|
||||
$target = $dbForProject->updateDocument('targets', $target->getId(), $target);
|
||||
$target = $dbForProject->updateDocument('targets', $target->getId(), new Document([
|
||||
'identifier' => $target->getAttribute('identifier'),
|
||||
'expired' => $target->getAttribute('expired'),
|
||||
'name' => $target->getAttribute('name'),
|
||||
]));
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
|
||||
@@ -3251,7 +3251,7 @@ Http::post('/v1/messaging/messages/email')
|
||||
}
|
||||
}
|
||||
|
||||
$message = $dbForProject->createDocument('messages', new Document([
|
||||
$message = new Document([
|
||||
'$id' => $messageId,
|
||||
'providerType' => MESSAGE_TYPE_EMAIL,
|
||||
'topics' => $topics,
|
||||
@@ -3267,7 +3267,12 @@ Http::post('/v1/messaging/messages/email')
|
||||
'attachments' => $attachments,
|
||||
],
|
||||
'status' => $status,
|
||||
]));
|
||||
]);
|
||||
try {
|
||||
$message = $dbForProject->createDocument('messages', $message);
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::MESSAGE_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
switch ($status) {
|
||||
case MessageStatus::PROCESSING:
|
||||
@@ -3400,7 +3405,7 @@ Http::post('/v1/messaging/messages/sms')
|
||||
}
|
||||
}
|
||||
|
||||
$message = $dbForProject->createDocument('messages', new Document([
|
||||
$message = new Document([
|
||||
'$id' => $messageId,
|
||||
'providerType' => MESSAGE_TYPE_SMS,
|
||||
'topics' => $topics,
|
||||
@@ -3410,7 +3415,12 @@ Http::post('/v1/messaging/messages/sms')
|
||||
'content' => $content,
|
||||
],
|
||||
'status' => $status,
|
||||
]));
|
||||
]);
|
||||
try {
|
||||
$message = $dbForProject->createDocument('messages', $message);
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::MESSAGE_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
switch ($status) {
|
||||
case MessageStatus::PROCESSING:
|
||||
@@ -3620,7 +3630,7 @@ Http::post('/v1/messaging/messages/push')
|
||||
$pushData['priority'] = $priority;
|
||||
}
|
||||
|
||||
$message = $dbForProject->createDocument('messages', new Document([
|
||||
$message = new Document([
|
||||
'$id' => $messageId,
|
||||
'providerType' => MESSAGE_TYPE_PUSH,
|
||||
'topics' => $topics,
|
||||
@@ -3629,7 +3639,12 @@ Http::post('/v1/messaging/messages/push')
|
||||
'scheduledAt' => $scheduledAt,
|
||||
'data' => $pushData,
|
||||
'status' => $status,
|
||||
]));
|
||||
]);
|
||||
try {
|
||||
$message = $dbForProject->createDocument('messages', $message);
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::MESSAGE_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
switch ($status) {
|
||||
case MessageStatus::PROCESSING:
|
||||
|
||||
@@ -717,22 +717,14 @@ Http::get('/v1/migrations/appwrite/report')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response) {
|
||||
|
||||
$appwrite = new Appwrite($projectID, $endpoint, $key);
|
||||
|
||||
try {
|
||||
$appwrite = new Appwrite($projectID, $endpoint, $key);
|
||||
$report = $appwrite->report($resources);
|
||||
} catch (\Throwable $e) {
|
||||
switch ($e->getCode()) {
|
||||
case 401:
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
|
||||
case 429:
|
||||
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
|
||||
case 500:
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
throw new Exception(
|
||||
Exception::MIGRATION_PROVIDER_ERROR,
|
||||
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
|
||||
);
|
||||
}
|
||||
|
||||
$response
|
||||
@@ -771,21 +763,14 @@ Http::get('/v1/migrations/firebase/report')
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Invalid Service Account JSON');
|
||||
}
|
||||
|
||||
$firebase = new Firebase($serviceAccount);
|
||||
|
||||
try {
|
||||
$firebase = new Firebase($serviceAccount);
|
||||
$report = $firebase->report($resources);
|
||||
} catch (\Throwable $e) {
|
||||
switch ($e->getCode()) {
|
||||
case 401:
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
|
||||
case 429:
|
||||
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
|
||||
case 500:
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
throw new Exception(
|
||||
Exception::MIGRATION_PROVIDER_ERROR,
|
||||
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
|
||||
);
|
||||
}
|
||||
|
||||
$response
|
||||
@@ -820,21 +805,14 @@ Http::get('/v1/migrations/supabase/report')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response) {
|
||||
$supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port);
|
||||
|
||||
try {
|
||||
$supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port);
|
||||
$report = $supabase->report($resources);
|
||||
} catch (\Throwable $e) {
|
||||
switch ($e->getCode()) {
|
||||
case 401:
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
|
||||
case 429:
|
||||
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
|
||||
case 500:
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
throw new Exception(
|
||||
Exception::MIGRATION_PROVIDER_ERROR,
|
||||
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
|
||||
);
|
||||
}
|
||||
|
||||
$response
|
||||
@@ -869,21 +847,14 @@ Http::get('/v1/migrations/nhost/report')
|
||||
->param('port', 5432, new Integer(true), 'Source\'s Database Port.', true)
|
||||
->inject('response')
|
||||
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response) {
|
||||
$nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port);
|
||||
|
||||
try {
|
||||
$nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port);
|
||||
$report = $nhost->report($resources);
|
||||
} catch (\Throwable $e) {
|
||||
switch ($e->getCode()) {
|
||||
case 401:
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
|
||||
case 429:
|
||||
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
|
||||
case 500:
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
|
||||
throw new Exception(
|
||||
Exception::MIGRATION_PROVIDER_ERROR,
|
||||
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
|
||||
);
|
||||
}
|
||||
|
||||
$response
|
||||
|
||||
@@ -73,8 +73,9 @@ use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
/** TODO: Remove function when we move to using utopia/platform */
|
||||
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
|
||||
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, ?string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
|
||||
{
|
||||
$name = $name ?? '';
|
||||
$plaintextPassword = $password;
|
||||
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
|
||||
@@ -255,7 +256,7 @@ Http::post('/v1/users')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$plaintext = new Plaintext();
|
||||
|
||||
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
|
||||
@@ -291,7 +292,7 @@ Http::post('/v1/users/bcrypt')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$bcrypt = new Bcrypt();
|
||||
$bcrypt->setCost(8); // Default cost
|
||||
|
||||
@@ -329,7 +330,7 @@ Http::post('/v1/users/md5')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$md5 = new MD5();
|
||||
|
||||
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
@@ -366,7 +367,7 @@ Http::post('/v1/users/argon2')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$argon2 = new Argon2();
|
||||
|
||||
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
@@ -404,7 +405,7 @@ Http::post('/v1/users/sha')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->action(function (string $userId, string $email, string $password, string $passwordVersion, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$sha = new Sha();
|
||||
if (!empty($passwordVersion)) {
|
||||
$sha->setVersion($passwordVersion);
|
||||
@@ -444,7 +445,7 @@ Http::post('/v1/users/phpass')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$phpass = new PHPass();
|
||||
|
||||
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
@@ -486,7 +487,7 @@ Http::post('/v1/users/scrypt')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$scrypt = new Scrypt();
|
||||
$scrypt
|
||||
->setSalt($passwordSalt)
|
||||
@@ -532,7 +533,7 @@ Http::post('/v1/users/scrypt-modified')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$scryptModified = new ScryptModified();
|
||||
$scryptModified
|
||||
->setSalt($passwordSalt)
|
||||
@@ -1160,7 +1161,7 @@ Http::patch('/v1/users/:userId/status')
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', (bool) $status));
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['status' => (bool) $status]));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId());
|
||||
@@ -1203,7 +1204,7 @@ Http::put('/v1/users/:userId/labels')
|
||||
|
||||
$user->setAttribute('labels', (array) \array_values(\array_unique($labels)));
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['labels' => $user->getAttribute('labels')]));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId());
|
||||
@@ -1244,7 +1245,7 @@ Http::patch('/v1/users/:userId/verification/phone')
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('phoneVerification', $phoneVerification));
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['phoneVerification' => $phoneVerification]));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId());
|
||||
@@ -1288,7 +1289,7 @@ Http::patch('/v1/users/:userId/name')
|
||||
|
||||
$user->setAttribute('name', $name);
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['name' => $user->getAttribute('name')]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
@@ -1343,7 +1344,10 @@ Http::patch('/v1/users/:userId/password')
|
||||
->setAttribute('password', '')
|
||||
->setAttribute('passwordUpdate', DateTime::now());
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'password' => $user->getAttribute('password'),
|
||||
'passwordUpdate' => $user->getAttribute('passwordUpdate'),
|
||||
]));
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
$response->dynamic($user, Response::MODEL_USER);
|
||||
}
|
||||
@@ -1376,7 +1380,13 @@ Http::patch('/v1/users/:userId/password')
|
||||
->setAttribute('hash', $hasher->getName())
|
||||
->setAttribute('hashOptions', $hasher->getOptions());
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'password' => $user->getAttribute('password'),
|
||||
'passwordHistory' => $user->getAttribute('passwordHistory'),
|
||||
'passwordUpdate' => $user->getAttribute('passwordUpdate'),
|
||||
'hash' => $user->getAttribute('hash'),
|
||||
'hashOptions' => $user->getAttribute('hashOptions'),
|
||||
]));
|
||||
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
$invalidate = $project->getAttribute('auths', default: [])['invalidateSessions'] ?? false;
|
||||
@@ -1468,7 +1478,15 @@ Http::patch('/v1/users/:userId/email')
|
||||
;
|
||||
|
||||
try {
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'email' => $user->getAttribute('email'),
|
||||
'emailVerification' => $user->getAttribute('emailVerification'),
|
||||
'emailCanonical' => $user->getAttribute('emailCanonical'),
|
||||
'emailIsCanonical' => $user->getAttribute('emailIsCanonical'),
|
||||
'emailIsCorporate' => $user->getAttribute('emailIsCorporate'),
|
||||
'emailIsDisposable' => $user->getAttribute('emailIsDisposable'),
|
||||
'emailIsFree' => $user->getAttribute('emailIsFree'),
|
||||
]));
|
||||
/**
|
||||
* @var Document $oldTarget
|
||||
*/
|
||||
@@ -1476,7 +1494,8 @@ Http::patch('/v1/users/:userId/email')
|
||||
|
||||
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
|
||||
if (\strlen($email) !== 0) {
|
||||
$dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email));
|
||||
$dbForProject->updateDocument('targets', $oldTarget->getId(), new Document(['identifier' => $email]));
|
||||
$oldTarget->setAttribute('identifier', $email);
|
||||
} else {
|
||||
$dbForProject->deleteDocument('targets', $oldTarget->getId());
|
||||
}
|
||||
@@ -1541,12 +1560,15 @@ Http::patch('/v1/users/:userId/phone')
|
||||
|
||||
$oldPhone = $user->getAttribute('phone');
|
||||
|
||||
// Store null instead of empty string so unique constraint allows multiple users without phone
|
||||
$phoneValue = $number !== '' ? $number : null;
|
||||
|
||||
$user
|
||||
->setAttribute('phone', $number)
|
||||
->setAttribute('phone', $phoneValue)
|
||||
->setAttribute('phoneVerification', false)
|
||||
;
|
||||
|
||||
if (\strlen($number) !== 0) {
|
||||
if ($number !== '') {
|
||||
$target = $dbForProject->findOne('targets', [
|
||||
Query::equal('identifier', [$number]),
|
||||
]);
|
||||
@@ -1557,20 +1579,24 @@ Http::patch('/v1/users/:userId/phone')
|
||||
}
|
||||
|
||||
try {
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'phone' => $phoneValue,
|
||||
'phoneVerification' => $user->getAttribute('phoneVerification'),
|
||||
]));
|
||||
/**
|
||||
* @var Document $oldTarget
|
||||
*/
|
||||
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
|
||||
|
||||
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
|
||||
if (\strlen($number) !== 0) {
|
||||
$dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $number));
|
||||
if ($number !== '') {
|
||||
$dbForProject->updateDocument('targets', $oldTarget->getId(), new Document(['identifier' => $number]));
|
||||
$oldTarget->setAttribute('identifier', $number);
|
||||
} else {
|
||||
$dbForProject->deleteDocument('targets', $oldTarget->getId());
|
||||
}
|
||||
} else {
|
||||
if (\strlen($number) !== 0) {
|
||||
if ($number !== '') {
|
||||
$target = $dbForProject->createDocument('targets', new Document([
|
||||
'$permissions' => [
|
||||
Permission::read(Role::user($user->getId())),
|
||||
@@ -1629,7 +1655,7 @@ Http::patch('/v1/users/:userId/verification')
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification));
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['emailVerification' => $emailVerification]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
@@ -1667,7 +1693,7 @@ Http::patch('/v1/users/:userId/prefs')
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs));
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['prefs' => $prefs]));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId());
|
||||
@@ -1767,7 +1793,13 @@ Http::patch('/v1/users/:userId/targets/:targetId')
|
||||
$target->setAttribute('name', $name);
|
||||
}
|
||||
|
||||
$target = $dbForProject->updateDocument('targets', $target->getId(), $target);
|
||||
$target = $dbForProject->updateDocument('targets', $target->getId(), new Document([
|
||||
'identifier' => $target->getAttribute('identifier'),
|
||||
'expired' => $target->getAttribute('expired'),
|
||||
'providerId' => $target->getAttribute('providerId'),
|
||||
'providerInternalId' => $target->getAttribute('providerInternalId'),
|
||||
'name' => $target->getAttribute('name'),
|
||||
]));
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$queueForEvents
|
||||
@@ -1835,7 +1867,7 @@ Http::patch('/v1/users/:userId/mfa')
|
||||
|
||||
$user->setAttribute('mfa', $mfa);
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['mfa' => $user->getAttribute('mfa')]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
@@ -2023,7 +2055,7 @@ Http::patch('/v1/users/:userId/mfa/recovery-codes')
|
||||
|
||||
$mfaRecoveryCodes = Type::generateBackupCodes();
|
||||
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
@@ -2095,7 +2127,7 @@ Http::put('/v1/users/:userId/mfa/recovery-codes')
|
||||
|
||||
$mfaRecoveryCodes = Type::generateBackupCodes();
|
||||
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
|
||||
@@ -1,705 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Filter\BranchDomain as BranchDomainFilter;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Vcs\Comment;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
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\Http\Http;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\VCS\Adapter\Git\GitHub;
|
||||
use Utopia\VCS\Exception\RepositoryNotFound;
|
||||
|
||||
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForPlatform, Authorization $authorization, Build $queueForBuilds, callable $getProjectDB, Request $request, array $platform) {
|
||||
$errors = [];
|
||||
foreach ($repositories as $repository) {
|
||||
try {
|
||||
$resourceType = $repository->getAttribute('resourceType');
|
||||
|
||||
if ($resourceType !== "function" && $resourceType !== "site") {
|
||||
continue;
|
||||
}
|
||||
|
||||
$projectId = $repository->getAttribute('projectId');
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND, 'Repository references non-existent project');
|
||||
}
|
||||
$dbForProject = $getProjectDB($project);
|
||||
|
||||
$resourceCollection = $resourceType === "function" ? 'functions' : 'sites';
|
||||
$resourceId = $repository->getAttribute('resourceId');
|
||||
$resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
|
||||
$resourceInternalId = $resource->getSequence();
|
||||
|
||||
$deploymentId = ID::unique();
|
||||
$repositoryId = $repository->getId();
|
||||
$repositoryInternalId = $repository->getSequence();
|
||||
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
|
||||
$installationId = $repository->getAttribute('installationId');
|
||||
$installationInternalId = $repository->getAttribute('installationInternalId');
|
||||
$productionBranch = $resource->getAttribute('providerBranch');
|
||||
$activate = false;
|
||||
|
||||
if ($providerBranch == $productionBranch && $external === false) {
|
||||
$activate = true;
|
||||
}
|
||||
|
||||
$owner = $github->getOwnerName($providerInstallationId) ?? '';
|
||||
try {
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
if (empty($repositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
} catch (RepositoryNotFound $e) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (empty($repositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$isAuthorized = !$external;
|
||||
|
||||
if (!$isAuthorized && !empty($providerPullRequestId)) {
|
||||
if (\in_array($providerPullRequestId, $repository->getAttribute('providerPullRequestIds', []))) {
|
||||
$isAuthorized = true;
|
||||
}
|
||||
}
|
||||
|
||||
$commentStatus = $isAuthorized ? 'waiting' : 'failed';
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$hostname = $platform['consoleHostname'] ?? '';
|
||||
|
||||
$authorizeUrl = $protocol . '://' . $hostname . "/console/git/authorize-contributor?projectId={$projectId}&installationId={$installationId}&repositoryId={$repositoryId}&providerPullRequestId={$providerPullRequestId}";
|
||||
|
||||
$action = $isAuthorized ? ['type' => 'logs'] : ['type' => 'authorize', 'url' => $authorizeUrl];
|
||||
|
||||
$latestCommentId = '';
|
||||
|
||||
if (!empty($providerPullRequestId) && $resource->getAttribute('providerSilentMode', false) === false) {
|
||||
$latestComment = $authorization->skip(fn () => $dbForPlatform->findOne('vcsComments', [
|
||||
Query::equal('providerRepositoryId', [$providerRepositoryId]),
|
||||
Query::equal('providerPullRequestId', [$providerPullRequestId]),
|
||||
Query::orderDesc('$createdAt'),
|
||||
]));
|
||||
|
||||
if (!$latestComment->isEmpty()) {
|
||||
$latestCommentId = $latestComment->getAttribute('providerCommentId', '');
|
||||
|
||||
$retries = 0;
|
||||
$lockAcquired = false;
|
||||
|
||||
while ($retries < 9) {
|
||||
$retries++;
|
||||
|
||||
try {
|
||||
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
|
||||
'$id' => $latestCommentId
|
||||
]));
|
||||
$lockAcquired = true;
|
||||
break;
|
||||
} catch (\Throwable $err) {
|
||||
if ($retries >= 9) {
|
||||
Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage());
|
||||
}
|
||||
|
||||
\sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
if ($lockAcquired) {
|
||||
// Wrap in try/finally to ensure lock file gets deleted
|
||||
try {
|
||||
$comment = new Comment($platform);
|
||||
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
|
||||
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
|
||||
|
||||
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
|
||||
} finally {
|
||||
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$comment = new Comment($platform);
|
||||
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
|
||||
$latestCommentId = \strval($github->createComment($owner, $repositoryName, $providerPullRequestId, $comment->generateComment()));
|
||||
|
||||
if (!empty($latestCommentId)) {
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
|
||||
$latestComment = $authorization->skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::team(ID::custom($teamId))),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'developer')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
|
||||
],
|
||||
'installationInternalId' => $installationInternalId,
|
||||
'installationId' => $installationId,
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'projectId' => $project->getId(),
|
||||
'providerRepositoryId' => $providerRepositoryId,
|
||||
'providerBranch' => $providerBranch,
|
||||
'providerPullRequestId' => $providerPullRequestId,
|
||||
'providerCommentId' => $latestCommentId
|
||||
])));
|
||||
}
|
||||
}
|
||||
} elseif (!empty($providerBranch)) {
|
||||
$latestComments = $authorization->skip(fn () => $dbForPlatform->find('vcsComments', [
|
||||
Query::equal('providerRepositoryId', [$providerRepositoryId]),
|
||||
Query::equal('providerBranch', [$providerBranch]),
|
||||
Query::orderDesc('$createdAt'),
|
||||
]));
|
||||
|
||||
foreach ($latestComments as $comment) {
|
||||
$latestCommentId = $comment->getAttribute('providerCommentId', '');
|
||||
|
||||
$retries = 0;
|
||||
$lockAcquired = false;
|
||||
|
||||
while ($retries < 9) {
|
||||
$retries++;
|
||||
|
||||
try {
|
||||
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
|
||||
'$id' => $latestCommentId
|
||||
]));
|
||||
$lockAcquired = true;
|
||||
break;
|
||||
} catch (\Throwable $err) {
|
||||
if ($retries >= 9) {
|
||||
Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage());
|
||||
}
|
||||
|
||||
\sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
if ($lockAcquired) {
|
||||
// Wrap in try/finally to ensure lock file gets deleted
|
||||
try {
|
||||
$comment = new Comment($platform);
|
||||
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
|
||||
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
|
||||
|
||||
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
|
||||
} finally {
|
||||
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isAuthorized) {
|
||||
$resourceName = $resource->getAttribute('name');
|
||||
$projectName = $project->getAttribute('name');
|
||||
$name = "{$resourceName} ({$projectName})";
|
||||
$message = 'Authorization required for external contributor.';
|
||||
|
||||
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
|
||||
try {
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
if (empty($repositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
} catch (RepositoryNotFound $e) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
$owner = $github->getOwnerName($providerInstallationId);
|
||||
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'failure', $message, $authorizeUrl, $name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($external) {
|
||||
$pullRequestResponse = $github->getPullRequest($owner, $repositoryName, $providerPullRequestId);
|
||||
$providerRepositoryName = $pullRequestResponse['head']['repo']['owner']['login'];
|
||||
$providerRepositoryOwner = $pullRequestResponse['head']['repo']['name'];
|
||||
}
|
||||
|
||||
$commands = [];
|
||||
if (!empty($resource->getAttribute('installCommand', ''))) {
|
||||
$commands[] = $resource->getAttribute('installCommand', '');
|
||||
}
|
||||
if (!empty($resource->getAttribute('buildCommand', ''))) {
|
||||
$commands[] = $resource->getAttribute('buildCommand', '');
|
||||
}
|
||||
if (!empty($resource->getAttribute('commands', ''))) {
|
||||
$commands[] = $resource->getAttribute('commands', '');
|
||||
}
|
||||
|
||||
$deployment = $authorization->skip(fn () => $dbForProject->createDocument('deployments', new Document([
|
||||
'$id' => $deploymentId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceId' => $resourceId,
|
||||
'resourceInternalId' => $resourceInternalId,
|
||||
'resourceType' => $resourceCollection,
|
||||
'entrypoint' => $resource->getAttribute('entrypoint', ''),
|
||||
'buildCommands' => \implode(' && ', $commands),
|
||||
'startCommand' => $resource->getAttribute('startCommand', ''),
|
||||
'buildOutput' => $resource->getAttribute('outputDirectory', ''),
|
||||
'adapter' => $resource->getAttribute('adapter', ''),
|
||||
'fallbackFile' => $resource->getAttribute('fallbackFile', ''),
|
||||
'type' => 'vcs',
|
||||
'installationId' => $installationId,
|
||||
'installationInternalId' => $installationInternalId,
|
||||
'providerRepositoryId' => $providerRepositoryId,
|
||||
'repositoryId' => $repositoryId,
|
||||
'repositoryInternalId' => $repositoryInternalId,
|
||||
'providerBranchUrl' => $providerBranchUrl,
|
||||
'providerRepositoryName' => $providerRepositoryName,
|
||||
'providerRepositoryOwner' => $providerRepositoryOwner,
|
||||
'providerRepositoryUrl' => $providerRepositoryUrl,
|
||||
'providerCommitHash' => $providerCommitHash,
|
||||
'providerCommitAuthorUrl' => $providerCommitAuthorUrl,
|
||||
'providerCommitAuthor' => $providerCommitAuthor,
|
||||
'providerCommitMessage' => mb_strimwidth($providerCommitMessage, 0, 255, '...'),
|
||||
'providerCommitUrl' => $providerCommitUrl,
|
||||
'providerCommentId' => \strval($latestCommentId),
|
||||
'providerBranch' => $providerBranch,
|
||||
'activate' => $activate,
|
||||
])));
|
||||
|
||||
$resource = $resource
|
||||
->setAttribute('latestDeploymentId', $deployment->getId())
|
||||
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
|
||||
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
|
||||
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource));
|
||||
|
||||
if ($resource->getCollection() === 'sites') {
|
||||
$projectId = $project->getId();
|
||||
|
||||
// Deployment preview
|
||||
$sitesDomain = $platform['sitesDomain'];
|
||||
$domain = ID::unique() . "." . $sitesDomain;
|
||||
$ruleId = md5($domain);
|
||||
$previewRuleId = $ruleId;
|
||||
$authorization->skip(
|
||||
fn () => $dbForPlatform->createDocument('rules', new Document([
|
||||
'$id' => $ruleId,
|
||||
'projectId' => $project->getId(),
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'domain' => $domain,
|
||||
'type' => 'deployment',
|
||||
'trigger' => 'deployment',
|
||||
'deploymentId' => $deployment->getId(),
|
||||
'deploymentInternalId' => $deployment->getSequence(),
|
||||
'deploymentResourceType' => 'site',
|
||||
'deploymentResourceId' => $resourceId,
|
||||
'deploymentResourceInternalId' => $resourceInternalId,
|
||||
'deploymentVcsProviderBranch' => $providerBranch,
|
||||
'status' => 'verified',
|
||||
'certificateId' => '',
|
||||
'search' => implode(' ', [$ruleId, $domain]),
|
||||
'owner' => 'Appwrite',
|
||||
'region' => $project->getAttribute('region')
|
||||
]))
|
||||
);
|
||||
|
||||
// VCS branch preview
|
||||
if (!empty($providerBranch)) {
|
||||
$domain = (new BranchDomainFilter())->apply([
|
||||
'branch' => $providerBranch,
|
||||
'resourceId' => $resource->getId(),
|
||||
'projectId' => $project->getId(),
|
||||
'sitesDomain' => $sitesDomain,
|
||||
]);
|
||||
$ruleId = md5($domain);
|
||||
try {
|
||||
$authorization->skip(
|
||||
fn () => $dbForPlatform->createDocument('rules', new Document([
|
||||
'$id' => $ruleId,
|
||||
'projectId' => $project->getId(),
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'domain' => $domain,
|
||||
'type' => 'deployment',
|
||||
'trigger' => 'deployment',
|
||||
'deploymentId' => $deployment->getId(),
|
||||
'deploymentInternalId' => $deployment->getSequence(),
|
||||
'deploymentResourceType' => 'site',
|
||||
'deploymentResourceId' => $resourceId,
|
||||
'deploymentResourceInternalId' => $resourceInternalId,
|
||||
'deploymentVcsProviderBranch' => $providerBranch,
|
||||
'status' => 'verified',
|
||||
'certificateId' => '',
|
||||
'search' => implode(' ', [$ruleId, $domain]),
|
||||
'owner' => 'Appwrite',
|
||||
'region' => $project->getAttribute('region')
|
||||
]))
|
||||
);
|
||||
} catch (Duplicate $err) {
|
||||
// Ignore, rule already exists; will be updated by builds worker
|
||||
}
|
||||
}
|
||||
|
||||
// VCS commit preview
|
||||
if (!empty($providerCommitHash)) {
|
||||
$domain = "commit-" . substr($providerCommitHash, 0, 16) . ".{$sitesDomain}";
|
||||
$ruleId = md5($domain);
|
||||
try {
|
||||
$authorization->skip(
|
||||
fn () => $dbForPlatform->createDocument('rules', new Document([
|
||||
'$id' => $ruleId,
|
||||
'projectId' => $project->getId(),
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'domain' => $domain,
|
||||
'type' => 'deployment',
|
||||
'trigger' => 'deployment',
|
||||
'deploymentId' => $deployment->getId(),
|
||||
'deploymentInternalId' => $deployment->getSequence(),
|
||||
'deploymentResourceType' => 'site',
|
||||
'deploymentResourceId' => $resourceId,
|
||||
'deploymentResourceInternalId' => $resourceInternalId,
|
||||
'deploymentVcsProviderBranch' => $providerBranch,
|
||||
'status' => 'verified',
|
||||
'certificateId' => '',
|
||||
'search' => implode(' ', [$ruleId, $domain]),
|
||||
'owner' => 'Appwrite',
|
||||
'region' => $project->getAttribute('region')
|
||||
]))
|
||||
);
|
||||
} catch (Duplicate $err) {
|
||||
// Ignore, rule already exists; will be updated by builds worker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($resource->getCollection() === 'sites' && !empty($latestCommentId) && !empty($previewRuleId)) {
|
||||
$retries = 0;
|
||||
$lockAcquired = false;
|
||||
|
||||
while ($retries < 9) {
|
||||
$retries++;
|
||||
|
||||
try {
|
||||
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
|
||||
'$id' => $latestCommentId
|
||||
]));
|
||||
$lockAcquired = true;
|
||||
break;
|
||||
} catch (\Throwable $err) {
|
||||
if ($retries >= 9) {
|
||||
Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage());
|
||||
}
|
||||
|
||||
\sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
if ($lockAcquired) {
|
||||
// Wrap in try/finally to ensure lock file gets deleted
|
||||
try {
|
||||
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
|
||||
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
|
||||
|
||||
if (!empty($previewUrl)) {
|
||||
$comment = new Comment($platform);
|
||||
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
|
||||
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, $previewUrl);
|
||||
$github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
|
||||
}
|
||||
} finally {
|
||||
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($providerCommitHash) && $resource->getAttribute('providerSilentMode', false) === false) {
|
||||
$resourceName = $resource->getAttribute('name');
|
||||
$projectName = $project->getAttribute('name');
|
||||
$region = $project->getAttribute('region', 'default');
|
||||
$name = "{$resourceName} ({$projectName})";
|
||||
$message = 'Starting...';
|
||||
|
||||
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
|
||||
try {
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
if (empty($repositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
} catch (RepositoryNotFound $e) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
$owner = $github->getOwnerName($providerInstallationId);
|
||||
|
||||
$providerTargetUrl = $protocol . '://' . $hostname . "/console/project-$region-$projectId/$resourceCollection/$resourceType-$resourceId";
|
||||
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'pending', $message, $providerTargetUrl, $name);
|
||||
}
|
||||
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
->setResource($resource)
|
||||
->setDeployment($deployment)
|
||||
->setProject($project); // set the project because it won't be set for git deployments
|
||||
|
||||
$queueForBuilds->trigger(); // must trigger here so that we create a build for each function/site
|
||||
|
||||
//TODO: Add event?
|
||||
} catch (Throwable $e) {
|
||||
$errors[] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$queueForBuilds->reset(); // prevent shutdown hook from triggering again
|
||||
|
||||
if (!empty($errors)) {
|
||||
throw new Exception(Exception::GENERAL_UNKNOWN, \implode("\n", $errors));
|
||||
}
|
||||
};
|
||||
|
||||
Http::post('/v1/vcs/github/events')
|
||||
->desc('Create event')
|
||||
->groups(['api', 'vcs'])
|
||||
->label('scope', 'public')
|
||||
->inject('gitHub')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForBuilds')
|
||||
->inject('platform')
|
||||
->action(
|
||||
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
|
||||
$payload = $request->getRawPayload();
|
||||
$signatureRemote = $request->getHeader('x-hub-signature-256', '');
|
||||
$signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', '');
|
||||
|
||||
$valid = empty($signatureRemote) ? true : $github->validateWebhookEvent($payload, $signatureRemote, $signatureLocal);
|
||||
|
||||
if (!$valid) {
|
||||
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, "Invalid webhook payload signature. Please make sure the webhook secret has same value in your GitHub app and in the _APP_VCS_GITHUB_WEBHOOK_SECRET environment variable");
|
||||
}
|
||||
|
||||
$event = $request->getHeader('x-github-event', '');
|
||||
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
|
||||
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
|
||||
$parsedPayload = $github->getEvent($event, $payload);
|
||||
|
||||
if ($event == $github::EVENT_PUSH) {
|
||||
$providerBranchCreated = $parsedPayload["branchCreated"] ?? false;
|
||||
$providerBranchDeleted = $parsedPayload["branchDeleted"] ?? false;
|
||||
$providerBranch = $parsedPayload["branch"] ?? '';
|
||||
$providerBranchUrl = $parsedPayload["branchUrl"] ?? '';
|
||||
$providerRepositoryId = $parsedPayload["repositoryId"] ?? '';
|
||||
$providerRepositoryName = $parsedPayload["repositoryName"] ?? '';
|
||||
$providerInstallationId = $parsedPayload["installationId"] ?? '';
|
||||
$providerRepositoryUrl = $parsedPayload["repositoryUrl"] ?? '';
|
||||
$providerCommitHash = $parsedPayload["commitHash"] ?? '';
|
||||
$providerRepositoryOwner = $parsedPayload["owner"] ?? '';
|
||||
$providerCommitAuthorName = $parsedPayload["headCommitAuthorName"] ?? '';
|
||||
$providerCommitAuthorEmail = $parsedPayload["headCommitAuthorEmail"] ?? '';
|
||||
$providerCommitAuthorUrl = $parsedPayload["authorUrl"] ?? '';
|
||||
$providerCommitMessage = $parsedPayload["headCommitMessage"] ?? '';
|
||||
$providerCommitUrl = $parsedPayload["headCommitUrl"] ?? '';
|
||||
|
||||
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
|
||||
|
||||
//find resourceId from relevant resources table
|
||||
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
|
||||
Query::equal('providerRepositoryId', [$providerRepositoryId]),
|
||||
Query::limit(100),
|
||||
]));
|
||||
|
||||
// create new deployment only on push (not committed by us) and not when branch is created or deleted
|
||||
if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) {
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
|
||||
}
|
||||
} elseif ($event == $github::EVENT_INSTALLATION) {
|
||||
if ($parsedPayload["action"] == "deleted") {
|
||||
// TODO: Use worker for this job instead (update function/site as well)
|
||||
$providerInstallationId = $parsedPayload["installationId"];
|
||||
|
||||
$installations = $dbForPlatform->find('installations', [
|
||||
Query::equal('providerInstallationId', [$providerInstallationId]),
|
||||
Query::limit(1000)
|
||||
]);
|
||||
|
||||
foreach ($installations as $installation) {
|
||||
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
|
||||
Query::equal('installationInternalId', [$installation->getSequence()]),
|
||||
Query::limit(1000)
|
||||
]));
|
||||
|
||||
foreach ($repositories as $repository) {
|
||||
$authorization->skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId()));
|
||||
}
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->deleteDocument('installations', $installation->getId()));
|
||||
}
|
||||
}
|
||||
} elseif ($event == $github::EVENT_PULL_REQUEST) {
|
||||
if ($parsedPayload["action"] == "opened" || $parsedPayload["action"] == "reopened" || $parsedPayload["action"] == "synchronize") {
|
||||
$providerBranch = $parsedPayload["branch"] ?? '';
|
||||
$providerBranchUrl = $parsedPayload["branchUrl"] ?? '';
|
||||
$providerRepositoryId = $parsedPayload["repositoryId"] ?? '';
|
||||
$providerRepositoryName = $parsedPayload["repositoryName"] ?? '';
|
||||
$providerInstallationId = $parsedPayload["installationId"] ?? '';
|
||||
$providerRepositoryUrl = $parsedPayload["repositoryUrl"] ?? '';
|
||||
$providerPullRequestId = $parsedPayload["pullRequestNumber"] ?? '';
|
||||
$providerCommitHash = $parsedPayload["commitHash"] ?? '';
|
||||
$providerRepositoryOwner = $parsedPayload["owner"] ?? '';
|
||||
$external = $parsedPayload["external"] ?? true;
|
||||
$providerCommitUrl = $parsedPayload["headCommitUrl"] ?? '';
|
||||
$providerCommitAuthorUrl = $parsedPayload["authorUrl"] ?? '';
|
||||
|
||||
// Ignore sync for non-external. We handle it in push webhook
|
||||
if (!$external && $parsedPayload["action"] == "synchronize") {
|
||||
return $response->json($parsedPayload);
|
||||
}
|
||||
|
||||
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
|
||||
|
||||
$commitDetails = $github->getCommit($providerRepositoryOwner, $providerRepositoryName, $providerCommitHash);
|
||||
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
|
||||
$providerCommitMessage = $commitDetails["commitMessage"] ?? '';
|
||||
|
||||
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
|
||||
Query::equal('providerRepositoryId', [$providerRepositoryId]),
|
||||
Query::orderDesc('$createdAt')
|
||||
]));
|
||||
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
|
||||
} elseif ($parsedPayload["action"] == "closed") {
|
||||
// Allowed external contributions cleanup
|
||||
|
||||
$providerRepositoryId = $parsedPayload["repositoryId"] ?? '';
|
||||
$providerPullRequestId = $parsedPayload["pullRequestNumber"] ?? '';
|
||||
$external = $parsedPayload["external"] ?? true;
|
||||
|
||||
if ($external) {
|
||||
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
|
||||
Query::equal('providerRepositoryId', [$providerRepositoryId]),
|
||||
Query::orderDesc('$createdAt')
|
||||
]));
|
||||
|
||||
foreach ($repositories as $repository) {
|
||||
$providerPullRequestIds = $repository->getAttribute('providerPullRequestIds', []);
|
||||
|
||||
if (\in_array($providerPullRequestId, $providerPullRequestIds)) {
|
||||
$providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]);
|
||||
$repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds);
|
||||
$repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$response->json($parsedPayload);
|
||||
}
|
||||
);
|
||||
|
||||
Http::patch('/v1/vcs/github/installations/:installationId/repositories/:repositoryId')
|
||||
->desc('Update external deployment (authorize)')
|
||||
->groups(['api', 'vcs'])
|
||||
->label('scope', 'vcs.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vcs',
|
||||
group: 'repositories',
|
||||
name: 'updateExternalDeployments',
|
||||
description: '/docs/references/vcs/update-external-deployments.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('installationId', '', new Text(256), 'Installation Id')
|
||||
->param('repositoryId', '', new Text(256), 'VCS Repository Id')
|
||||
->param('providerPullRequestId', '', new Text(256), 'GitHub Pull Request Id')
|
||||
->inject('gitHub')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForBuilds')
|
||||
->inject('platform')
|
||||
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
|
||||
$installation = $dbForPlatform->getDocument('installations', $installationId);
|
||||
|
||||
if ($installation->isEmpty()) {
|
||||
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$repository = $authorization->skip(fn () => $dbForPlatform->findOne('repositories', [
|
||||
Query::equal('$id', [$repositoryId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()])
|
||||
]));
|
||||
|
||||
if ($repository->isEmpty()) {
|
||||
throw new Exception(Exception::REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (\in_array($providerPullRequestId, $repository->getAttribute('providerPullRequestIds', []))) {
|
||||
throw new Exception(Exception::PROVIDER_CONTRIBUTION_CONFLICT);
|
||||
}
|
||||
|
||||
$providerPullRequestIds = \array_unique(\array_merge($repository->getAttribute('providerPullRequestIds', []), [$providerPullRequestId]));
|
||||
$repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds);
|
||||
|
||||
// TODO: Delete from array when PR is closed
|
||||
|
||||
$repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
|
||||
|
||||
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
|
||||
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
|
||||
$providerInstallationId = $installation->getAttribute('providerInstallationId');
|
||||
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
|
||||
|
||||
$repositories = [$repository];
|
||||
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
|
||||
|
||||
$owner = $github->getOwnerName($providerInstallationId);
|
||||
try {
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
if (empty($repositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
} catch (RepositoryNotFound $e) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
$pullRequestResponse = $github->getPullRequest($owner, $repositoryName, $providerPullRequestId);
|
||||
|
||||
$providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? '';
|
||||
$providerCommitHash = $pullRequestResponse['head']['sha'] ?? '';
|
||||
$providerBranchUrl = $pullRequestResponse['head']['repo']['html_url'] ?? '';
|
||||
$providerRepositoryName = $pullRequestResponse['head']['repo']['name'] ?? '';
|
||||
$providerRepositoryUrl = $pullRequestResponse['head']['repo']['html_url'] ?? '';
|
||||
$providerRepositoryOwner = $pullRequestResponse['head']['repo']['owner']['login'] ?? '';
|
||||
$providerCommitAuthor = $pullRequestResponse['head']['user']['login'] ?? '';
|
||||
$providerCommitAuthorUrl = $pullRequestResponse['head']['user']['html_url'] ?? '';
|
||||
$providerCommitMessage = $pullRequestResponse['title'] ?? '';
|
||||
$providerCommitUrl = $pullRequestResponse['html_url'] ?? '';
|
||||
|
||||
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, '', '', '', '', $providerCommitHash, '', '', '', '', $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
+69
-119
@@ -5,11 +5,11 @@ require_once __DIR__ . '/../init.php';
|
||||
use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Bus\Events\ExecutionCompleted;
|
||||
use Appwrite\Bus\Events\RequestCompleted;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete as DeleteEvent;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Execution;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\Network\Cors;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
@@ -30,11 +30,14 @@ use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
|
||||
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
|
||||
use Appwrite\Utopia\Response\Filters\V18 as ResponseV18;
|
||||
use Appwrite\Utopia\Response\Filters\V19 as ResponseV19;
|
||||
use Appwrite\Utopia\Response\Filters\V20 as ResponseV20;
|
||||
use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
|
||||
use Appwrite\Utopia\View;
|
||||
use Executor\Executor;
|
||||
use MaxMind\Db\Reader;
|
||||
use Swoole\Http\Request as SwooleRequest;
|
||||
use Swoole\Table;
|
||||
use Utopia\Bus\Bus;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
@@ -62,7 +65,7 @@ Config::setParam('domainVerification', false);
|
||||
Config::setParam('cookieDomain', 'localhost');
|
||||
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
|
||||
|
||||
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
|
||||
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
|
||||
{
|
||||
$host = $request->getHostname() ?? '';
|
||||
if (!empty($previewHostname)) {
|
||||
@@ -131,8 +134,9 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$accessedAt = $project->getAttribute('accessedAt', 0);
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
|
||||
$project->setAttribute('accessedAt', DateTime::now());
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
|
||||
'accessedAt' => DateTime::now()
|
||||
])));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -320,7 +324,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
};
|
||||
|
||||
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
|
||||
$spec = Config::getParam('specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
|
||||
$spec = Config::getParam('specifications')[$resource->getAttribute('runtimeSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
|
||||
|
||||
$runtime = match ($type) {
|
||||
'function' => $runtimes[$resource->getAttribute('runtime')] ?? null,
|
||||
@@ -550,6 +554,10 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($deployment->getAttribute('startCommand', ''))) {
|
||||
$startCommand = 'cd /usr/local/server/src/function/ && ' . $deployment->getAttribute('startCommand', '');
|
||||
}
|
||||
|
||||
$runtimeEntrypoint = match ($version) {
|
||||
'v2' => '',
|
||||
default => "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$startCommand\"",
|
||||
@@ -706,10 +714,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
}
|
||||
} finally {
|
||||
if ($type === 'function' || $type === 'site') {
|
||||
$queueForExecutions
|
||||
->setExecution($execution)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
$bus->dispatch(new ExecutionCompleted(
|
||||
execution: $execution->getArrayCopy(),
|
||||
project: $project->getArrayCopy(),
|
||||
spec: $spec,
|
||||
resource: $resource->getArrayCopy(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -754,70 +764,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
->setStatusCode($execution['responseStatusCode'] ?? 200)
|
||||
->send($body);
|
||||
|
||||
$fileSize = 0;
|
||||
$file = $request->getFiles('file');
|
||||
if (!empty($file)) {
|
||||
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
|
||||
}
|
||||
|
||||
if (!empty($apiKey) && !empty($apiKey->getDisabledMetrics())) {
|
||||
foreach ($apiKey->getDisabledMetrics() as $key) {
|
||||
$queueForStatsUsage->disableMetric($key);
|
||||
}
|
||||
}
|
||||
|
||||
$metricTypeExecutions = str_replace(['{resourceType}'], [$deployment->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_EXECUTIONS);
|
||||
$metricTypeIdExecutions = str_replace(['{resourceType}', '{resourceInternalId}'], [$deployment->getAttribute('resourceType'), $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS);
|
||||
$metricTypeExecutionsCompute = str_replace(['{resourceType}'], [$deployment->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE);
|
||||
$metricTypeIdExecutionsCompute = str_replace(['{resourceType}', '{resourceInternalId}'], [$deployment->getAttribute('resourceType'), $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE);
|
||||
$metricTypeExecutionsMbSeconds = str_replace(['{resourceType}'], [$deployment->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS);
|
||||
$metricTypeIdExecutionsMBSeconds = str_replace(['{resourceType}', '{resourceInternalId}'], [$deployment->getAttribute('resourceType'), $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS);
|
||||
if ($deployment->getAttribute('resourceType') === 'sites') {
|
||||
$queueForStatsUsage
|
||||
->disableMetric(METRIC_NETWORK_REQUESTS)
|
||||
->disableMetric(METRIC_NETWORK_INBOUND)
|
||||
->disableMetric(METRIC_NETWORK_OUTBOUND);
|
||||
if ($resource->getAttribute('adapter') !== 'ssr') {
|
||||
$queueForStatsUsage
|
||||
->disableMetric(METRIC_EXECUTIONS)
|
||||
->disableMetric(METRIC_EXECUTIONS_COMPUTE)
|
||||
->disableMetric(METRIC_EXECUTIONS_MB_SECONDS)
|
||||
->disableMetric($metricTypeExecutions)
|
||||
->disableMetric($metricTypeIdExecutions)
|
||||
->disableMetric($metricTypeExecutionsCompute)
|
||||
->disableMetric($metricTypeIdExecutionsCompute)
|
||||
->disableMetric($metricTypeExecutionsMbSeconds)
|
||||
->disableMetric($metricTypeIdExecutionsMBSeconds);
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_SITES_REQUESTS, 1)
|
||||
->addMetric(METRIC_SITES_INBOUND, $request->getSize() + $fileSize)
|
||||
->addMetric(METRIC_SITES_OUTBOUND, $response->getSize())
|
||||
->addMetric(str_replace('{siteInternalId}', $resource->getSequence(), METRIC_SITES_ID_REQUESTS), 1)
|
||||
->addMetric(str_replace('{siteInternalId}', $resource->getSequence(), METRIC_SITES_ID_INBOUND), $request->getSize() + $fileSize)
|
||||
->addMetric(str_replace('{siteInternalId}', $resource->getSequence(), METRIC_SITES_ID_OUTBOUND), $response->getSize())
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
$compute = (int)($execution->getAttribute('duration') * 1000);
|
||||
$mbSeconds = (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT));
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_NETWORK_REQUESTS, 1)
|
||||
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
|
||||
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
|
||||
->addMetric(METRIC_EXECUTIONS, 1)
|
||||
->addMetric($metricTypeExecutions, 1)
|
||||
->addMetric($metricTypeIdExecutions, 1)
|
||||
->addMetric(METRIC_EXECUTIONS_COMPUTE, $compute) // per project
|
||||
->addMetric($metricTypeExecutionsCompute, $compute) // per function
|
||||
->addMetric($metricTypeIdExecutionsCompute, $compute) // per function
|
||||
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, $mbSeconds)
|
||||
->addMetric($metricTypeExecutionsMbSeconds, $mbSeconds)
|
||||
->addMetric($metricTypeIdExecutionsMBSeconds, $mbSeconds)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
response: $response,
|
||||
deployment: $deployment->getArrayCopy(),
|
||||
));
|
||||
|
||||
/* cleanup */
|
||||
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
|
||||
@@ -881,9 +833,8 @@ Http::init()
|
||||
->inject('locale')
|
||||
->inject('localeCodes')
|
||||
->inject('geodb')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForExecutions')
|
||||
->inject('bus')
|
||||
->inject('executor')
|
||||
->inject('platform')
|
||||
->inject('isResourceBlocked')
|
||||
@@ -894,7 +845,7 @@ Http::init()
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Execution $queueForExecutions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
@@ -902,7 +853,7 @@ Http::init()
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
// Only run Router when external domain
|
||||
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -988,17 +939,23 @@ Http::init()
|
||||
*/
|
||||
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
if ($responseFormat) {
|
||||
if (version_compare($responseFormat, '1.4.0', '<')) {
|
||||
$response->addFilter(new ResponseV16());
|
||||
if (version_compare($responseFormat, '1.9.0', '<')) {
|
||||
$response->addFilter(new ResponseV21());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.5.0', '<')) {
|
||||
$response->addFilter(new ResponseV17());
|
||||
if (version_compare($responseFormat, '1.8.0', '<')) {
|
||||
$response->addFilter(new ResponseV20());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.7.0', '<')) {
|
||||
$response->addFilter(new ResponseV19());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.6.0', '<')) {
|
||||
$response->addFilter(new ResponseV18());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.7.0', '<')) {
|
||||
$response->addFilter(new ResponseV19());
|
||||
if (version_compare($responseFormat, '1.5.0', '<')) {
|
||||
$response->addFilter(new ResponseV17());
|
||||
}
|
||||
if (version_compare($responseFormat, '1.4.0', '<')) {
|
||||
$response->addFilter(new ResponseV16());
|
||||
}
|
||||
if (version_compare($responseFormat, APP_VERSION_STABLE, '>')) {
|
||||
$warnings[] = "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is " . APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks";
|
||||
@@ -1178,8 +1135,7 @@ Http::options()
|
||||
->inject('dbForPlatform')
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForExecutions')
|
||||
->inject('bus')
|
||||
->inject('executor')
|
||||
->inject('geodb')
|
||||
->inject('isResourceBlocked')
|
||||
@@ -1192,14 +1148,14 @@ Http::options()
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
// Only run Router when external domain
|
||||
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -1215,12 +1171,11 @@ Http::options()
|
||||
/** OPTIONS requests in utopia do not execute shutdown handlers, as a result we need to track the OPTIONS requests explicitly
|
||||
* @see https://github.com/utopia-php/http/blob/0.33.16/src/App.php#L825-L855
|
||||
*/
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_NETWORK_REQUESTS, 1)
|
||||
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize())
|
||||
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
response: $response,
|
||||
));
|
||||
});
|
||||
|
||||
Http::error()
|
||||
@@ -1231,10 +1186,10 @@ Http::error()
|
||||
->inject('project')
|
||||
->inject('logger')
|
||||
->inject('log')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('bus')
|
||||
->inject('devKey')
|
||||
->inject('authorization')
|
||||
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage, Document $devKey, Authorization $authorization) {
|
||||
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$route = $utopia->getRoute();
|
||||
$class = \get_class($error);
|
||||
@@ -1307,21 +1262,12 @@ Http::error()
|
||||
*/
|
||||
if (!$publish && $project->getId() !== 'console') {
|
||||
if (!DBUser::isPrivileged($authorization->getRoles())) {
|
||||
$fileSize = 0;
|
||||
$file = $request->getFiles('file');
|
||||
if (!empty($file)) {
|
||||
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_NETWORK_REQUESTS, 1)
|
||||
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
|
||||
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
response: $response,
|
||||
));
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
if ($logger && $publish) {
|
||||
@@ -1453,6 +1399,9 @@ Http::error()
|
||||
$sdk = $route?->getLabel("sdk", false);
|
||||
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD';
|
||||
if (!empty($sdk)) {
|
||||
if (\is_array($sdk)) {
|
||||
$sdk = $sdk[0];
|
||||
}
|
||||
/** @var \Appwrite\SDK\Method $sdk */
|
||||
$action = $sdk->getNamespace() . '.' . $sdk->getMethodName();
|
||||
} elseif ($route === null) {
|
||||
@@ -1568,8 +1517,7 @@ Http::get('/robots.txt')
|
||||
->inject('dbForPlatform')
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForExecutions')
|
||||
->inject('bus')
|
||||
->inject('executor')
|
||||
->inject('geodb')
|
||||
->inject('isResourceBlocked')
|
||||
@@ -1579,13 +1527,13 @@ Http::get('/robots.txt')
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
$template = new View(__DIR__ . '/../views/general/robots.phtml');
|
||||
$response->text($template->render(false));
|
||||
} else {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -1603,8 +1551,7 @@ Http::get('/humans.txt')
|
||||
->inject('dbForPlatform')
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForExecutions')
|
||||
->inject('bus')
|
||||
->inject('executor')
|
||||
->inject('geodb')
|
||||
->inject('isResourceBlocked')
|
||||
@@ -1614,13 +1561,13 @@ Http::get('/humans.txt')
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
$template = new View(__DIR__ . '/../views/general/humans.phtml');
|
||||
$response->text($template->render(false));
|
||||
} else {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -1718,7 +1665,10 @@ Http::get('/v1/ping')
|
||||
->setAttribute('pingedAt', $pingedAt);
|
||||
|
||||
$authorization->skip(function () use ($dbForPlatform, $project) {
|
||||
$dbForPlatform->updateDocument('projects', $project->getId(), $project);
|
||||
$dbForPlatform->updateDocument('projects', $project->getId(), new Document([
|
||||
'pingCount' => $project->getAttribute('pingCount'),
|
||||
'pingedAt' => $project->getAttribute('pingedAt')
|
||||
]));
|
||||
});
|
||||
|
||||
$queueForEvents
|
||||
|
||||
+142
-113
@@ -2,6 +2,7 @@
|
||||
|
||||
use Appwrite\Auth\Key;
|
||||
use Appwrite\Auth\MFA\Type\TOTP;
|
||||
use Appwrite\Bus\Events\RequestCompleted;
|
||||
use Appwrite\Event\Audit;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
@@ -9,24 +10,28 @@ use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Message\Usage as UsageMessage;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\Functions\EventProcessor;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Abuse\Abuse;
|
||||
use Utopia\Bus\Bus;
|
||||
use Utopia\Cache\Adapter\Filesystem;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Authorization\Input;
|
||||
@@ -50,7 +55,7 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
|
||||
$replace = $parts[1] ?? '';
|
||||
|
||||
$params = match ($namespace) {
|
||||
'user' => (array)$user,
|
||||
'user' => (array) $user,
|
||||
'request' => $requestParams,
|
||||
default => $responsePayload,
|
||||
};
|
||||
@@ -58,13 +63,13 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
|
||||
if (array_key_exists($replace, $params)) {
|
||||
$replacement = $params[$replace];
|
||||
// Convert to string if it's not already a string
|
||||
if (!is_string($replacement)) {
|
||||
if (! is_string($replacement)) {
|
||||
if (is_array($replacement)) {
|
||||
$replacement = json_encode($replacement);
|
||||
} elseif (is_object($replacement) && method_exists($replacement, '__toString')) {
|
||||
$replacement = (string)$replacement;
|
||||
$replacement = (string) $replacement;
|
||||
} elseif (is_scalar($replacement)) {
|
||||
$replacement = (string)$replacement;
|
||||
$replacement = (string) $replacement;
|
||||
} else {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, "The server encountered an error while parsing the label: $label. Please create an issue on GitHub to allow us to investigate further https://github.com/appwrite/appwrite/issues/new/choose");
|
||||
}
|
||||
@@ -72,6 +77,7 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
|
||||
$label = \str_replace($find, $replacement, $label);
|
||||
}
|
||||
}
|
||||
|
||||
return $label;
|
||||
};
|
||||
|
||||
@@ -157,7 +163,7 @@ Http::init()
|
||||
$scopes = $roles[$role]['scopes'];
|
||||
|
||||
// Step 5: API Key Authentication
|
||||
if (!empty($apiKey)) {
|
||||
if (! empty($apiKey)) {
|
||||
// Check if key is expired
|
||||
if ($apiKey->isExpired()) {
|
||||
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
|
||||
@@ -167,7 +173,6 @@ Http::init()
|
||||
$role = $apiKey->getRole();
|
||||
$scopes = $apiKey->getScopes();
|
||||
|
||||
|
||||
// Handle special app role case
|
||||
if ($apiKey->getRole() === User::ROLE_APPS) {
|
||||
// Disable authorization checks for project API keys
|
||||
@@ -190,19 +195,19 @@ Http::init()
|
||||
// For standard keys, update last accessed time
|
||||
if (\in_array($apiKey->getType(), [API_KEY_STANDARD, API_KEY_ORGANIZATION, API_KEY_ACCOUNT])) {
|
||||
$dbKey = null;
|
||||
if (!empty($apiKey->getProjectId())) {
|
||||
if (! empty($apiKey->getProjectId())) {
|
||||
$dbKey = $project->find(
|
||||
key: 'secret',
|
||||
find: $request->getHeader('x-appwrite-key', ''),
|
||||
subject: 'keys'
|
||||
);
|
||||
} elseif (!empty($apiKey->getUserId())) {
|
||||
} elseif (! empty($apiKey->getUserId())) {
|
||||
$dbKey = $user->find(
|
||||
key: 'secret',
|
||||
find: $request->getHeader('x-appwrite-key', ''),
|
||||
subject: 'keys'
|
||||
);
|
||||
} elseif (!empty($apiKey->getTeamId())) {
|
||||
} elseif (! empty($apiKey->getTeamId())) {
|
||||
$dbKey = $team->find(
|
||||
key: 'secret',
|
||||
find: $request->getHeader('x-appwrite-key', ''),
|
||||
@@ -210,7 +215,7 @@ Http::init()
|
||||
);
|
||||
}
|
||||
|
||||
if (!$dbKey) {
|
||||
if (! $dbKey) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@@ -228,7 +233,7 @@ Http::init()
|
||||
if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) {
|
||||
$sdks = $dbKey->getAttribute('sdks', []);
|
||||
|
||||
if (!in_array($sdk, $sdks)) {
|
||||
if (! in_array($sdk, $sdks)) {
|
||||
$sdks[] = $sdk;
|
||||
|
||||
$updates->setAttribute('sdks', $sdks);
|
||||
@@ -236,14 +241,14 @@ Http::init()
|
||||
}
|
||||
}
|
||||
|
||||
if (!$updates->isEmpty()) {
|
||||
if (! $updates->isEmpty()) {
|
||||
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->updateDocument('keys', $dbKey->getId(), $updates));
|
||||
|
||||
if (!empty($apiKey->getProjectId())) {
|
||||
if (! empty($apiKey->getProjectId())) {
|
||||
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
} elseif (!empty($apiKey->getUserId())) {
|
||||
} elseif (! empty($apiKey->getUserId())) {
|
||||
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('users', $user->getId()));
|
||||
} elseif (!empty($apiKey->getTeamId())) {
|
||||
} elseif (! empty($apiKey->getTeamId())) {
|
||||
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('teams', $team->getId()));
|
||||
}
|
||||
}
|
||||
@@ -280,7 +285,7 @@ Http::init()
|
||||
}
|
||||
}
|
||||
} // Admin User Authentication
|
||||
elseif (($project->getId() === 'console' && !$team->isEmpty() && !$user->isEmpty()) || ($project->getId() !== 'console' && !$user->isEmpty() && $mode === APP_MODE_ADMIN)) {
|
||||
elseif (($project->getId() === 'console' && ! $team->isEmpty() && ! $user->isEmpty()) || ($project->getId() !== 'console' && ! $user->isEmpty() && $mode === APP_MODE_ADMIN)) {
|
||||
$teamId = $team->getId();
|
||||
$adminRoles = [];
|
||||
$memberships = $user->getAttribute('memberships', []);
|
||||
@@ -305,7 +310,7 @@ Http::init()
|
||||
// Useful for those who have project-specific roles but don't have team-wide role.
|
||||
$scopes = ['teams.read', 'projects.read'];
|
||||
foreach ($adminRoles as $adminRole) {
|
||||
$isTeamWideRole = !str_starts_with($adminRole, 'project-');
|
||||
$isTeamWideRole = ! str_starts_with($adminRole, 'project-');
|
||||
$isProjectSpecificRole = $projectId !== 'console' && str_starts_with($adminRole, 'project-' . $projectId);
|
||||
|
||||
if ($isTeamWideRole || $isProjectSpecificRole) {
|
||||
@@ -343,34 +348,39 @@ Http::init()
|
||||
* But, for actions on resources (sites, functions, etc.) in a non-console project, we explicitly check
|
||||
* whether the admin user has necessary permission on the project (sites, functions, etc. don't have permissions associated to them).
|
||||
*/
|
||||
if (empty($apiKey) && !$user->isEmpty() && $project->getId() !== 'console' && $mode === APP_MODE_ADMIN) {
|
||||
if (empty($apiKey) && ! $user->isEmpty() && $project->getId() !== 'console' && $mode === APP_MODE_ADMIN) {
|
||||
$input = new Input(Database::PERMISSION_READ, $project->getPermissionsByType(Database::PERMISSION_READ));
|
||||
$initialStatus = $authorization->getStatus();
|
||||
$authorization->enable();
|
||||
if (!$authorization->isValid($input)) {
|
||||
if (! $authorization->isValid($input)) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
$authorization->setStatus($initialStatus);
|
||||
}
|
||||
|
||||
// Step 6: Update project and user last activity
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
if (! $project->isEmpty() && $project->getId() !== 'console') {
|
||||
$accessedAt = $project->getAttribute('accessedAt', 0);
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
|
||||
$project->setAttribute('accessedAt', DateTime::now());
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
|
||||
'accessedAt' => DateTime::now()
|
||||
])));
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($user->getId())) {
|
||||
if (! empty($user->getId())) {
|
||||
$accessedAt = $user->getAttribute('accessedAt', 0);
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
|
||||
$user->setAttribute('accessedAt', DateTime::now());
|
||||
|
||||
if ($project->getId() !== 'console' && APP_MODE_ADMIN !== $mode) {
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
if ($project->getId() !== 'console' && $mode !== APP_MODE_ADMIN) {
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'accessedAt' => $user->getAttribute('accessedAt')
|
||||
]));
|
||||
} else {
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), $user));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), new Document([
|
||||
'accessedAt' => $user->getAttribute('accessedAt')
|
||||
])));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -387,26 +397,26 @@ Http::init()
|
||||
$method = $method[0];
|
||||
}
|
||||
|
||||
if (!empty($method)) {
|
||||
if (! empty($method)) {
|
||||
$namespace = $method->getNamespace();
|
||||
|
||||
if (
|
||||
array_key_exists($namespace, $project->getAttribute('services', []))
|
||||
&& !$project->getAttribute('services', [])[$namespace]
|
||||
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
&& ! $project->getAttribute('services', [])[$namespace]
|
||||
&& ! (User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 9: Validate scope permissions
|
||||
$allowed = (array)$route->getLabel('scope', 'none');
|
||||
$allowed = (array) $route->getLabel('scope', 'none');
|
||||
if (empty(\array_intersect($allowed, $scopes))) {
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scopes (' . \json_encode($allowed) . ')');
|
||||
}
|
||||
|
||||
// Step 10: Check if user is blocked
|
||||
if (false === $user->getAttribute('status')) { // Account is blocked
|
||||
if ($user->getAttribute('status') === false) { // Account is blocked
|
||||
throw new Exception(Exception::USER_BLOCKED);
|
||||
}
|
||||
|
||||
@@ -424,7 +434,7 @@ Http::init()
|
||||
$minimumFactors = ($mfaEnabled && $hasMoreFactors) ? 2 : 1;
|
||||
|
||||
// Step 13: Handle Multi-Factor Authentication
|
||||
if (!in_array('mfa', $route->getGroups())) {
|
||||
if (! in_array('mfa', $route->getGroups())) {
|
||||
if ($session && \count($session->getAttribute('factors', [])) < $minimumFactors) {
|
||||
throw new Exception(Exception::USER_MORE_FACTORS_REQUIRED);
|
||||
}
|
||||
@@ -444,7 +454,7 @@ Http::init()
|
||||
->inject('queueForDeletes')
|
||||
->inject('queueForDatabase')
|
||||
->inject('queueForBuilds')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForMails')
|
||||
->inject('dbForProject')
|
||||
@@ -457,14 +467,14 @@ Http::init()
|
||||
->inject('telemetry')
|
||||
->inject('platform')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, 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();
|
||||
|
||||
if (
|
||||
array_key_exists('rest', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['rest']
|
||||
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
&& ! $project->getAttribute('apis', [])['rest']
|
||||
&& ! (User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
@@ -476,7 +486,7 @@ Http::init()
|
||||
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
|
||||
$timeLimitArray = [];
|
||||
|
||||
$abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
|
||||
$abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
|
||||
|
||||
foreach ($abuseKeyLabel as $abuseKey) {
|
||||
$start = $request->getContentRangeStart();
|
||||
@@ -489,7 +499,7 @@ Http::init()
|
||||
->setParam('{ip}', $request->getIP())
|
||||
->setParam('{url}', $request->getHostname() . $route->getPath())
|
||||
->setParam('{method}', $request->getMethod())
|
||||
->setParam('{chunkId}', (int)($start / ($end + 1 - $start)));
|
||||
->setParam('{chunkId}', (int) ($start / ($end + 1 - $start)));
|
||||
$timeLimitArray[] = $timeLimit;
|
||||
}
|
||||
|
||||
@@ -501,7 +511,7 @@ Http::init()
|
||||
|
||||
foreach ($timeLimitArray as $timeLimit) {
|
||||
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
|
||||
if (!empty($value)) {
|
||||
if (! empty($value)) {
|
||||
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
|
||||
}
|
||||
}
|
||||
@@ -524,8 +534,8 @@ Http::init()
|
||||
|
||||
if (
|
||||
$enabled // Abuse is enabled
|
||||
&& !$isAppUser // User is not API key
|
||||
&& !$isPrivilegedUser // User is not an admin
|
||||
&& ! $isAppUser // User is not API key
|
||||
&& ! $isPrivilegedUser // User is not an admin
|
||||
&& $devKey->isEmpty() // request doesn't not contain development key
|
||||
&& $abuse->check() // Route is rate-limited
|
||||
) {
|
||||
@@ -554,19 +564,13 @@ Http::init()
|
||||
->setProject($project);
|
||||
|
||||
/* If a session exists, use the user associated with the session */
|
||||
if (!$user->isEmpty()) {
|
||||
if (! $user->isEmpty()) {
|
||||
$userClone = clone $user;
|
||||
// $user doesn't support `type` and can cause unintended effects.
|
||||
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
|
||||
$queueForAudits->setUser($userClone);
|
||||
}
|
||||
|
||||
if (!empty($apiKey) && !empty($apiKey->getDisabledMetrics())) {
|
||||
foreach ($apiKey->getDisabledMetrics() as $key) {
|
||||
$queueForStatsUsage->disableMetric($key);
|
||||
}
|
||||
}
|
||||
|
||||
/* Auto-set projects */
|
||||
$queueForDeletes->setProject($project);
|
||||
$queueForDatabase->setProject($project);
|
||||
@@ -580,73 +584,70 @@ Http::init()
|
||||
$queueForBuilds->setPlatform($platform);
|
||||
$queueForMails->setPlatform($platform);
|
||||
|
||||
|
||||
$useCache = $route->getLabel('cache', false);
|
||||
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
|
||||
if ($useCache) {
|
||||
$route = $utopia->match($request);
|
||||
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
|
||||
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !User::isPrivileged($authorization->getRoles());
|
||||
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! User::isPrivileged($authorization->getRoles());
|
||||
|
||||
$key = $request->cacheIdentifier();
|
||||
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
$cache = new Cache(
|
||||
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
|
||||
);
|
||||
$timestamp = 60 * 60 * 24 * 180; // Temporarily increase the TTL to 180 day to ensure files in the cache are still fetched.
|
||||
$data = $cache->load($key, $timestamp);
|
||||
|
||||
if (!empty($data) && !$cacheLog->isEmpty()) {
|
||||
$usageMetric = $route->getLabel('usage.metric', null);
|
||||
if ($usageMetric === METRIC_AVATARS_SCREENSHOTS_GENERATED) {
|
||||
$queueForStatsUsage->disableMetric(METRIC_AVATARS_SCREENSHOTS_GENERATED);
|
||||
}
|
||||
if (! empty($data) && ! $cacheLog->isEmpty()) {
|
||||
$parts = explode('/', $cacheLog->getAttribute('resourceType', ''));
|
||||
$type = $parts[0] ?? null;
|
||||
|
||||
if ($type === 'bucket' && (!$isImageTransformation || !$isDisabled)) {
|
||||
if ($type === 'bucket' && (! $isImageTransformation || ! $isDisabled)) {
|
||||
$bucketId = $parts[1] ?? null;
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
|
||||
$isToken = ! $resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) {
|
||||
if ($bucket->isEmpty() || (! $bucket->getAttribute('enabled') && ! $isAppUser && ! $isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$bucket->getAttribute('transformations', true) && !$isAppUser && !$isPrivilegedUser) {
|
||||
if (! $bucket->getAttribute('transformations', true) && ! $isAppUser && ! $isPrivilegedUser) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED);
|
||||
}
|
||||
|
||||
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
|
||||
$valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead()));
|
||||
if (!$fileSecurity && !$valid && !$isToken) {
|
||||
if (! $fileSecurity && ! $valid && ! $isToken) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$parts = explode('/', $cacheLog->getAttribute('resource'));
|
||||
$fileId = $parts[1] ?? null;
|
||||
|
||||
if ($fileSecurity && !$valid && !$isToken) {
|
||||
if ($fileSecurity && ! $valid && ! $isToken) {
|
||||
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
|
||||
} else {
|
||||
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
|
||||
}
|
||||
|
||||
if (!$resourceToken->isEmpty() && $resourceToken->getAttribute('fileInternalId') !== $file->getSequence()) {
|
||||
if (! $resourceToken->isEmpty() && $resourceToken->getAttribute('fileInternalId') !== $file->getSequence()) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if ($file->isEmpty()) {
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
//Do not update transformedAt if it's a console user
|
||||
if (!User::isPrivileged($authorization->getRoles())) {
|
||||
// Do not update transformedAt if it's a console user
|
||||
if (! User::isPrivileged($authorization->getRoles())) {
|
||||
$transformedAt = $file->getAttribute('transformedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
|
||||
$file->setAttribute('transformedAt', DateTime::now());
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file));
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), new Document([
|
||||
'transformedAt' => $file->getAttribute('transformedAt')
|
||||
])));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -656,7 +657,7 @@ Http::init()
|
||||
->addHeader('X-Appwrite-Cache', 'hit')
|
||||
->setContentType($cacheLog->getAttribute('mimeType'));
|
||||
$storageCacheOperationsCounter->add(1, ['result' => 'hit']);
|
||||
if (!$isImageTransformation || !$isDisabled) {
|
||||
if (! $isImageTransformation || ! $isDisabled) {
|
||||
$response->send($data);
|
||||
}
|
||||
} else {
|
||||
@@ -679,7 +680,7 @@ Http::init()
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$user->isEmpty()) {
|
||||
if (! $user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_SESSION_ALREADY_EXISTS);
|
||||
}
|
||||
});
|
||||
@@ -733,7 +734,8 @@ Http::shutdown()
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForAudits')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('publisherForUsage')
|
||||
->inject('queueForDeletes')
|
||||
->inject('queueForDatabase')
|
||||
->inject('queueForBuilds')
|
||||
@@ -745,11 +747,13 @@ Http::shutdown()
|
||||
->inject('authorization')
|
||||
->inject('timelimit')
|
||||
->inject('eventProcessor')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor) use ($parseLabel) {
|
||||
->inject('bus')
|
||||
->inject('apiKey')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey) use ($parseLabel) {
|
||||
|
||||
$responsePayload = $response->getPayload();
|
||||
|
||||
if (!empty($queueForEvents->getEvent())) {
|
||||
if (! empty($queueForEvents->getEvent())) {
|
||||
if (empty($queueForEvents->getPayload())) {
|
||||
$queueForEvents->setPayload($responsePayload);
|
||||
}
|
||||
@@ -771,7 +775,7 @@ Http::shutdown()
|
||||
}
|
||||
|
||||
// Only trigger functions if there are matching function events
|
||||
if (!empty($functionsEvents)) {
|
||||
if (! empty($functionsEvents)) {
|
||||
foreach ($generatedEvents as $event) {
|
||||
if (isset($functionsEvents[$event])) {
|
||||
$queueForFunctions
|
||||
@@ -783,7 +787,7 @@ Http::shutdown()
|
||||
}
|
||||
|
||||
// Only trigger webhooks if there are matching webhook events
|
||||
if (!empty($webhooksEvents)) {
|
||||
if (! empty($webhooksEvents)) {
|
||||
foreach ($generatedEvents as $event) {
|
||||
if (isset($webhooksEvents[$event])) {
|
||||
$queueForWebhooks
|
||||
@@ -807,7 +811,7 @@ Http::shutdown()
|
||||
|
||||
if ($abuseEnabled && \count($abuseResetCode) > 0 && \in_array($response->getStatusCode(), $abuseResetCode)) {
|
||||
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
|
||||
$abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
|
||||
$abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
|
||||
|
||||
foreach ($abuseKeyLabel as $abuseKey) {
|
||||
$start = $request->getContentRangeStart();
|
||||
@@ -820,10 +824,10 @@ Http::shutdown()
|
||||
->setParam('{ip}', $request->getIP())
|
||||
->setParam('{url}', $request->getHostname() . $route->getPath())
|
||||
->setParam('{method}', $request->getMethod())
|
||||
->setParam('{chunkId}', (int)($start / ($end + 1 - $start)));
|
||||
->setParam('{chunkId}', (int) ($start / ($end + 1 - $start)));
|
||||
|
||||
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
|
||||
if (!empty($value)) {
|
||||
if (! empty($value)) {
|
||||
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
|
||||
}
|
||||
}
|
||||
@@ -837,14 +841,14 @@ Http::shutdown()
|
||||
* Audit labels
|
||||
*/
|
||||
$pattern = $route->getLabel('audits.resource', null);
|
||||
if (!empty($pattern)) {
|
||||
if (! empty($pattern)) {
|
||||
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
|
||||
if (!empty($resource) && $resource !== $pattern) {
|
||||
if (! empty($resource) && $resource !== $pattern) {
|
||||
$queueForAudits->setResource($resource);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$user->isEmpty()) {
|
||||
if (! $user->isEmpty()) {
|
||||
$userClone = clone $user;
|
||||
// $user doesn't support `type` and can cause unintended effects.
|
||||
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
|
||||
@@ -870,13 +874,13 @@ Http::shutdown()
|
||||
$queueForAudits->setUser($user);
|
||||
}
|
||||
|
||||
if (!empty($queueForAudits->getResource()) && !$queueForAudits->getUser()->isEmpty()) {
|
||||
if (! empty($queueForAudits->getResource()) && ! $queueForAudits->getUser()->isEmpty()) {
|
||||
/**
|
||||
* audits.payload is switched to default true
|
||||
* in order to auto audit payload for all endpoints
|
||||
*/
|
||||
$pattern = $route->getLabel('audits.payload', true);
|
||||
if (!empty($pattern)) {
|
||||
if (! empty($pattern)) {
|
||||
$queueForAudits->setPayload($responsePayload);
|
||||
}
|
||||
|
||||
@@ -887,19 +891,19 @@ Http::shutdown()
|
||||
$queueForAudits->trigger();
|
||||
}
|
||||
|
||||
if (!empty($queueForDeletes->getType())) {
|
||||
if (! empty($queueForDeletes->getType())) {
|
||||
$queueForDeletes->trigger();
|
||||
}
|
||||
|
||||
if (!empty($queueForDatabase->getType())) {
|
||||
if (! empty($queueForDatabase->getType())) {
|
||||
$queueForDatabase->trigger();
|
||||
}
|
||||
|
||||
if (!empty($queueForBuilds->getType())) {
|
||||
if (! empty($queueForBuilds->getType())) {
|
||||
$queueForBuilds->trigger();
|
||||
}
|
||||
|
||||
if (!empty($queueForMessaging->getType())) {
|
||||
if (! empty($queueForMessaging->getType())) {
|
||||
$queueForMessaging->trigger();
|
||||
}
|
||||
|
||||
@@ -908,14 +912,14 @@ Http::shutdown()
|
||||
if ($useCache) {
|
||||
$resource = $resourceType = null;
|
||||
$data = $response->getPayload();
|
||||
if (!empty($data['payload'])) {
|
||||
if (! empty($data['payload'])) {
|
||||
$pattern = $route->getLabel('cache.resource', null);
|
||||
if (!empty($pattern)) {
|
||||
if (! empty($pattern)) {
|
||||
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
|
||||
}
|
||||
|
||||
$pattern = $route->getLabel('cache.resourceType', null);
|
||||
if (!empty($pattern)) {
|
||||
if (! empty($pattern)) {
|
||||
$resourceType = $parseLabel($pattern, $responsePayload, $requestParams, $user);
|
||||
}
|
||||
|
||||
@@ -925,21 +929,28 @@ Http::shutdown()
|
||||
|
||||
$key = $request->cacheIdentifier();
|
||||
$signature = md5($data['payload']);
|
||||
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
$accessedAt = $cacheLog->getAttribute('accessedAt', 0);
|
||||
$now = DateTime::now();
|
||||
if ($cacheLog->isEmpty()) {
|
||||
$authorization->skip(fn () => $dbForProject->createDocument('cache', new Document([
|
||||
'$id' => $key,
|
||||
'resource' => $resource,
|
||||
'resourceType' => $resourceType,
|
||||
'mimeType' => $response->getContentType(),
|
||||
'accessedAt' => $now,
|
||||
'signature' => $signature,
|
||||
])));
|
||||
try {
|
||||
$authorization->skip(fn () => $dbForProject->createDocument('cache', new Document([
|
||||
'$id' => $key,
|
||||
'resource' => $resource,
|
||||
'resourceType' => $resourceType,
|
||||
'mimeType' => $response->getContentType(),
|
||||
'accessedAt' => $now,
|
||||
'signature' => $signature,
|
||||
])));
|
||||
} catch (DuplicateException) {
|
||||
// Race condition: another concurrent request already created the cache document
|
||||
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
}
|
||||
} elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
|
||||
$cacheLog->setAttribute('accessedAt', $now);
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), new Document([
|
||||
'accessedAt' => $cacheLog->getAttribute('accessedAt')
|
||||
])));
|
||||
// Overwrite the file every APP_CACHE_UPDATE seconds to update the file modified time that is used in the TTL checks in cache->load()
|
||||
$cache->save($key, $data['payload']);
|
||||
}
|
||||
@@ -951,22 +962,40 @@ Http::shutdown()
|
||||
}
|
||||
|
||||
if ($project->getId() !== 'console') {
|
||||
if (!User::isPrivileged($authorization->getRoles())) {
|
||||
$fileSize = 0;
|
||||
$file = $request->getFiles('file');
|
||||
if (!empty($file)) {
|
||||
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_NETWORK_REQUESTS, 1)
|
||||
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
|
||||
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
|
||||
if (! User::isPrivileged($authorization->getRoles())) {
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
response: $response,
|
||||
));
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
// Publish usage metrics if context has data
|
||||
if (! $usage->isEmpty()) {
|
||||
$metrics = $usage->getMetrics();
|
||||
|
||||
// Filter out API key disabled metrics using suffix pattern matching
|
||||
$disabledMetrics = $apiKey?->getDisabledMetrics() ?? [];
|
||||
if (! empty($disabledMetrics)) {
|
||||
$metrics = array_values(array_filter($metrics, function ($metric) use ($disabledMetrics) {
|
||||
foreach ($disabledMetrics as $pattern) {
|
||||
if (str_ends_with($metric['key'], $pattern)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
$message = new UsageMessage(
|
||||
project: $project,
|
||||
metrics: $metrics,
|
||||
reduce: $usage->getReduce()
|
||||
);
|
||||
|
||||
$publisherForUsage->enqueue($message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -188,6 +188,10 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
|
||||
Console::success('Reload completed...');
|
||||
});
|
||||
|
||||
Http::setResource('bus', function ($register, $utopia) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $utopia->getResource($name));
|
||||
}, ['register', 'utopia']);
|
||||
|
||||
include __DIR__ . '/controllers/general.php';
|
||||
|
||||
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
|
||||
@@ -577,6 +581,9 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
|
||||
|
||||
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD';
|
||||
if (!empty($sdk)) {
|
||||
if (\is_array($sdk)) {
|
||||
$sdk = $sdk[0];
|
||||
}
|
||||
/** @var Appwrite\SDK\Method $sdk */
|
||||
$action = $sdk->getNamespace() . '.' . $sdk->getMethodName();
|
||||
} elseif ($route === null) {
|
||||
|
||||
@@ -93,6 +93,7 @@ const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=
|
||||
const APP_COMPUTE_CPUS_DEFAULT = 0.5;
|
||||
const APP_COMPUTE_MEMORY_DEFAULT = 512;
|
||||
const APP_COMPUTE_SPECIFICATION_DEFAULT = Specification::S_1VCPU_512MB;
|
||||
const APP_COMPUTE_DEPLOYMENT_MAX_RETENTION = 100 * 365; // 100 years
|
||||
const APP_SDK_PLATFORM_SERVER = 'server';
|
||||
const APP_SDK_PLATFORM_CLIENT = 'client';
|
||||
const APP_SDK_PLATFORM_CONSOLE = 'console';
|
||||
@@ -361,6 +362,12 @@ const METRIC_AVATARS_SCREENSHOTS_GENERATED = 'avatars.screenshotsGenerated';
|
||||
const METRIC_FUNCTIONS_RUNTIME = 'functions.runtimes.{runtime}';
|
||||
const METRIC_SITES_FRAMEWORK = 'sites.frameworks.{framework}';
|
||||
|
||||
// Realtime metrics
|
||||
const METRIC_REALTIME_CONNECTIONS = 'realtime.connections';
|
||||
const METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT = 'realtime.messages.sent';
|
||||
const METRIC_REALTIME_INBOUND = 'realtime.inbound';
|
||||
const METRIC_REALTIME_OUTBOUND = 'realtime.outbound';
|
||||
|
||||
// Resource types
|
||||
const RESOURCE_TYPE_PROJECTS = 'projects';
|
||||
const RESOURCE_TYPE_FUNCTIONS = 'functions';
|
||||
|
||||
@@ -420,6 +420,7 @@ $register->set('smtp', function () {
|
||||
$mail->Password = $password;
|
||||
$mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', '');
|
||||
$mail->SMTPAutoTLS = false;
|
||||
$mail->SMTPKeepAlive = true;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Timeout = 10; /* Connection timeout */
|
||||
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
|
||||
@@ -449,3 +450,11 @@ $register->set('promiseAdapter', function () {
|
||||
$register->set('hooks', function () {
|
||||
return new Hooks();
|
||||
});
|
||||
$listeners = require __DIR__ . '/../listeners.php';
|
||||
$register->set('bus', function () use ($listeners) {
|
||||
$bus = new \Utopia\Bus\Bus();
|
||||
foreach ($listeners as $listener) {
|
||||
$bus->subscribe($listener);
|
||||
}
|
||||
return $bus;
|
||||
});
|
||||
|
||||
+130
-118
@@ -10,15 +10,14 @@ use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Execution;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\StatsResources;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Functions\EventProcessor;
|
||||
@@ -27,6 +26,7 @@ use Appwrite\Network\Cors;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Network\Validator\Redirect;
|
||||
use Appwrite\Usage\Context as UsageContext;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
@@ -58,6 +58,7 @@ use Utopia\Logger\Log;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Broker\Pool as BrokerPool;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Storage\Device\AWS;
|
||||
use Utopia\Storage\Device\Backblaze;
|
||||
@@ -89,6 +90,7 @@ Http::setResource('register', fn () => $register);
|
||||
Http::setResource('locale', function () {
|
||||
$locale = new Locale(System::getEnv('_APP_LOCALE', 'en'));
|
||||
$locale->setFallback(System::getEnv('_APP_LOCALE', 'en'));
|
||||
|
||||
return $locale;
|
||||
});
|
||||
|
||||
@@ -109,9 +111,6 @@ Http::setResource('publisherFunctions', function (Publisher $publisher) {
|
||||
Http::setResource('publisherMigrations', function (Publisher $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
Http::setResource('publisherStatsUsage', function (Publisher $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
Http::setResource('publisherMails', function (Publisher $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
@@ -151,18 +150,19 @@ Http::setResource('queueForWebhooks', function (Publisher $publisher) {
|
||||
Http::setResource('queueForRealtime', function () {
|
||||
return new Realtime();
|
||||
}, []);
|
||||
Http::setResource('queueForStatsUsage', function (Publisher $publisher) {
|
||||
return new StatsUsage($publisher);
|
||||
}, ['publisher']);
|
||||
Http::setResource('usage', function () {
|
||||
return new UsageContext();
|
||||
}, []);
|
||||
Http::setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
Http::setResource('queueForAudits', function (Publisher $publisher) {
|
||||
return new AuditEvent($publisher);
|
||||
}, ['publisher']);
|
||||
Http::setResource('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
Http::setResource('queueForExecutions', function (Publisher $publisher) {
|
||||
return new Execution($publisher);
|
||||
}, ['publisher']);
|
||||
Http::setResource('eventProcessor', function () {
|
||||
return new EventProcessor();
|
||||
}, []);
|
||||
@@ -190,14 +190,14 @@ Http::setResource('allowedHostnames', function (array $platform, Document $proje
|
||||
$allowed = [...($platform['hostnames'] ?? [])];
|
||||
|
||||
/* Add platform configured hostnames */
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
if (! $project->isEmpty() && $project->getId() !== 'console') {
|
||||
$platforms = $project->getAttribute('platforms', []);
|
||||
$hostnames = Platform::getHostnames($platforms);
|
||||
$allowed = [...$allowed, ...$hostnames];
|
||||
}
|
||||
|
||||
/* Add the request hostname if a dev key is found */
|
||||
if (!$devKey->isEmpty()) {
|
||||
if (! $devKey->isEmpty()) {
|
||||
$allowed[] = $request->getHostname();
|
||||
}
|
||||
|
||||
@@ -215,12 +215,12 @@ Http::setResource('allowedHostnames', function (array $platform, Document $proje
|
||||
}
|
||||
|
||||
/* Allow the request origin of rule */
|
||||
if (!$rule->isEmpty() && !empty($rule->getAttribute('domain', ''))) {
|
||||
if (! $rule->isEmpty() && ! empty($rule->getAttribute('domain', ''))) {
|
||||
$allowed[] = $rule->getAttribute('domain', '');
|
||||
}
|
||||
|
||||
/* Allow the request origin if a dev key is found */
|
||||
if (!$devKey->isEmpty() && !empty($hostname)) {
|
||||
if (! $devKey->isEmpty() && ! empty($hostname)) {
|
||||
$allowed[] = $hostname;
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ Http::setResource('allowedHostnames', function (array $platform, Document $proje
|
||||
Http::setResource('allowedSchemes', function (array $platform, Document $project) {
|
||||
$allowed = [...($platform['schemas'] ?? [])];
|
||||
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
if (! $project->isEmpty() && $project->getId() !== 'console') {
|
||||
/* Add hardcoded schemes */
|
||||
$allowed[] = 'exp';
|
||||
$allowed[] = 'appwrite-callback-' . $project->getId();
|
||||
@@ -277,7 +277,7 @@ Http::setResource('rule', function (Request $request, Database $dbForPlatform, D
|
||||
|
||||
// Temporary implementation until custom wildcard domains are an official feature
|
||||
// Allow trusted projects; Used for Console (website) previews
|
||||
if (!$permitsCurrentProject && !$rule->isEmpty() && !empty($rule->getAttribute('projectId', ''))) {
|
||||
if (! $permitsCurrentProject && ! $rule->isEmpty() && ! empty($rule->getAttribute('projectId', ''))) {
|
||||
$trustedProjects = [];
|
||||
foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) {
|
||||
if (empty($trustedProject)) {
|
||||
@@ -290,7 +290,7 @@ Http::setResource('rule', function (Request $request, Database $dbForPlatform, D
|
||||
}
|
||||
}
|
||||
|
||||
if (!$permitsCurrentProject) {
|
||||
if (! $permitsCurrentProject) {
|
||||
return new Document();
|
||||
}
|
||||
|
||||
@@ -313,16 +313,18 @@ Http::setResource('cors', function (array $allowedHostnames) {
|
||||
}, ['allowedHostnames']);
|
||||
|
||||
Http::setResource('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
|
||||
if (!$devKey->isEmpty()) {
|
||||
if (! $devKey->isEmpty()) {
|
||||
return new URL();
|
||||
}
|
||||
|
||||
return new Origin($allowedHostnames, $allowedSchemes);
|
||||
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
|
||||
|
||||
Http::setResource('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
|
||||
if (!$devKey->isEmpty()) {
|
||||
if (! $devKey->isEmpty()) {
|
||||
return new URL();
|
||||
}
|
||||
|
||||
return new Redirect($allowedHostnames, $allowedSchemes);
|
||||
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
|
||||
|
||||
@@ -346,12 +348,11 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
* overwriting the previous value.
|
||||
* 7. If account API key is passed, use user of the account API key as long as user ID header matches too
|
||||
*/
|
||||
|
||||
$authorization->setDefaultStatus(true);
|
||||
|
||||
$store->setKey('a_session_' . $project->getId());
|
||||
|
||||
if (APP_MODE_ADMIN === $mode) {
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
$store->setKey('a_session_' . $console->getId());
|
||||
}
|
||||
|
||||
@@ -366,7 +367,7 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
|
||||
$sessionHeader = $request->getHeader('x-appwrite-session', '');
|
||||
|
||||
if (!empty($sessionHeader)) {
|
||||
if (! empty($sessionHeader)) {
|
||||
$store->decode($sessionHeader);
|
||||
}
|
||||
}
|
||||
@@ -386,14 +387,14 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
}
|
||||
|
||||
$user = null;
|
||||
if (APP_MODE_ADMIN === $mode) {
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
/** @var User $user */
|
||||
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
|
||||
} else {
|
||||
if ($project->isEmpty()) {
|
||||
$user = new User([]);
|
||||
} else {
|
||||
if (!empty($store->getProperty('id', ''))) {
|
||||
if (! empty($store->getProperty('id', ''))) {
|
||||
if ($project->getId() === 'console') {
|
||||
/** @var User $user */
|
||||
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
|
||||
@@ -406,16 +407,16 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
}
|
||||
|
||||
if (
|
||||
!$user ||
|
||||
! $user ||
|
||||
$user->isEmpty() // Check a document has been found in the DB
|
||||
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken)
|
||||
|| ! $user->sessionVerify($store->getProperty('secret', ''), $proofForToken)
|
||||
) { // Validate user has valid login token
|
||||
$user = new User([]);
|
||||
}
|
||||
|
||||
$authJWT = $request->getHeader('x-appwrite-jwt', '');
|
||||
if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication
|
||||
if (!$user->isEmpty()) {
|
||||
if (! empty($authJWT) && ! $project->isEmpty()) { // JWT authentication
|
||||
if (! $user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_JWT_AND_COOKIE_SET);
|
||||
}
|
||||
|
||||
@@ -427,7 +428,7 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
}
|
||||
|
||||
$jwtUserId = $payload['userId'] ?? '';
|
||||
if (!empty($jwtUserId)) {
|
||||
if (! empty($jwtUserId)) {
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
$user = $dbForPlatform->getDocument('users', $jwtUserId);
|
||||
} else {
|
||||
@@ -435,7 +436,7 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
}
|
||||
}
|
||||
$jwtSessionId = $payload['sessionId'] ?? '';
|
||||
if (!empty($jwtSessionId)) {
|
||||
if (! empty($jwtSessionId)) {
|
||||
if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token
|
||||
$user = new User([]);
|
||||
}
|
||||
@@ -445,22 +446,22 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
// Account based on account API key
|
||||
$accountKey = $request->getHeader('x-appwrite-key', '');
|
||||
$accountKeyUserId = $request->getHeader('x-appwrite-user', '');
|
||||
if (!empty($accountKeyUserId) && !empty($accountKey)) {
|
||||
if (!$user->isEmpty()) {
|
||||
if (! empty($accountKeyUserId) && ! empty($accountKey)) {
|
||||
if (! $user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
|
||||
}
|
||||
|
||||
$accountKeyUser = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId));
|
||||
if (!$accountKeyUser->isEmpty()) {
|
||||
if (! $accountKeyUser->isEmpty()) {
|
||||
$key = $accountKeyUser->find(
|
||||
key: 'secret',
|
||||
find: $accountKey,
|
||||
subject: 'keys'
|
||||
);
|
||||
|
||||
if (!empty($key)) {
|
||||
if (! empty($key)) {
|
||||
$expire = $key->getAttribute('expire');
|
||||
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
|
||||
if (! empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
|
||||
throw new Exception(Exception::ACCOUNT_KEY_EXPIRED);
|
||||
}
|
||||
|
||||
@@ -479,10 +480,9 @@ Http::setResource('project', function ($dbForPlatform, $request, $console, $auth
|
||||
/** @var Appwrite\Utopia\Request $request */
|
||||
/** @var Utopia\Database\Database $dbForPlatform */
|
||||
/** @var Utopia\Database\Document $console */
|
||||
|
||||
$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', ''));
|
||||
// Realtime channel "project" can send project=Query array
|
||||
if (!\is_string($projectId)) {
|
||||
if (! \is_string($projectId)) {
|
||||
$projectId = $request->getHeader('x-appwrite-project', '');
|
||||
}
|
||||
|
||||
@@ -503,7 +503,7 @@ Http::setResource('session', function (User $user, Store $store, Token $proofFor
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
$sessionId = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken);
|
||||
|
||||
if (!$sessionId) {
|
||||
if (! $sessionId) {
|
||||
return;
|
||||
}
|
||||
foreach ($sessions as $session) {
|
||||
@@ -513,7 +513,6 @@ Http::setResource('session', function (User $user, Store $store, Token $proofFor
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}, ['user', 'store', 'proofForToken']);
|
||||
|
||||
Http::setResource('store', function (): Store {
|
||||
@@ -537,12 +536,14 @@ Http::setResource('proofForPassword', function (): Password {
|
||||
Http::setResource('proofForToken', function (): Token {
|
||||
$token = new Token();
|
||||
$token->setHash(new Sha());
|
||||
|
||||
return $token;
|
||||
});
|
||||
|
||||
Http::setResource('proofForCode', function (): Code {
|
||||
$code = new Code();
|
||||
$code->setHash(new Sha());
|
||||
|
||||
return $code;
|
||||
});
|
||||
|
||||
@@ -554,7 +555,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, StatsUsage $queueForStatsUsage, 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) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
return $dbForPlatform;
|
||||
}
|
||||
@@ -619,9 +620,8 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
->from($queueForEvents)
|
||||
->trigger();
|
||||
|
||||
|
||||
/** Trigger webhooks events only if a project has them enabled */
|
||||
if (!empty($project->getAttribute('webhooks'))) {
|
||||
if (! empty($project->getAttribute('webhooks'))) {
|
||||
$queueForWebhooks
|
||||
->from($queueForEvents)
|
||||
->trigger();
|
||||
@@ -640,7 +640,6 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
*/
|
||||
$functionsEventsCacheListener = function (string $event, Document $document, Document $project, Database $dbForProject) {
|
||||
|
||||
|
||||
if ($document->getCollection() !== 'functions') {
|
||||
return;
|
||||
}
|
||||
@@ -662,7 +661,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
$dbForProject->getCache()->purge($cacheKey);
|
||||
};
|
||||
|
||||
$usageDatabaseListener = function (string $event, Document $document, StatsUsage $queueForStatsUsage) {
|
||||
$usageDatabaseListener = function (string $event, Document $document, UsageContext $usage) {
|
||||
$value = 1;
|
||||
|
||||
switch ($event) {
|
||||
@@ -682,81 +681,78 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
|
||||
switch (true) {
|
||||
case $document->getCollection() === 'teams':
|
||||
$queueForStatsUsage->addMetric(METRIC_TEAMS, $value); // per project
|
||||
$usage->addMetric(METRIC_TEAMS, $value); // per project
|
||||
break;
|
||||
case $document->getCollection() === 'users':
|
||||
$queueForStatsUsage->addMetric(METRIC_USERS, $value); // per project
|
||||
$usage->addMetric(METRIC_USERS, $value); // per project
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage->addReduce($document);
|
||||
$usage->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case $document->getCollection() === 'sessions': // sessions
|
||||
$queueForStatsUsage->addMetric(METRIC_SESSIONS, $value); //per project
|
||||
$usage->addMetric(METRIC_SESSIONS, $value); // per project
|
||||
break;
|
||||
case $document->getCollection() === 'databases': // databases
|
||||
$queueForStatsUsage->addMetric(METRIC_DATABASES, $value); // per project
|
||||
$usage->addMetric(METRIC_DATABASES, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage->addReduce($document);
|
||||
$usage->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
|
||||
case str_starts_with($document->getCollection(), 'database_') && ! str_contains($document->getCollection(), 'collection'): // collections
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addMetric(METRIC_COLLECTIONS, $value) // per project
|
||||
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value);
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage->addReduce($document);
|
||||
$usage->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): //documents
|
||||
case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): // documents
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$databaseInternalId = $parts[1] ?? 0;
|
||||
$collectionInternalId = $parts[3] ?? 0;
|
||||
$queueForStatsUsage
|
||||
$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
|
||||
break;
|
||||
case $document->getCollection() === 'buckets': //buckets
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_BUCKETS, $value); // per project
|
||||
case $document->getCollection() === 'buckets': // buckets
|
||||
$usage->addMetric(METRIC_BUCKETS, $value); // per project
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case str_starts_with($document->getCollection(), 'bucket_'): // files
|
||||
$parts = explode('_', $document->getCollection());
|
||||
$bucketInternalId = $parts[1];
|
||||
$queueForStatsUsage
|
||||
$bucketInternalId = $parts[1];
|
||||
$usage
|
||||
->addMetric(METRIC_FILES, $value) // per project
|
||||
->addMetric(METRIC_FILES_STORAGE, $document->getAttribute('sizeOriginal') * $value) // per project
|
||||
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES), $value) // per bucket
|
||||
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES_STORAGE), $document->getAttribute('sizeOriginal') * $value); // per bucket
|
||||
break;
|
||||
case $document->getCollection() === 'functions':
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_FUNCTIONS, $value); // per project
|
||||
$usage->addMetric(METRIC_FUNCTIONS, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case $document->getCollection() === 'sites':
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_SITES, $value); // per project
|
||||
$usage->addMetric(METRIC_SITES, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case $document->getCollection() === 'deployments':
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addMetric(METRIC_DEPLOYMENTS, $value) // per project
|
||||
->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $value) // per project
|
||||
->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS), $value) // per function
|
||||
@@ -776,30 +772,27 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
$queueForWebhooks = new Webhook($publisherWebhooks);
|
||||
$queueForRealtime = new Realtime();
|
||||
|
||||
|
||||
$database
|
||||
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
|
||||
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
|
||||
->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
|
||||
->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
|
||||
->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
|
||||
->on(Database::EVENT_DOCUMENT_CREATE, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener(
|
||||
$project,
|
||||
$document,
|
||||
$response,
|
||||
$queueForEventsClone->from($queueForEvents),
|
||||
$queueForFunctions->from($queueForEvents),
|
||||
$queueForWebhooks->from($queueForEvents),
|
||||
$queueForRealtime->from($queueForEvents)
|
||||
))
|
||||
->on(Database::EVENT_DOCUMENT_CREATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
|
||||
->on(Database::EVENT_DOCUMENT_UPDATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
|
||||
->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
|
||||
;
|
||||
|
||||
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage))
|
||||
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage))
|
||||
->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage))
|
||||
->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage))
|
||||
->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage))
|
||||
->on(Database::EVENT_DOCUMENT_CREATE, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener(
|
||||
$project,
|
||||
$document,
|
||||
$response,
|
||||
$queueForEventsClone->from($queueForEvents),
|
||||
$queueForFunctions->from($queueForEvents),
|
||||
$queueForWebhooks->from($queueForEvents),
|
||||
$queueForRealtime->from($queueForEvents)
|
||||
))
|
||||
->on(Database::EVENT_DOCUMENT_CREATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
|
||||
->on(Database::EVENT_DOCUMENT_UPDATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
|
||||
->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', 'queueForStatsUsage', 'authorization']);
|
||||
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization']);
|
||||
|
||||
Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
|
||||
@@ -848,8 +841,7 @@ Http::setResource('getProjectDB', function (Group $pools, Database $dbForPlatfor
|
||||
->setMetadata('project', $project->getId())
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES)
|
||||
->setDocumentType('users', User::class)
|
||||
;
|
||||
->setDocumentType('users', User::class);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
@@ -869,6 +861,7 @@ Http::setResource('getProjectDB', function (Group $pools, Database $dbForPlatfor
|
||||
if (isset($databases[$dsn->getHost()])) {
|
||||
$database = $databases[$dsn->getHost()];
|
||||
$configure($database);
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
@@ -885,8 +878,9 @@ Http::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizati
|
||||
$database = null;
|
||||
|
||||
return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) {
|
||||
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
if ($database !== null && $project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant((int) $project->getSequence());
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
@@ -902,7 +896,7 @@ Http::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizati
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
|
||||
// set tenant
|
||||
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
if ($project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant((int) $project->getSequence());
|
||||
}
|
||||
|
||||
@@ -912,6 +906,7 @@ Http::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizati
|
||||
|
||||
Http::setResource('audit', function ($dbForProject) {
|
||||
$adapter = new AdapterDatabase($dbForProject);
|
||||
|
||||
return new Audit($adapter);
|
||||
}, ['dbForProject']);
|
||||
|
||||
@@ -927,6 +922,7 @@ Http::setResource('cache', function (Group $pools, Telemetry $telemetry) {
|
||||
|
||||
$cache = new Cache(new Sharding($adapters));
|
||||
$cache->setTelemetry($telemetry);
|
||||
|
||||
return $cache;
|
||||
}, ['pools', 'telemetry']);
|
||||
|
||||
@@ -972,9 +968,9 @@ Http::setResource('deviceForBuilds', function ($project, Telemetry $telemetry) {
|
||||
|
||||
function getDevice(string $root, string $connection = ''): Device
|
||||
{
|
||||
$connection = !empty($connection) ? $connection : System::getEnv('_APP_CONNECTIONS_STORAGE', '');
|
||||
$connection = ! empty($connection) ? $connection : System::getEnv('_APP_CONNECTIONS_STORAGE', '');
|
||||
|
||||
if (!empty($connection)) {
|
||||
if (! empty($connection)) {
|
||||
$acl = 'private';
|
||||
$device = Storage::DEVICE_LOCAL;
|
||||
$accessKey = '';
|
||||
@@ -996,8 +992,9 @@ function getDevice(string $root, string $connection = ''): Device
|
||||
|
||||
switch ($device) {
|
||||
case Storage::DEVICE_S3:
|
||||
if (!empty($url)) {
|
||||
$bucketRoot = (!empty($bucket) ? $bucket . '/' : '') . \ltrim($root, '/');
|
||||
if (! empty($url)) {
|
||||
$bucketRoot = (! empty($bucket) ? $bucket . '/' : '') . \ltrim($root, '/');
|
||||
|
||||
return new S3($bucketRoot, $accessKey, $accessSecret, $url, $region, $acl);
|
||||
} else {
|
||||
return new AWS($root, $accessKey, $accessSecret, $bucket, $region, $acl);
|
||||
@@ -1006,6 +1003,7 @@ function getDevice(string $root, string $connection = ''): Device
|
||||
case STORAGE::DEVICE_DO_SPACES:
|
||||
$device = new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl);
|
||||
$device->setHttpVersion(S3::HTTP_VERSION_1_1);
|
||||
|
||||
return $device;
|
||||
case Storage::DEVICE_BACKBLAZE:
|
||||
return new Backblaze($root, $accessKey, $accessSecret, $bucket, $region, $acl);
|
||||
@@ -1029,8 +1027,9 @@ function getDevice(string $root, string $connection = ''): Device
|
||||
$s3Bucket = System::getEnv('_APP_STORAGE_S3_BUCKET', '');
|
||||
$s3Acl = 'private';
|
||||
$s3EndpointUrl = System::getEnv('_APP_STORAGE_S3_ENDPOINT', '');
|
||||
if (!empty($s3EndpointUrl)) {
|
||||
$bucketRoot = (!empty($s3Bucket) ? $s3Bucket . '/' : '') . \ltrim($root, '/');
|
||||
if (! empty($s3EndpointUrl)) {
|
||||
$bucketRoot = (! empty($s3Bucket) ? $s3Bucket . '/' : '') . \ltrim($root, '/');
|
||||
|
||||
return new S3($bucketRoot, $s3AccessKey, $s3SecretKey, $s3EndpointUrl, $s3Region, $s3Acl);
|
||||
} else {
|
||||
return new AWS($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl);
|
||||
@@ -1044,6 +1043,7 @@ function getDevice(string $root, string $connection = ''): Device
|
||||
$doSpacesAcl = 'private';
|
||||
$device = new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl);
|
||||
$device->setHttpVersion(S3::HTTP_VERSION_1_1);
|
||||
|
||||
return $device;
|
||||
case Storage::DEVICE_BACKBLAZE:
|
||||
$backblazeAccessKey = System::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', '');
|
||||
@@ -1051,6 +1051,7 @@ function getDevice(string $root, string $connection = ''): Device
|
||||
$backblazeRegion = System::getEnv('_APP_STORAGE_BACKBLAZE_REGION', '');
|
||||
$backblazeBucket = System::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', '');
|
||||
$backblazeAcl = 'private';
|
||||
|
||||
return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl);
|
||||
case Storage::DEVICE_LINODE:
|
||||
$linodeAccessKey = System::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', '');
|
||||
@@ -1058,6 +1059,7 @@ function getDevice(string $root, string $connection = ''): Device
|
||||
$linodeRegion = System::getEnv('_APP_STORAGE_LINODE_REGION', '');
|
||||
$linodeBucket = System::getEnv('_APP_STORAGE_LINODE_BUCKET', '');
|
||||
$linodeAcl = 'private';
|
||||
|
||||
return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl);
|
||||
case Storage::DEVICE_WASABI:
|
||||
$wasabiAccessKey = System::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', '');
|
||||
@@ -1065,6 +1067,7 @@ function getDevice(string $root, string $connection = ''): Device
|
||||
$wasabiRegion = System::getEnv('_APP_STORAGE_WASABI_REGION', '');
|
||||
$wasabiBucket = System::getEnv('_APP_STORAGE_WASABI_BUCKET', '');
|
||||
$wasabiAcl = 'private';
|
||||
|
||||
return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl);
|
||||
}
|
||||
}
|
||||
@@ -1091,7 +1094,6 @@ Http::setResource('passwordsDictionary', function ($register) {
|
||||
return $register->get('passwordsDictionary');
|
||||
}, ['register']);
|
||||
|
||||
|
||||
Http::setResource('servers', function () {
|
||||
$platforms = Config::getParam('sdks');
|
||||
$server = $platforms[APP_SDK_PLATFORM_SERVER];
|
||||
@@ -1199,16 +1201,17 @@ Http::setResource('gitHub', function (Cache $cache) {
|
||||
}, ['cache']);
|
||||
|
||||
Http::setResource('requestTimestamp', function ($request) {
|
||||
//TODO: Move this to the Request class itself
|
||||
// TODO: Move this to the Request class itself
|
||||
$timestampHeader = $request->getHeader('x-appwrite-timestamp');
|
||||
$requestTimestamp = null;
|
||||
if (!empty($timestampHeader)) {
|
||||
if (! empty($timestampHeader)) {
|
||||
try {
|
||||
$requestTimestamp = new \DateTime($timestampHeader);
|
||||
} catch (\Throwable $e) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid X-Appwrite-Timestamp header value');
|
||||
}
|
||||
}
|
||||
|
||||
return $requestTimestamp;
|
||||
}, ['request']);
|
||||
|
||||
@@ -1225,13 +1228,13 @@ Http::setResource('devKey', function (Request $request, Document $project, array
|
||||
|
||||
// Check if given key match project's development keys
|
||||
$key = $project->find('secret', $devKey, 'devKeys');
|
||||
if (!$key) {
|
||||
if (! $key) {
|
||||
return new Document([]);
|
||||
}
|
||||
|
||||
// check expiration
|
||||
$expire = $key->getAttribute('expire');
|
||||
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
|
||||
if (! empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
|
||||
return new Document([]);
|
||||
}
|
||||
|
||||
@@ -1239,7 +1242,9 @@ Http::setResource('devKey', function (Request $request, Document $project, array
|
||||
$accessedAt = $key->getAttribute('accessedAt', 0);
|
||||
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
|
||||
$key->setAttribute('accessedAt', DatabaseDateTime::now());
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
|
||||
'accessedAt' => $key->getAttribute('accessedAt')
|
||||
])));
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
|
||||
@@ -1250,13 +1255,16 @@ Http::setResource('devKey', function (Request $request, Document $project, array
|
||||
if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) {
|
||||
$sdks = $key->getAttribute('sdks', []);
|
||||
|
||||
if (!in_array($sdk, $sdks)) {
|
||||
if (! in_array($sdk, $sdks)) {
|
||||
$sdks[] = $sdk;
|
||||
$key->setAttribute('sdks', $sdks);
|
||||
|
||||
/** Update access time as well */
|
||||
$key->setAttribute('accessedAt', DatabaseDateTime::now());
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
|
||||
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
|
||||
'sdks' => $key->getAttribute('sdks'),
|
||||
'accessedAt' => $key->getAttribute('accessedAt')
|
||||
])));
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
@@ -1270,7 +1278,7 @@ Http::setResource('team', function (Document $project, Database $dbForPlatform,
|
||||
$teamInternalId = $project->getAttribute('teamInternalId', '');
|
||||
} else {
|
||||
$route = $utopia->match($request);
|
||||
$path = !empty($route) ? $route->getPath() : $request->getURI();
|
||||
$path = ! empty($route) ? $route->getPath() : $request->getURI();
|
||||
$orgHeader = $request->getHeader('x-appwrite-organization', '');
|
||||
if (str_starts_with($path, '/v1/projects/:projectId')) {
|
||||
$uri = $request->getURI();
|
||||
@@ -1285,8 +1293,9 @@ Http::setResource('team', function (Document $project, Database $dbForPlatform,
|
||||
}
|
||||
|
||||
$team = $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $teamId));
|
||||
|
||||
return $team;
|
||||
} elseif (!empty($orgHeader)) {
|
||||
} elseif (! empty($orgHeader)) {
|
||||
return $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $orgHeader));
|
||||
}
|
||||
}
|
||||
@@ -1316,13 +1325,13 @@ Http::setResource('previewHostname', function (Request $request, ?Key $apiKey) {
|
||||
|
||||
if (Http::isDevelopment()) {
|
||||
$allowed = true;
|
||||
} elseif (!\is_null($apiKey) && $apiKey->getHostnameOverride() === true) {
|
||||
} elseif (! \is_null($apiKey) && $apiKey->getHostnameOverride() === true) {
|
||||
$allowed = true;
|
||||
}
|
||||
|
||||
if ($allowed) {
|
||||
$host = $request->getQuery('appwrite-hostname', $request->getHeader('x-appwrite-hostname', '')) ?? '';
|
||||
if (!empty($host)) {
|
||||
if (! empty($host)) {
|
||||
return $host;
|
||||
}
|
||||
}
|
||||
@@ -1343,19 +1352,19 @@ Http::setResource('apiKey', function (Request $request, Document $project, Docum
|
||||
$organizationHeader = $request->getHeader('x-appwrite-organization');
|
||||
$projectHeader = $request->getHeader('x-appwrite-project');
|
||||
|
||||
if (!empty($key->getProjectId())) {
|
||||
if (! empty($key->getProjectId())) {
|
||||
if (empty($projectHeader) || $projectHeader !== $key->getProjectId()) {
|
||||
throw new Exception(Exception::PROJECT_ID_MISSING);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($key->getUserId())) {
|
||||
if (! empty($key->getUserId())) {
|
||||
if (empty($userHeader) || $userHeader !== $key->getUserId()) {
|
||||
throw new Exception(Exception::USER_ID_MISSING);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($key->getTeamId())) {
|
||||
if (! empty($key->getTeamId())) {
|
||||
if (empty($organizationHeader) || $organizationHeader !== $key->getTeamId()) {
|
||||
throw new Exception(Exception::ORGANIZATION_ID_MISSING);
|
||||
}
|
||||
@@ -1369,7 +1378,7 @@ Http::setResource('executor', fn () => new Executor());
|
||||
Http::setResource('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
|
||||
$tokenJWT = $request->getParam('token');
|
||||
|
||||
if (!empty($tokenJWT) && !$project->isEmpty()) { // JWT authentication
|
||||
if (! empty($tokenJWT) && ! $project->isEmpty()) { // JWT authentication
|
||||
// Use a large but reasonable maxAge to avoid auto-exp when token has no expiry
|
||||
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), RESOURCE_TOKEN_ALGORITHM, RESOURCE_TOKEN_MAX_AGE, RESOURCE_TOKEN_LEEWAY); // Instantiate with key, algo, maxAge and leeway.
|
||||
|
||||
@@ -1413,7 +1422,9 @@ Http::setResource('resourceToken', function ($project, $dbForProject, $request,
|
||||
$accessedAt = $token->getAttribute('accessedAt', 0);
|
||||
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) {
|
||||
$token->setAttribute('accessedAt', DatabaseDateTime::now());
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token));
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), new Document([
|
||||
'accessedAt' => $token->getAttribute('accessedAt')
|
||||
])));
|
||||
}
|
||||
|
||||
return new Document([
|
||||
@@ -1427,6 +1438,7 @@ Http::setResource('resourceToken', function ($project, $dbForProject, $request,
|
||||
default => throw new Exception(Exception::TOKEN_RESOURCE_TYPE_INVALID),
|
||||
};
|
||||
}
|
||||
|
||||
return new Document([]);
|
||||
}, ['project', 'dbForProject', 'request', 'authorization']);
|
||||
|
||||
|
||||
+6
-1
@@ -5,4 +5,9 @@ use Utopia\Span\Span;
|
||||
use Utopia\Span\Storage;
|
||||
|
||||
Span::setStorage(new Storage\Coroutine());
|
||||
Span::addExporter(new Exporter\Pretty());
|
||||
Span::addExporter(new Exporter\Pretty(), function (Span $span): bool {
|
||||
if (\str_starts_with($span->getAction(), 'listener.')) {
|
||||
return $span->getError() !== null;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Bus\Listeners\Log;
|
||||
use Appwrite\Bus\Listeners\Usage;
|
||||
|
||||
return [
|
||||
new Log(),
|
||||
new Usage(),
|
||||
];
|
||||
+169
-52
@@ -224,6 +224,13 @@ if (!function_exists('getTelemetry')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('triggerStats')) {
|
||||
function triggerStats(array $event, string $projectId): void
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$realtime = getRealtime();
|
||||
|
||||
/**
|
||||
@@ -248,48 +255,62 @@ $adapter
|
||||
|
||||
$server = new Server($adapter);
|
||||
|
||||
$logError = function (Throwable $error, string $action) use ($register) {
|
||||
$logger = $register->get('realtimeLogger');
|
||||
// Allows overriding
|
||||
if (!function_exists('logError')) {
|
||||
function logError(Throwable $error, string $action, array $tags = [], ?Document $project = null, ?Document $user = null, ?Authorization $authorization = null): void
|
||||
{
|
||||
global $register;
|
||||
|
||||
if ($logger && !$error instanceof Exception) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$logger = $register->get('realtimeLogger');
|
||||
|
||||
$log = new Log();
|
||||
$log->setNamespace("realtime");
|
||||
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
if ($logger && !$error instanceof Exception) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log = new Log();
|
||||
$log->setNamespace("realtime");
|
||||
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('projectId', $project?->getId() ?: 'n/a');
|
||||
$log->addTag('userId', $user?->getId() ?: 'n/a');
|
||||
|
||||
$log->setAction($action);
|
||||
foreach ($tags as $key => $value) {
|
||||
$log->addTag($key, $value ?: 'n/a');
|
||||
}
|
||||
|
||||
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addExtra('detailedTrace', $error->getTrace());
|
||||
$log->addExtra('roles', $authorization?->getRoles() ?? []);
|
||||
|
||||
try {
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Error log pushed with status code: ' . $responseCode);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Error pushing log: ' . $th->getMessage());
|
||||
$log->setAction($action);
|
||||
|
||||
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
try {
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Error log pushed with status code: ' . $responseCode);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Error pushing log: ' . $th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
Console::error('[Error] Type: ' . get_class($error));
|
||||
Console::error('[Error] Message: ' . $error->getMessage());
|
||||
Console::error('[Error] File: ' . $error->getFile());
|
||||
Console::error('[Error] Line: ' . $error->getLine());
|
||||
}
|
||||
}
|
||||
|
||||
Console::error('[Error] Type: ' . get_class($error));
|
||||
Console::error('[Error] Message: ' . $error->getMessage());
|
||||
Console::error('[Error] File: ' . $error->getFile());
|
||||
Console::error('[Error] Line: ' . $error->getLine());
|
||||
};
|
||||
$server->error(logError(...));
|
||||
|
||||
$server->error($logError);
|
||||
|
||||
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument, $logError) {
|
||||
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument) {
|
||||
sleep(5); // wait for the initial database schema to be ready
|
||||
Console::success('Server started successfully');
|
||||
|
||||
@@ -326,7 +347,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
|
||||
*/
|
||||
// TODO: Remove this if check once it doesn't cause issues for cloud
|
||||
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') {
|
||||
Timer::tick(5000, function () use ($register, $stats, &$statsDocument, $logError) {
|
||||
Timer::tick(5000, function () use ($register, $stats, &$statsDocument) {
|
||||
$payload = [];
|
||||
foreach ($stats as $projectId => $value) {
|
||||
$payload[$projectId] = $stats->get($projectId, 'connectionsTotal');
|
||||
@@ -342,15 +363,18 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
|
||||
->setAttribute('timestamp', DateTime::now())
|
||||
->setAttribute('value', json_encode($payload));
|
||||
|
||||
$database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
|
||||
$database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), new Document([
|
||||
'timestamp' => $statsDocument->getAttribute('timestamp'),
|
||||
'value' => $statsDocument->getAttribute('value')
|
||||
])));
|
||||
} catch (Throwable $th) {
|
||||
$logError($th, "updateWorkerDocument");
|
||||
logError($th, "updateWorkerDocument");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, $logError) {
|
||||
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime) {
|
||||
Console::success('Worker ' . $workerId . ' started successfully');
|
||||
|
||||
$telemetry = getTelemetry($workerId);
|
||||
@@ -362,7 +386,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
$attempts = 0;
|
||||
$start = time();
|
||||
|
||||
Timer::tick(5000, function () use ($server, $register, $realtime, $stats, $logError) {
|
||||
Timer::tick(5000, function () use ($server, $register, $realtime, $stats) {
|
||||
/**
|
||||
* Sending current connections to project channels on the console project every 5 seconds.
|
||||
*/
|
||||
@@ -531,24 +555,45 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
$outboundBytes = 0;
|
||||
|
||||
foreach ($groups as $group) {
|
||||
$data = $event['data'];
|
||||
$data['subscriptions'] = $group['subscriptions'];
|
||||
|
||||
$server->send($group['ids'], json_encode([
|
||||
$payloadJson = json_encode([
|
||||
'type' => 'event',
|
||||
'data' => $data
|
||||
]));
|
||||
$total += count($group['ids']);
|
||||
]);
|
||||
|
||||
$server->send($group['ids'], $payloadJson);
|
||||
|
||||
$count = count($group['ids']);
|
||||
$total += $count;
|
||||
$outboundBytes += strlen($payloadJson) * $count;
|
||||
}
|
||||
|
||||
if ($total > 0) {
|
||||
$register->get('telemetry.messageSentCounter')->add($total);
|
||||
$stats->incr($event['project'], 'messages', $total);
|
||||
|
||||
$projectId = $event['project'] ?? null;
|
||||
|
||||
if (!empty($projectId)) {
|
||||
$metrics = [
|
||||
METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT => $total,
|
||||
];
|
||||
|
||||
if ($outboundBytes > 0) {
|
||||
$metrics[METRIC_REALTIME_OUTBOUND] = $outboundBytes;
|
||||
}
|
||||
|
||||
triggerStats($metrics, $projectId);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Throwable $th) {
|
||||
$logError($th, "pubSubConnection");
|
||||
logError($th, "pubSubConnection");
|
||||
|
||||
Console::error('Pub/sub error: ' . $th->getMessage());
|
||||
$attempts++;
|
||||
@@ -560,7 +605,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
Console::error('Failed to restart pub/sub...');
|
||||
});
|
||||
|
||||
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $logError) {
|
||||
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) {
|
||||
$app = new Http('UTC');
|
||||
$request = new Request($request);
|
||||
$response = new Response(new SwooleResponse());
|
||||
@@ -571,6 +616,10 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
Http::setResource('request', fn () => $request);
|
||||
Http::setResource('response', fn () => $response);
|
||||
|
||||
$project = null;
|
||||
$logUser = null;
|
||||
$authorization = null;
|
||||
|
||||
try {
|
||||
/** @var Document $project */
|
||||
$project = $app->getResource('project');
|
||||
@@ -591,8 +640,15 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
|
||||
$projectRegion = $project->getAttribute('region', '');
|
||||
$currentRegion = System::getEnv('_APP_REGION', 'default');
|
||||
if (!empty($projectRegion) && $projectRegion !== $currentRegion) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Project is not accessible in this region. Please make sure you are using the correct endpoint');
|
||||
}
|
||||
|
||||
$timelimit = $app->getResource('timelimit');
|
||||
$user = $app->getResource('user'); /** @var User $user */
|
||||
$logUser = $user;
|
||||
|
||||
/*
|
||||
* Abuse Check
|
||||
@@ -610,6 +666,12 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many requests');
|
||||
}
|
||||
|
||||
$rawSize = $request->getSize();
|
||||
|
||||
triggerStats([
|
||||
METRIC_REALTIME_INBOUND => $rawSize,
|
||||
], $project->getId());
|
||||
|
||||
/*
|
||||
* Validate Client Domain - Check to avoid CSRF attack.
|
||||
* Adding Appwrite API domains to allow XDOMAIN communication.
|
||||
@@ -664,14 +726,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
|
||||
$user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
|
||||
|
||||
$server->send([$connection], json_encode([
|
||||
$connectedPayloadJson = json_encode([
|
||||
'type' => 'connected',
|
||||
'data' => [
|
||||
'channels' => $names,
|
||||
'subscriptions' => $mapping,
|
||||
'user' => $user
|
||||
]
|
||||
]));
|
||||
]);
|
||||
|
||||
$server->send([$connection], $connectedPayloadJson);
|
||||
|
||||
$register->get('telemetry.connectionCounter')->add(1);
|
||||
$register->get('telemetry.connectionCreatedCounter')->add(1);
|
||||
@@ -682,12 +746,18 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
]);
|
||||
$stats->incr($project->getId(), 'connections');
|
||||
$stats->incr($project->getId(), 'connectionsTotal');
|
||||
|
||||
$connectedOutboundBytes = \strlen($connectedPayloadJson);
|
||||
|
||||
triggerStats([METRIC_REALTIME_CONNECTIONS => 1, METRIC_REALTIME_OUTBOUND => $connectedOutboundBytes], $project->getId());
|
||||
|
||||
|
||||
} catch (Throwable $th) {
|
||||
$logError($th, "initServer");
|
||||
logError($th, 'realtime', project: $project, user: $logUser, authorization: $authorization);
|
||||
|
||||
// Handle SQL error code is 'HY000'
|
||||
$code = $th->getCode();
|
||||
if (!is_int($code)) {
|
||||
if (!\is_int($code)) {
|
||||
$code = 500;
|
||||
}
|
||||
|
||||
@@ -718,8 +788,12 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
}
|
||||
});
|
||||
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId, $logError) {
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
|
||||
$project = null;
|
||||
$authorization = null;
|
||||
|
||||
try {
|
||||
$rawSize = \strlen($message);
|
||||
$response = new Response(new SwooleResponse());
|
||||
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
|
||||
|
||||
@@ -732,7 +806,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
$database = getConsoleDB();
|
||||
$database->setAuthorization($authorization);
|
||||
|
||||
if ($projectId !== 'console') {
|
||||
if (!empty($projectId) && $projectId !== 'console') {
|
||||
$project = $authorization->skip(fn () => $database->getDocument('projects', $projectId));
|
||||
|
||||
$database = getProjectDB($project);
|
||||
@@ -758,17 +832,41 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many messages.');
|
||||
}
|
||||
|
||||
// Record realtime inbound bytes for this project
|
||||
if ($project !== null && !$project->isEmpty()) {
|
||||
triggerStats([
|
||||
METRIC_REALTIME_INBOUND => $rawSize,
|
||||
], $project->getId());
|
||||
}
|
||||
|
||||
$message = json_decode($message, true);
|
||||
|
||||
if (is_null($message) || (!array_key_exists('type', $message) && !array_key_exists('data', $message))) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message format is not valid.');
|
||||
}
|
||||
|
||||
// Ping does not require project context; other messages do (e.g. after unsubscribe during auth)
|
||||
if (empty($projectId) && ($message['type'] ?? '') !== 'ping') {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing project context. Reconnect to the project first.');
|
||||
}
|
||||
|
||||
switch ($message['type']) {
|
||||
case 'ping':
|
||||
$server->send([$connection], json_encode([
|
||||
$pongPayloadJson = json_encode([
|
||||
'type' => 'pong'
|
||||
]));
|
||||
]);
|
||||
|
||||
$server->send([$connection], $pongPayloadJson);
|
||||
|
||||
if ($project !== null && !$project->isEmpty()) {
|
||||
$pongOutboundBytes = \strlen($pongPayloadJson);
|
||||
|
||||
if ($pongOutboundBytes > 0) {
|
||||
triggerStats([
|
||||
METRIC_REALTIME_OUTBOUND => $pongOutboundBytes,
|
||||
], $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case 'authentication':
|
||||
@@ -829,14 +927,27 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
}
|
||||
|
||||
$user = $response->output($user, Response::MODEL_ACCOUNT);
|
||||
$server->send([$connection], json_encode([
|
||||
|
||||
$authResponsePayloadJson = json_encode([
|
||||
'type' => 'response',
|
||||
'data' => [
|
||||
'to' => 'authentication',
|
||||
'success' => true,
|
||||
'user' => $user
|
||||
]
|
||||
]));
|
||||
]);
|
||||
|
||||
$server->send([$connection], $authResponsePayloadJson);
|
||||
|
||||
if ($project !== null && !$project->isEmpty()) {
|
||||
$authOutboundBytes = \strlen($authResponsePayloadJson);
|
||||
|
||||
if ($authOutboundBytes > 0) {
|
||||
triggerStats([
|
||||
METRIC_REALTIME_OUTBOUND => $authOutboundBytes,
|
||||
], $project->getId());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -844,7 +955,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
|
||||
}
|
||||
} catch (Throwable $th) {
|
||||
$logError($th, "realtimeMessage");
|
||||
logError($th, 'realtimeMessage', project: $project, authorization: $authorization);
|
||||
$code = $th->getCode();
|
||||
if (!is_int($code)) {
|
||||
$code = 500;
|
||||
@@ -877,6 +988,12 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) {
|
||||
if (array_key_exists($connection, $realtime->connections)) {
|
||||
$stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal');
|
||||
$register->get('telemetry.connectionCounter')->add(-1);
|
||||
|
||||
$projectId = $realtime->connections[$connection]['projectId'];
|
||||
|
||||
triggerStats([
|
||||
METRIC_REALTIME_CONNECTIONS => -1,
|
||||
], $projectId);
|
||||
}
|
||||
$realtime->unsubscribe($connection);
|
||||
|
||||
|
||||
+35
-31
@@ -9,16 +9,16 @@ use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Execution;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Migration;
|
||||
use Appwrite\Event\Publisher\Usage as UsagePublisher;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\Screenshot;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Event\Webhook;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Executor\Executor;
|
||||
use Swoole\Runtime;
|
||||
@@ -43,6 +43,7 @@ use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Broker\Pool as BrokerPool;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
use Utopia\Queue\Server;
|
||||
use Utopia\Registry\Registry;
|
||||
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
|
||||
@@ -59,7 +60,8 @@ Server::setResource('register', fn () => $register);
|
||||
Server::setResource('authorization', function () {
|
||||
$authorization = new Authorization();
|
||||
$authorization->disable();
|
||||
return $authorization;
|
||||
|
||||
return $authorization;
|
||||
}, []);
|
||||
|
||||
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register, Authorization $authorization) {
|
||||
@@ -71,9 +73,7 @@ Server::setResource('dbForPlatform', function (Cache $cache, Registry $register,
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setNamespace('_console')
|
||||
->setDocumentType('users', User::class)
|
||||
;
|
||||
|
||||
->setDocumentType('users', User::class);
|
||||
|
||||
return $dbForPlatform;
|
||||
}, ['cache', 'register', 'authorization']);
|
||||
@@ -112,7 +112,7 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int)$project->getSequence())
|
||||
->setTenant((int) $project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
@@ -152,7 +152,7 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int)$project->getSequence())
|
||||
->setTenant((int) $project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
@@ -174,7 +174,7 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int)$project->getSequence())
|
||||
->setTenant((int) $project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
@@ -194,9 +194,11 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
|
||||
|
||||
Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
|
||||
$database = null;
|
||||
|
||||
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
|
||||
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant((int)$project->getSequence());
|
||||
if ($database !== null && $project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant((int) $project->getSequence());
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
@@ -212,8 +214,8 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authoriza
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
|
||||
|
||||
// set tenant
|
||||
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant((int)$project->getSequence());
|
||||
if ($project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
|
||||
$database->setTenant((int) $project->getSequence());
|
||||
}
|
||||
|
||||
return $database;
|
||||
@@ -228,6 +230,7 @@ Server::setResource('auditRetention', function (Document $project) {
|
||||
if ($project->getId() === 'console') {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
|
||||
}
|
||||
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
|
||||
}, ['project']);
|
||||
|
||||
@@ -253,7 +256,7 @@ Server::setResource('redis', function () {
|
||||
$pass = System::getEnv('_APP_REDIS_PASS', '');
|
||||
|
||||
$redis = new \Redis();
|
||||
@$redis->pconnect($host, (int)$port);
|
||||
@$redis->pconnect($host, (int) $port);
|
||||
if ($pass) {
|
||||
$redis->auth($pass);
|
||||
}
|
||||
@@ -270,7 +273,6 @@ Server::setResource('timelimit', function (\Redis $redis) {
|
||||
|
||||
Server::setResource('log', fn () => new Log());
|
||||
|
||||
|
||||
Server::setResource('publisher', function (Group $pools) {
|
||||
return new BrokerPool(publisher: $pools->get('publisher'));
|
||||
}, ['pools']);
|
||||
@@ -287,10 +289,6 @@ Server::setResource('publisherMigrations', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('publisherStatsUsage', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('publisherMessaging', function (BrokerPool $publisher) {
|
||||
return $publisher;
|
||||
}, ['publisher']);
|
||||
@@ -311,9 +309,13 @@ Server::setResource('consumerStatsUsage', function (BrokerPool $consumer) {
|
||||
return $consumer;
|
||||
}, ['consumer']);
|
||||
|
||||
Server::setResource('queueForStatsUsage', function (Publisher $publisher) {
|
||||
return new StatsUsage($publisher);
|
||||
}, ['publisher']);
|
||||
Server::setResource('usage', function () {
|
||||
return new Context();
|
||||
}, []);
|
||||
Server::setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
|
||||
$publisher,
|
||||
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
|
||||
), ['publisher']);
|
||||
|
||||
Server::setResource('queueForDatabase', function (Publisher $publisher) {
|
||||
return new EventDatabase($publisher);
|
||||
@@ -355,10 +357,6 @@ Server::setResource('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForExecutions', function (Publisher $publisher) {
|
||||
return new Execution($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForRealtime', function () {
|
||||
return new Realtime();
|
||||
}, []);
|
||||
@@ -488,11 +486,13 @@ Server::setResource('getAudit', function (Database $dbForPlatform, callable $get
|
||||
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
|
||||
if ($project->isEmpty() || $project->getId() === 'console') {
|
||||
$adapter = new AdapterDatabase($dbForPlatform);
|
||||
|
||||
return new UtopiaAudit($adapter);
|
||||
}
|
||||
|
||||
$dbForProject = $getProjectDB($project);
|
||||
$adapter = new AdapterDatabase($dbForProject);
|
||||
|
||||
return new UtopiaAudit($adapter);
|
||||
};
|
||||
}, ['dbForPlatform', 'getProjectDB']);
|
||||
@@ -509,7 +509,7 @@ $pools = $register->get('pools');
|
||||
$platform = new Appwrite();
|
||||
$args = $platform->getEnv('argv');
|
||||
|
||||
if (!isset($args[1])) {
|
||||
if (! isset($args[1])) {
|
||||
Console::error('Missing worker name');
|
||||
Console::exit(1);
|
||||
}
|
||||
@@ -534,14 +534,18 @@ try {
|
||||
'workersNum' => System::getEnv('_APP_WORKERS_NUM', 1),
|
||||
'connection' => $pools->get('consumer')->pop()->getResource(),
|
||||
'workerName' => strtolower($workerName) ?? null,
|
||||
'queueName' => $queueName
|
||||
'queueName' => $queueName,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
|
||||
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
|
||||
}
|
||||
|
||||
$worker = $platform->getWorker();
|
||||
|
||||
Server::setResource('bus', function ($register) use ($worker) {
|
||||
return $register->get('bus')->setResolver(fn (string $name) => $worker->getResource($name));
|
||||
}, ['register']);
|
||||
|
||||
$worker
|
||||
->error()
|
||||
->inject('error')
|
||||
@@ -550,11 +554,11 @@ $worker
|
||||
->inject('pools')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project, Authorization $authorization) use ($worker, $queueName) {
|
||||
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project, Authorization $authorization) use ($queueName) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
if ($logger) {
|
||||
$log->setNamespace("appwrite-worker");
|
||||
$log->setNamespace('appwrite-worker');
|
||||
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
exec php /usr/src/code/app/cli.php time-travel "$@"
|
||||
+7
-4
@@ -14,12 +14,13 @@
|
||||
"lint": "vendor/bin/pint --test --config pint.json",
|
||||
"format": "vendor/bin/pint --config pint.json",
|
||||
"bench": "vendor/bin/phpbench run --report=benchmark",
|
||||
"check": "./vendor/bin/phpstan analyse -c phpstan.neon"
|
||||
"analyze": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Appwrite\\": "src/Appwrite",
|
||||
"Executor\\": "src/Executor"
|
||||
"Executor\\": "src/Executor",
|
||||
"Utopia\\Bus\\": "src/Utopia/Bus"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
@@ -69,12 +70,13 @@
|
||||
"utopia-php/locale": "0.8.*",
|
||||
"utopia-php/logger": "0.6.*",
|
||||
"utopia-php/messaging": "0.20.*",
|
||||
"utopia-php/migration": "1.6.*",
|
||||
"utopia-php/migration": "1.7.*",
|
||||
"utopia-php/platform": "0.7.*",
|
||||
"utopia-php/pools": "1.*",
|
||||
"utopia-php/span": "1.1.*",
|
||||
"utopia-php/preloader": "0.2.*",
|
||||
"utopia-php/queue": "0.15.*",
|
||||
"utopia-php/servers": "0.2.5",
|
||||
"utopia-php/registry": "0.5.*",
|
||||
"utopia-php/storage": "1.0.*",
|
||||
"utopia-php/system": "0.10.*",
|
||||
@@ -97,8 +99,9 @@
|
||||
"brianium/paratest": "7.*",
|
||||
"phpunit/phpunit": "12.*",
|
||||
"swoole/ide-helper": "6.*",
|
||||
"phpstan/phpstan": "1.12.*",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"textalk/websocket": "1.5.*",
|
||||
"czproject/git-php": "4.*",
|
||||
"laravel/pint": "1.*",
|
||||
"phpbench/phpbench": "1.*"
|
||||
},
|
||||
|
||||
Generated
+101
-37
@@ -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": "1fb043a556550f62ab27a0aad0b9332a",
|
||||
"content-hash": "392d5bd2621b3eb57f8b1bf38d9e4bc3",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -2708,16 +2708,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client",
|
||||
"version": "v7.4.5",
|
||||
"version": "v7.4.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-client.git",
|
||||
"reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f"
|
||||
"reference": "1010624285470eb60e88ed10035102c75b4ea6af"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f",
|
||||
"reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f",
|
||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/1010624285470eb60e88ed10035102c75b4ea6af",
|
||||
"reference": "1010624285470eb60e88ed10035102c75b4ea6af",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2785,7 +2785,7 @@
|
||||
"http"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-client/tree/v7.4.5"
|
||||
"source": "https://github.com/symfony/http-client/tree/v7.4.7"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -2805,7 +2805,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-27T16:16:02+00:00"
|
||||
"time": "2026-03-05T11:16:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client-contracts",
|
||||
@@ -3850,16 +3850,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "5.3.4",
|
||||
"version": "5.3.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "ba1ee9cb2c7624d0fada782b285bd9958a07bbe5"
|
||||
"reference": "4920bb60afb98d4bd81f4d331765716ae1d40255"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/ba1ee9cb2c7624d0fada782b285bd9958a07bbe5",
|
||||
"reference": "ba1ee9cb2c7624d0fada782b285bd9958a07bbe5",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/4920bb60afb98d4bd81f4d331765716ae1d40255",
|
||||
"reference": "4920bb60afb98d4bd81f4d331765716ae1d40255",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3902,9 +3902,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/database/issues",
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.4"
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.8"
|
||||
},
|
||||
"time": "2026-02-24T00:37:36+00:00"
|
||||
"time": "2026-03-11T01:03:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -4267,16 +4267,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/framework",
|
||||
"version": "0.33.40",
|
||||
"version": "0.33.41",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/http.git",
|
||||
"reference": "0ba25e1282c6a2f849053f7ccf28d567c2c321b1"
|
||||
"reference": "0f3bf2377c867e547c929c3733b8224afee6ef06"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/0ba25e1282c6a2f849053f7ccf28d567c2c321b1",
|
||||
"reference": "0ba25e1282c6a2f849053f7ccf28d567c2c321b1",
|
||||
"url": "https://api.github.com/repos/utopia-php/http/zipball/0f3bf2377c867e547c929c3733b8224afee6ef06",
|
||||
"reference": "0f3bf2377c867e547c929c3733b8224afee6ef06",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4310,9 +4310,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/http/issues",
|
||||
"source": "https://github.com/utopia-php/http/tree/0.33.40"
|
||||
"source": "https://github.com/utopia-php/http/tree/0.33.41"
|
||||
},
|
||||
"time": "2026-02-19T13:00:08+00:00"
|
||||
"time": "2026-02-24T12:01:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
@@ -4517,16 +4517,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
"version": "1.6.1",
|
||||
"version": "1.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/migration.git",
|
||||
"reference": "c5c7544d02d2418536d41050794050132f247d62"
|
||||
"reference": "97583ae502e40621ea91a71de19d053c5ae2e558"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/c5c7544d02d2418536d41050794050132f247d62",
|
||||
"reference": "c5c7544d02d2418536d41050794050132f247d62",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/97583ae502e40621ea91a71de19d053c5ae2e558",
|
||||
"reference": "97583ae502e40621ea91a71de19d053c5ae2e558",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4566,9 +4566,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/migration/issues",
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.6.1"
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.7.0"
|
||||
},
|
||||
"time": "2026-02-17T05:49:48+00:00"
|
||||
"time": "2026-03-10T06:36:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/mongo",
|
||||
@@ -4684,16 +4684,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/pools",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/pools.git",
|
||||
"reference": "b7d8dd00306cdd8bf3ff6f1dc90caeaf27dabeb1"
|
||||
"reference": "74de7c5457a2c447f27e7ec4d72e8412a7d68c10"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/pools/zipball/b7d8dd00306cdd8bf3ff6f1dc90caeaf27dabeb1",
|
||||
"reference": "b7d8dd00306cdd8bf3ff6f1dc90caeaf27dabeb1",
|
||||
"url": "https://api.github.com/repos/utopia-php/pools/zipball/74de7c5457a2c447f27e7ec4d72e8412a7d68c10",
|
||||
"reference": "74de7c5457a2c447f27e7ec4d72e8412a7d68c10",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4731,9 +4731,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/pools/issues",
|
||||
"source": "https://github.com/utopia-php/pools/tree/1.0.2"
|
||||
"source": "https://github.com/utopia-php/pools/tree/1.0.3"
|
||||
},
|
||||
"time": "2026-01-28T13:12:36+00:00"
|
||||
"time": "2026-02-26T08:42:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/preloader",
|
||||
@@ -5580,6 +5580,70 @@
|
||||
],
|
||||
"time": "2026-02-06T10:53:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "czproject/git-php",
|
||||
"version": "v4.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/czproject/git-php.git",
|
||||
"reference": "1f1ecc92aea9ee31120f4f5b759f5aa947420b0a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/czproject/git-php/zipball/1f1ecc92aea9ee31120f4f5b759f5aa947420b0a",
|
||||
"reference": "1f1ecc92aea9ee31120f4f5b759f5aa947420b0a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "8.0 - 8.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"nette/tester": "^2.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jan Pecha",
|
||||
"email": "janpecha@email.cz"
|
||||
}
|
||||
],
|
||||
"description": "Library for work with Git repository in PHP.",
|
||||
"keywords": [
|
||||
"git"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/czproject/git-php/issues",
|
||||
"source": "https://github.com/czproject/git-php/tree/v4.6.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sponsors/janpecha",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://www.janpecha.cz/donate/git-php/",
|
||||
"type": "other"
|
||||
},
|
||||
{
|
||||
"url": "https://donate.stripe.com/7sIcO2a9maTSg2A9AA",
|
||||
"type": "stripe"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/czproject",
|
||||
"type": "thanks.dev"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-10T07:24:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/annotations",
|
||||
"version": "2.0.2",
|
||||
@@ -6432,15 +6496,15 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "1.12.32",
|
||||
"version": "2.1.40",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8",
|
||||
"reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b",
|
||||
"reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2|^8.0"
|
||||
"php": "^7.4|^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"phpstan/phpstan-shim": "*"
|
||||
@@ -6481,7 +6545,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-09-30T10:16:31+00:00"
|
||||
"time": "2026-02-23T15:04:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
@@ -9067,5 +9131,5 @@
|
||||
"platform-overrides": {
|
||||
"php": "8.3"
|
||||
},
|
||||
"plugin-api-version": "2.6.0"
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
# Dev tools for local development only.
|
||||
# This file is automatically loaded by `docker compose` alongside docker-compose.yml.
|
||||
# CI sets COMPOSE_FILE=docker-compose.yml explicitly, so these services are excluded from test runs.
|
||||
|
||||
services:
|
||||
appwrite-mongo-express:
|
||||
profiles: ["mongodb"]
|
||||
image: mongo-express
|
||||
container_name: appwrite-mongo-express
|
||||
networks:
|
||||
- appwrite
|
||||
ports:
|
||||
- "8082:8081"
|
||||
environment:
|
||||
ME_CONFIG_MONGODB_URL: "mongodb://root:${_APP_DB_ROOT_PASS}@appwrite-mongodb:27017/?replicaSet=rs0&directConnection=true"
|
||||
ME_CONFIG_BASICAUTH_USERNAME: ${_APP_DB_USER}
|
||||
ME_CONFIG_BASICAUTH_PASSWORD: ${_APP_DB_PASS}
|
||||
depends_on:
|
||||
- mongodb
|
||||
|
||||
adminer:
|
||||
image: adminer
|
||||
container_name: appwrite-adminer
|
||||
restart: always
|
||||
ports:
|
||||
- 9506:8080
|
||||
networks:
|
||||
- appwrite
|
||||
- gateway
|
||||
environment:
|
||||
- ADMINER_DESIGN=pepa-linha
|
||||
- ADMINER_DEFAULT_SERVER=mariadb
|
||||
- ADMINER_DEFAULT_USERNAME=root
|
||||
- ADMINER_DEFAULT_PASSWORD=rootsecretpassword
|
||||
- ADMINER_DEFAULT_DB=appwrite
|
||||
configs:
|
||||
- source: adminer-index.php
|
||||
target: /var/www/html/index.php
|
||||
mode: 0755
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.constraint-label-stack=appwrite"
|
||||
- "traefik.docker.network=gateway"
|
||||
- "traefik.http.services.appwrite_adminer.loadbalancer.server.port=8080"
|
||||
- "traefik.http.routers.appwrite_adminer_http.entrypoints=appwrite_web"
|
||||
- "traefik.http.routers.appwrite_adminer_http.rule=Host(`mysql.localhost`)"
|
||||
- "traefik.http.routers.appwrite_adminer_http.service=appwrite_adminer"
|
||||
- "traefik.http.routers.appwrite_adminer_https.entrypoints=appwrite_websecure"
|
||||
- "traefik.http.routers.appwrite_adminer_https.rule=Host(`mysql.localhost`)"
|
||||
- "traefik.http.routers.appwrite_adminer_https.service=appwrite_adminer"
|
||||
- "traefik.http.routers.appwrite_adminer_https.tls=true"
|
||||
|
||||
redis-insight:
|
||||
image: redis/redisinsight:latest
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
- gateway
|
||||
environment:
|
||||
- RI_PRE_SETUP_DATABASES_PATH=/mnt/connections.json
|
||||
configs:
|
||||
- source: redisinsight-connections.json
|
||||
target: /mnt/connections.json
|
||||
mode: 0755
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.constraint-label-stack=appwrite"
|
||||
- "traefik.docker.network=gateway"
|
||||
- "traefik.http.services.appwrite_redisinsight.loadbalancer.server.port=5540"
|
||||
- "traefik.http.routers.appwrite_redisinsight_http.entrypoints=appwrite_web"
|
||||
- "traefik.http.routers.appwrite_redisinsight_http.rule=Host(`redis.localhost`)"
|
||||
- "traefik.http.routers.appwrite_redisinsight_http.service=appwrite_redisinsight"
|
||||
- "traefik.http.routers.appwrite_redisinsight_https.entrypoints=appwrite_websecure"
|
||||
- "traefik.http.routers.appwrite_redisinsight_https.rule=Host(`redis.localhost`)"
|
||||
- "traefik.http.routers.appwrite_redisinsight_https.service=appwrite_redisinsight"
|
||||
- "traefik.http.routers.appwrite_redisinsight_https.tls=true"
|
||||
ports:
|
||||
- "8081:5540"
|
||||
|
||||
graphql-explorer:
|
||||
container_name: appwrite-graphql-explorer
|
||||
image: appwrite/altair:0.3.0
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
ports:
|
||||
- "9509:3000"
|
||||
environment:
|
||||
- SERVER_URL=http://localhost/v1/graphql
|
||||
|
||||
configs:
|
||||
redisinsight-connections.json:
|
||||
content: |
|
||||
[
|
||||
{
|
||||
"compressor": "NONE",
|
||||
"id": "104dc90a-21ef-4d5e-8912-b30baabb152f",
|
||||
"host": "redis",
|
||||
"port": 6379,
|
||||
"name": "redis:6379",
|
||||
"db": 0,
|
||||
"username": "default",
|
||||
"password": null,
|
||||
"connectionType": "STANDALONE",
|
||||
"nameFromProvider": null,
|
||||
"provider": "REDIS",
|
||||
"lastConnection": "2025-10-16T09:22:02.591Z",
|
||||
"modules": [
|
||||
{
|
||||
"name": "ReJSON",
|
||||
"version": 20808,
|
||||
"semanticVersion": "2.8.8"
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"version": 21015,
|
||||
"semanticVersion": "2.10.15"
|
||||
}
|
||||
],
|
||||
"tls": false,
|
||||
"tlsServername": null,
|
||||
"verifyServerCert": null,
|
||||
"caCert": null,
|
||||
"clientCert": null,
|
||||
"ssh": false,
|
||||
"sshOptions": null,
|
||||
"forceStandalone": false,
|
||||
"tags": []
|
||||
}
|
||||
]
|
||||
|
||||
adminer-index.php:
|
||||
content: |
|
||||
<?php
|
||||
if(!count($$_GET)) {
|
||||
$$_POST['auth'] = [
|
||||
'server' => $$_ENV['ADMINER_DEFAULT_SERVER'],
|
||||
'driver' => 'server', /* seems to autodetect the driver from server settings */
|
||||
'username' => $$_ENV['ADMINER_DEFAULT_USERNAME'],
|
||||
'password' => $$_ENV['ADMINER_DEFAULT_PASSWORD'],
|
||||
'db' => $$_ENV['ADMINER_DEFAULT_DB'],
|
||||
];
|
||||
}
|
||||
include './adminer.php';
|
||||
+58
-190
@@ -10,6 +10,15 @@ x-logging: &x-logging
|
||||
max-file: "5"
|
||||
max-size: "10m"
|
||||
|
||||
x-build: &x-build
|
||||
build:
|
||||
context: .
|
||||
target: development
|
||||
args:
|
||||
DEBUG: false
|
||||
TESTING: true
|
||||
VERSION: dev
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:3.6
|
||||
@@ -50,15 +59,13 @@ services:
|
||||
|
||||
appwrite:
|
||||
container_name: appwrite
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
image: appwrite-dev
|
||||
build:
|
||||
context: .
|
||||
target: development
|
||||
args:
|
||||
DEBUG: false
|
||||
TESTING: true
|
||||
VERSION: dev
|
||||
healthcheck:
|
||||
test: ["CMD", "doctor"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
ports:
|
||||
- 9501:80
|
||||
networks:
|
||||
@@ -101,10 +108,10 @@ services:
|
||||
- ./dev:/usr/src/code/dev
|
||||
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
- redis
|
||||
- coredns
|
||||
# - clamav
|
||||
redis:
|
||||
condition: service_healthy
|
||||
coredns:
|
||||
condition: service_started
|
||||
entrypoint:
|
||||
- php
|
||||
- -e
|
||||
@@ -152,7 +159,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_SMTP_HOST
|
||||
- _APP_SMTP_PORT
|
||||
- _APP_SMTP_SECURE
|
||||
@@ -261,7 +267,7 @@ services:
|
||||
|
||||
appwrite-realtime:
|
||||
entrypoint: realtime
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-realtime
|
||||
image: appwrite-dev
|
||||
restart: unless-stopped
|
||||
@@ -305,7 +311,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_LOGGING_CONFIG_REALTIME
|
||||
@@ -314,7 +319,7 @@ services:
|
||||
|
||||
appwrite-worker-audits:
|
||||
entrypoint: worker-audits
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-worker-audits
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -340,13 +345,12 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
|
||||
appwrite-worker-webhooks:
|
||||
entrypoint: worker-webhooks
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-worker-webhooks
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -371,7 +375,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -382,7 +385,7 @@ services:
|
||||
|
||||
appwrite-worker-deletes:
|
||||
entrypoint: worker-deletes
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-worker-deletes
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -414,7 +417,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
@@ -448,7 +450,7 @@ services:
|
||||
|
||||
appwrite-worker-databases:
|
||||
entrypoint: worker-databases
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-worker-databases
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -474,7 +476,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_WORKERS_NUM
|
||||
- _APP_QUEUE_NAME
|
||||
@@ -482,7 +483,7 @@ services:
|
||||
|
||||
appwrite-worker-builds:
|
||||
entrypoint: worker-builds
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-worker-builds
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -513,7 +514,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_VCS_GITHUB_APP_NAME
|
||||
- _APP_VCS_GITHUB_PRIVATE_KEY
|
||||
@@ -558,7 +558,7 @@ services:
|
||||
|
||||
appwrite-worker-screenshots:
|
||||
entrypoint: worker-screenshots
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-worker-screenshots
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -621,7 +621,7 @@ services:
|
||||
|
||||
appwrite-worker-certificates:
|
||||
entrypoint: worker-certificates
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-worker-certificates
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -658,13 +658,12 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
|
||||
appwrite-worker-executions:
|
||||
entrypoint: worker-executions
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-worker-executions
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -694,7 +693,7 @@ services:
|
||||
|
||||
appwrite-worker-functions:
|
||||
entrypoint: worker-functions
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-worker-functions
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -722,7 +721,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_SITES_TIMEOUT
|
||||
- _APP_COMPUTE_BUILD_TIMEOUT
|
||||
@@ -739,7 +737,7 @@ services:
|
||||
|
||||
appwrite-worker-mails:
|
||||
entrypoint: worker-mails
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-worker-mails
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -781,7 +779,7 @@ services:
|
||||
|
||||
appwrite-worker-messaging:
|
||||
entrypoint: worker-messaging
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-worker-messaging
|
||||
restart: unless-stopped
|
||||
image: appwrite-dev
|
||||
@@ -808,7 +806,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_SMS_FROM
|
||||
- _APP_SMS_PROVIDER
|
||||
@@ -839,7 +836,7 @@ services:
|
||||
|
||||
appwrite-worker-migrations:
|
||||
entrypoint: worker-migrations
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-worker-migrations
|
||||
restart: unless-stopped
|
||||
image: appwrite-dev
|
||||
@@ -875,7 +872,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
|
||||
@@ -885,7 +881,7 @@ services:
|
||||
|
||||
appwrite-task-maintenance:
|
||||
entrypoint: maintenance
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-task-maintenance
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -918,7 +914,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_CACHE
|
||||
@@ -932,7 +927,7 @@ services:
|
||||
|
||||
appwrite-task-interval:
|
||||
entrypoint: interval
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-task-interval
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -973,7 +968,7 @@ services:
|
||||
appwrite-task-stats-resources:
|
||||
container_name: appwrite-task-stats-resources
|
||||
entrypoint: stats-resources
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
- appwrite
|
||||
@@ -994,7 +989,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -1006,7 +1000,7 @@ services:
|
||||
|
||||
appwrite-worker-stats-resources:
|
||||
entrypoint: worker-stats-resources
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-worker-stats-resources
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -1028,7 +1022,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -1040,7 +1033,7 @@ services:
|
||||
|
||||
appwrite-worker-stats-usage:
|
||||
entrypoint: worker-stats-usage
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-worker-stats-usage
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -1062,7 +1055,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -1074,7 +1066,7 @@ services:
|
||||
|
||||
appwrite-task-scheduler-functions:
|
||||
entrypoint: schedule-functions
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-task-scheduler-functions
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -1100,12 +1092,11 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
|
||||
appwrite-task-scheduler-executions:
|
||||
entrypoint: schedule-executions
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-task-scheduler-executions
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -1131,11 +1122,10 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
appwrite-task-scheduler-messages:
|
||||
entrypoint: schedule-messages
|
||||
<<: *x-logging
|
||||
<<: [*x-logging, *x-build]
|
||||
container_name: appwrite-task-scheduler-messages
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
@@ -1161,7 +1151,6 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
|
||||
appwrite-assistant:
|
||||
@@ -1255,6 +1244,11 @@ services:
|
||||
- MYSQL_PASSWORD=${_APP_DB_PASS}
|
||||
- MARIADB_AUTO_UPGRADE=1
|
||||
command: "mysqld --innodb-flush-method=fsync"
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
mongodb:
|
||||
profiles: ["mongodb"]
|
||||
@@ -1293,20 +1287,7 @@ services:
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
appwrite-mongo-express:
|
||||
profiles: ["mongodb"]
|
||||
image: mongo-express
|
||||
container_name: appwrite-mongo-express
|
||||
networks:
|
||||
- appwrite
|
||||
ports:
|
||||
- "8082:8081"
|
||||
environment:
|
||||
ME_CONFIG_MONGODB_URL: "mongodb://root:${_APP_DB_ROOT_PASS}@appwrite-mongodb:27017/?replicaSet=rs0&directConnection=true"
|
||||
ME_CONFIG_BASICAUTH_USERNAME: ${_APP_DB_USER}
|
||||
ME_CONFIG_BASICAUTH_PASSWORD: ${_APP_DB_PASS}
|
||||
depends_on:
|
||||
- mongodb
|
||||
|
||||
|
||||
postgresql:
|
||||
profiles: ["postgresql"]
|
||||
@@ -1327,6 +1308,11 @@ services:
|
||||
- POSTGRES_USER=${_APP_DB_USER}
|
||||
- POSTGRES_PASSWORD=${_APP_DB_PASS}
|
||||
command: "postgres"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
redis:
|
||||
image: redis:7.4.7-alpine
|
||||
@@ -1343,6 +1329,11 @@ services:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-redis:/data:rw
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
coredns: # DNS server for testing purposes (Proxy APIs)
|
||||
image: coredns/coredns:1.12.4
|
||||
@@ -1414,78 +1405,8 @@ services:
|
||||
networks:
|
||||
- appwrite
|
||||
|
||||
adminer:
|
||||
image: adminer
|
||||
container_name: appwrite-adminer
|
||||
<<: *x-logging
|
||||
restart: always
|
||||
ports:
|
||||
- 9506:8080
|
||||
networks:
|
||||
- appwrite
|
||||
- gateway
|
||||
environment:
|
||||
- ADMINER_DESIGN=pepa-linha
|
||||
- ADMINER_DEFAULT_SERVER=mariadb
|
||||
- ADMINER_DEFAULT_USERNAME=root
|
||||
- ADMINER_DEFAULT_PASSWORD=rootsecretpassword
|
||||
- ADMINER_DEFAULT_DB=appwrite
|
||||
configs:
|
||||
- source: adminer-index.php
|
||||
target: /var/www/html/index.php
|
||||
mode: 0755
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.constraint-label-stack=appwrite"
|
||||
- "traefik.docker.network=gateway"
|
||||
- "traefik.http.services.appwrite_adminer.loadbalancer.server.port=8080"
|
||||
- "traefik.http.routers.appwrite_adminer_http.entrypoints=appwrite_web"
|
||||
- "traefik.http.routers.appwrite_adminer_http.rule=Host(`mysql.localhost`)"
|
||||
- "traefik.http.routers.appwrite_adminer_http.service=appwrite_adminer"
|
||||
- "traefik.http.routers.appwrite_adminer_https.entrypoints=appwrite_websecure"
|
||||
- "traefik.http.routers.appwrite_adminer_https.rule=Host(`mysql.localhost`)"
|
||||
- "traefik.http.routers.appwrite_adminer_https.service=appwrite_adminer"
|
||||
- "traefik.http.routers.appwrite_adminer_https.tls=true"
|
||||
|
||||
redis-insight:
|
||||
image: redis/redisinsight:latest
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
- gateway
|
||||
environment:
|
||||
- RI_PRE_SETUP_DATABASES_PATH=/mnt/connections.json
|
||||
configs:
|
||||
- source: redisinsight-connections.json
|
||||
target: /mnt/connections.json
|
||||
mode: 0755
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.constraint-label-stack=appwrite"
|
||||
- "traefik.docker.network=gateway"
|
||||
- "traefik.http.services.appwrite_redisinsight.loadbalancer.server.port=5540"
|
||||
- "traefik.http.routers.appwrite_redisinsight_http.entrypoints=appwrite_web"
|
||||
- "traefik.http.routers.appwrite_redisinsight_http.rule=Host(`redis.localhost`)"
|
||||
- "traefik.http.routers.appwrite_redisinsight_http.service=appwrite_redisinsight"
|
||||
- "traefik.http.routers.appwrite_redisinsight_https.entrypoints=appwrite_websecure"
|
||||
- "traefik.http.routers.appwrite_redisinsight_https.rule=Host(`redis.localhost`)"
|
||||
- "traefik.http.routers.appwrite_redisinsight_https.service=appwrite_redisinsight"
|
||||
- "traefik.http.routers.appwrite_redisinsight_https.tls=true"
|
||||
ports:
|
||||
- "8081:5540"
|
||||
|
||||
graphql-explorer:
|
||||
container_name: appwrite-graphql-explorer
|
||||
image: appwrite/altair:0.3.0
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
ports:
|
||||
- "9509:3000"
|
||||
environment:
|
||||
- SERVER_URL=http://localhost/v1/graphql
|
||||
|
||||
# Dev Tools End ------------------------------------------------------------------------------------------
|
||||
# Dev tools (adminer, redis-insight, mongo-express, graphql-explorer)
|
||||
# are defined in docker-compose.override.yml
|
||||
|
||||
networks:
|
||||
gateway:
|
||||
@@ -1498,60 +1419,7 @@ networks:
|
||||
runtimes:
|
||||
name: runtimes
|
||||
|
||||
configs:
|
||||
redisinsight-connections.json:
|
||||
content: |
|
||||
[
|
||||
{
|
||||
"compressor": "NONE",
|
||||
"id": "104dc90a-21ef-4d5e-8912-b30baabb152f",
|
||||
"host": "redis",
|
||||
"port": 6379,
|
||||
"name": "redis:6379",
|
||||
"db": 0,
|
||||
"username": "default",
|
||||
"password": null,
|
||||
"connectionType": "STANDALONE",
|
||||
"nameFromProvider": null,
|
||||
"provider": "REDIS",
|
||||
"lastConnection": "2025-10-16T09:22:02.591Z",
|
||||
"modules": [
|
||||
{
|
||||
"name": "ReJSON",
|
||||
"version": 20808,
|
||||
"semanticVersion": "2.8.8"
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"version": 21015,
|
||||
"semanticVersion": "2.10.15"
|
||||
}
|
||||
],
|
||||
"tls": false,
|
||||
"tlsServername": null,
|
||||
"verifyServerCert": null,
|
||||
"caCert": null,
|
||||
"clientCert": null,
|
||||
"ssh": false,
|
||||
"sshOptions": null,
|
||||
"forceStandalone": false,
|
||||
"tags": []
|
||||
}
|
||||
]
|
||||
|
||||
adminer-index.php:
|
||||
content: |
|
||||
<?php
|
||||
if(!count($$_GET)) {
|
||||
$$_POST['auth'] = [
|
||||
'server' => $$_ENV['ADMINER_DEFAULT_SERVER'],
|
||||
'driver' => 'server', /* seems to autodetect the driver from server settings */
|
||||
'username' => $$_ENV['ADMINER_DEFAULT_USERNAME'],
|
||||
'password' => $$_ENV['ADMINER_DEFAULT_PASSWORD'],
|
||||
'db' => $$_ENV['ADMINER_DEFAULT_DB'],
|
||||
];
|
||||
}
|
||||
include './adminer.php';
|
||||
|
||||
volumes:
|
||||
appwrite-mariadb:
|
||||
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@appwrite.io/repo",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@appwrite.io/repo"
|
||||
}
|
||||
}
|
||||
}
|
||||
+83227
File diff suppressed because it is too large
Load Diff
+11
-3
@@ -1,9 +1,17 @@
|
||||
includes:
|
||||
- phpstan-baseline.neon
|
||||
|
||||
parameters:
|
||||
level: 8
|
||||
level: max
|
||||
paths:
|
||||
- src/Appwrite/Transformation
|
||||
- src
|
||||
- app
|
||||
- bin
|
||||
- tests
|
||||
bootstrapFiles:
|
||||
- app/init/constants.php
|
||||
scanDirectories:
|
||||
- vendor/swoole/ide-helper
|
||||
excludePaths:
|
||||
- tests/resources
|
||||
- app/sdks
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
colors="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
stopOnError="false"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
>
|
||||
<extensions>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Bus\Events;
|
||||
|
||||
use Utopia\Bus\Event;
|
||||
|
||||
class ExecutionCompleted implements Event
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $execution
|
||||
* @param array<string, mixed> $project
|
||||
* @param array<string, mixed> $spec
|
||||
* @param array<string, mixed> $resource
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly array $execution,
|
||||
public readonly array $project,
|
||||
public readonly array $spec = [],
|
||||
public readonly array $resource = [],
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Bus\Events;
|
||||
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Bus\Event;
|
||||
|
||||
class RequestCompleted implements Event
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $project
|
||||
* @param array<string, mixed> $deployment
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly array $project,
|
||||
public readonly Request $request,
|
||||
public readonly Response $response,
|
||||
public readonly array $deployment = [],
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Bus\Listeners;
|
||||
|
||||
use Appwrite\Bus\Events\ExecutionCompleted;
|
||||
use Appwrite\Event\Execution;
|
||||
use Utopia\Bus\Listener;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Queue\Publisher;
|
||||
|
||||
class Log extends Listener
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'log';
|
||||
}
|
||||
|
||||
public static function getEvents(): array
|
||||
{
|
||||
return [ExecutionCompleted::class];
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->desc('Persists execution logs to database via queue')
|
||||
->inject('publisher')
|
||||
->callback($this->handle(...));
|
||||
}
|
||||
|
||||
public function handle(ExecutionCompleted $event, Publisher $publisher): void
|
||||
{
|
||||
$queueForExecutions = new Execution($publisher);
|
||||
$queueForExecutions
|
||||
->setExecution(new Document($event->execution))
|
||||
->setProject(new Document($event->project))
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Bus\Listeners;
|
||||
|
||||
use Appwrite\Bus\Events\ExecutionCompleted;
|
||||
use Appwrite\Bus\Events\RequestCompleted;
|
||||
use Appwrite\Event\Message\Usage as UsageMessage;
|
||||
use Appwrite\Event\Publisher\Usage as Publisher;
|
||||
use Appwrite\Usage\Context;
|
||||
use Utopia\Bus\Event;
|
||||
use Utopia\Bus\Listener;
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class Usage extends Listener
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'usage';
|
||||
}
|
||||
|
||||
public static function getEvents(): array
|
||||
{
|
||||
return [
|
||||
ExecutionCompleted::class,
|
||||
RequestCompleted::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->desc('Records usage metrics')
|
||||
->inject('publisherForUsage')
|
||||
->inject('usage')
|
||||
->callback($this->handle(...));
|
||||
}
|
||||
|
||||
public function handle(Event $event, Publisher $publisherForUsage, Context $usage): void
|
||||
{
|
||||
match (true) {
|
||||
$event instanceof ExecutionCompleted => $this->handleExecutionCompleted($event, $publisherForUsage),
|
||||
$event instanceof RequestCompleted => $this->handleRequestCompleted($event, $usage),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function handleExecutionCompleted(ExecutionCompleted $event, Publisher $publisherForUsage): void
|
||||
{
|
||||
$execution = new Document($event->execution);
|
||||
$resource = new Document($event->resource);
|
||||
|
||||
// Non-SSR sites don't record execution metrics
|
||||
if ($execution->getAttribute('resourceType') === 'sites' && $resource->getAttribute('adapter') !== 'ssr') {
|
||||
return;
|
||||
}
|
||||
$project = new Document($event->project);
|
||||
$spec = $event->spec;
|
||||
|
||||
$resourceType = $execution->getAttribute('resourceType', '');
|
||||
$resourceInternalId = $execution->getAttribute('resourceInternalId', '');
|
||||
$duration = $execution->getAttribute('duration', 0);
|
||||
|
||||
$compute = (int)($duration * 1000);
|
||||
$mbSeconds = (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $duration * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT));
|
||||
|
||||
$context = (new Context())
|
||||
->addMetric(METRIC_EXECUTIONS, 1)
|
||||
->addMetric(str_replace(['{resourceType}'], [$resourceType], METRIC_RESOURCE_TYPE_EXECUTIONS), 1)
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$resourceType, $resourceInternalId], METRIC_RESOURCE_TYPE_ID_EXECUTIONS), 1)
|
||||
->addMetric(METRIC_EXECUTIONS_COMPUTE, $compute)
|
||||
->addMetric(str_replace(['{resourceType}'], [$resourceType], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE), $compute)
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$resourceType, $resourceInternalId], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE), $compute)
|
||||
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, $mbSeconds)
|
||||
->addMetric(str_replace(['{resourceType}'], [$resourceType], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS), $mbSeconds)
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$resourceType, $resourceInternalId], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), $mbSeconds);
|
||||
|
||||
$message = new UsageMessage(
|
||||
project: $project,
|
||||
metrics: $context->getMetrics(),
|
||||
reduce: $context->getReduce()
|
||||
);
|
||||
|
||||
$publisherForUsage->enqueue($message);
|
||||
}
|
||||
|
||||
private function handleRequestCompleted(RequestCompleted $event, Context $usage): void
|
||||
{
|
||||
$fileSize = 0;
|
||||
$file = $event->request->getFiles('file');
|
||||
if (!empty($file)) {
|
||||
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
|
||||
}
|
||||
|
||||
$deployment = new Document($event->deployment);
|
||||
|
||||
$inbound = $event->request->getSize() + $fileSize;
|
||||
$outbound = $event->response->getSize();
|
||||
|
||||
if ($deployment->getAttribute('resourceType') === 'sites') {
|
||||
$siteInternalId = $deployment->getAttribute('resourceInternalId', '');
|
||||
$usage
|
||||
->addMetric(METRIC_SITES_REQUESTS, 1)
|
||||
->addMetric(METRIC_SITES_INBOUND, $inbound)
|
||||
->addMetric(METRIC_SITES_OUTBOUND, $outbound)
|
||||
->addMetric(str_replace('{siteInternalId}', $siteInternalId, METRIC_SITES_ID_REQUESTS), 1)
|
||||
->addMetric(str_replace('{siteInternalId}', $siteInternalId, METRIC_SITES_ID_INBOUND), $inbound)
|
||||
->addMetric(str_replace('{siteInternalId}', $siteInternalId, METRIC_SITES_ID_OUTBOUND), $outbound);
|
||||
} else {
|
||||
$usage
|
||||
->addMetric(METRIC_NETWORK_REQUESTS, 1)
|
||||
->addMetric(METRIC_NETWORK_INBOUND, $inbound)
|
||||
->addMetric(METRIC_NETWORK_OUTBOUND, $outbound);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -725,7 +725,9 @@ class Event
|
||||
$events = $pairedEvents;
|
||||
}
|
||||
// mirrored events can have duplicates in case of smaller events
|
||||
return array_unique($events);
|
||||
// array unique can turns list to hasmap in case duplicates present
|
||||
// so forcing array value will turn this to array list always
|
||||
return array_values(array_unique($events));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Message;
|
||||
|
||||
abstract class Base
|
||||
{
|
||||
/**
|
||||
* Serialize message to array for queue
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract public function toArray(): array;
|
||||
|
||||
/**
|
||||
* Deserialize message from array
|
||||
*
|
||||
* @param array $data
|
||||
* @return static
|
||||
*/
|
||||
abstract public static function fromArray(array $data): static;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Message;
|
||||
|
||||
use Utopia\Database\Document;
|
||||
|
||||
class Usage extends Base
|
||||
{
|
||||
/**
|
||||
* @param Document $project
|
||||
* @param array<array{key: string, value: int}> $metrics
|
||||
* @param array<Document> $reduce
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly Document $project,
|
||||
public readonly array $metrics,
|
||||
public readonly array $reduce = [],
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'project' => [
|
||||
'$id' => $this->project->getId(),
|
||||
'$sequence' => $this->project->getSequence(),
|
||||
'database' => $this->project->getAttribute('database', ''),
|
||||
],
|
||||
'metrics' => $this->metrics,
|
||||
'reduce' => array_map(fn (Document $doc) => $doc->getArrayCopy(), $this->reduce),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return static
|
||||
*/
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new self(
|
||||
project: new Document($data['project'] ?? []),
|
||||
metrics: $data['metrics'] ?? [],
|
||||
reduce: array_map(fn (array $doc) => new Document($doc), $data['reduce'] ?? []),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Publisher;
|
||||
|
||||
use Appwrite\Event\Message\Base as BaseMessage;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
|
||||
readonly class Base
|
||||
{
|
||||
public function __construct(
|
||||
protected Publisher $publisher
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a message to the queue
|
||||
*/
|
||||
public function publish(Queue $queue, BaseMessage $message): string|bool
|
||||
{
|
||||
$payload = $message->toArray();
|
||||
|
||||
return $this->publisher->enqueue($queue, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of a queue
|
||||
*/
|
||||
public function getQueueSize(Queue $queue, bool $failed = false): int
|
||||
{
|
||||
return $this->publisher->getQueueSize($queue, $failed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event\Publisher;
|
||||
|
||||
use Appwrite\Event\Message\Usage as UsageMessage;
|
||||
use Utopia\Console;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\Queue\Queue;
|
||||
|
||||
readonly class Usage extends Base
|
||||
{
|
||||
public function __construct(
|
||||
Publisher $publisher,
|
||||
protected Queue $queue
|
||||
) {
|
||||
parent::__construct($publisher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue a usage message
|
||||
*/
|
||||
public function enqueue(UsageMessage $message): string|bool
|
||||
{
|
||||
try {
|
||||
return $this->publish($this->queue, $message);
|
||||
} catch (\Throwable $th) {
|
||||
Console::error('[Usage] Failed to publish usage message: ' . $th->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the usage queue
|
||||
*/
|
||||
public function getSize(bool $failed = false): int
|
||||
{
|
||||
return $this->getQueueSize($this->queue, $failed);
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event;
|
||||
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\System\System;
|
||||
|
||||
class StatsUsage extends Event
|
||||
{
|
||||
protected array $metrics = [];
|
||||
protected array $reduce = [];
|
||||
protected array $disabled = [];
|
||||
|
||||
protected bool $critical = false;
|
||||
|
||||
public function __construct(protected Publisher $publisher)
|
||||
{
|
||||
parent::__construct($publisher);
|
||||
|
||||
$this
|
||||
->setQueue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
|
||||
->setClass(System::getEnv('_APP_STATS_USAGE_CLASS_NAME', Event::STATS_USAGE_CLASS_NAME));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add reduce.
|
||||
*
|
||||
* @param Document $document
|
||||
* @return self
|
||||
*/
|
||||
public function addReduce(Document $document): self
|
||||
{
|
||||
$this->reduce[] = $document;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add metric.
|
||||
*
|
||||
* @param string $key
|
||||
* @param int $value
|
||||
* @return self
|
||||
*/
|
||||
public function addMetric(string $key, int $value): self
|
||||
{
|
||||
$this->metrics[] = [
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set disabled metrics.
|
||||
*
|
||||
* @param string $key
|
||||
* @return self
|
||||
*/
|
||||
public function disableMetric(string $key): self
|
||||
{
|
||||
$this->disabled[] = $key;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the payload for the event
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function preparePayload(): array
|
||||
{
|
||||
return [
|
||||
'project' => $this->getProject(),
|
||||
'reduce' => $this->reduce,
|
||||
'metrics' => \array_filter($this->metrics, function ($metric) {
|
||||
foreach ($this->disabled as $disabledMetric) {
|
||||
if (\str_ends_with($metric['key'], $disabledMetric)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function reset(): Event
|
||||
{
|
||||
$this->metrics = [];
|
||||
parent::reset();
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -166,6 +166,7 @@ class Exception extends \Exception
|
||||
|
||||
/** Sites */
|
||||
public const string SITE_NOT_FOUND = 'site_not_found';
|
||||
public const string SITE_ALREADY_EXISTS = 'site_already_exists';
|
||||
public const string SITE_TEMPLATE_NOT_FOUND = 'site_template_not_found';
|
||||
|
||||
/** Functions */
|
||||
@@ -365,6 +366,7 @@ class Exception extends \Exception
|
||||
|
||||
/** Message */
|
||||
public const string MESSAGE_NOT_FOUND = 'message_not_found';
|
||||
public const string MESSAGE_ALREADY_EXISTS = 'message_already_exists';
|
||||
public const string MESSAGE_MISSING_TARGET = 'message_missing_target';
|
||||
public const string MESSAGE_ALREADY_SENT = 'message_already_sent';
|
||||
public const string MESSAGE_ALREADY_PROCESSING = 'message_already_processing';
|
||||
|
||||
@@ -510,25 +510,13 @@ class Realtime extends MessagingAdapter
|
||||
$collectionId = $payload->getAttribute('$collectionId', '');
|
||||
$resourceId = $tableId ?: $collectionId;
|
||||
$channels = [];
|
||||
// backward compat(tablesdb will have databases channels + tablesdb prefixed channels)
|
||||
if ($parts[0] === 'databases' || $parts[0] === 'tablesdb') {
|
||||
$prefix = 'databases';
|
||||
|
||||
$channels = self::getDatabaseChannels('legacy', $database->getId(), $resourceId, $payload->getId(), $prefix);
|
||||
|
||||
$channels = array_unique([
|
||||
...$channels,
|
||||
...self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId(), $prefix)
|
||||
]);
|
||||
}
|
||||
|
||||
// prefixed channels -> tablesdb
|
||||
if ($parts[0] !== 'databases') {
|
||||
$channels = array_unique([
|
||||
...$channels,
|
||||
...self::getDatabaseChannels($parts[0], $database->getId(), $resourceId, $payload->getId()),
|
||||
]);
|
||||
}
|
||||
// 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())
|
||||
)));
|
||||
|
||||
$roles = $collection->getAttribute('documentSecurity', false)
|
||||
? \array_merge($collection->getRead(), $payload->getRead())
|
||||
@@ -620,6 +608,7 @@ class Realtime extends MessagingAdapter
|
||||
$channels[] = "{$basePrefix}.{$databaseId}.collections.{$resourceId}.documents";
|
||||
$channels[] = "{$basePrefix}.{$databaseId}.collections.{$resourceId}.documents.{$payloadId}";
|
||||
break;
|
||||
|
||||
case 'tablesdb':
|
||||
$channels[] = 'rows';
|
||||
$channels[] = "{$basePrefix}.{$databaseId}.tables.{$resourceId}.rows";
|
||||
|
||||
@@ -119,7 +119,7 @@ class Update extends Action
|
||||
|
||||
$authenticator->setAttribute('verified', true);
|
||||
|
||||
$dbForProject->updateDocument('authenticators', $authenticator->getId(), $authenticator);
|
||||
$dbForProject->updateDocument('authenticators', $authenticator->getId(), new Document(['verified' => true]));
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$factors = $session->getAttribute('factors', []);
|
||||
@@ -127,7 +127,7 @@ class Update extends Action
|
||||
$factors = \array_values(\array_unique($factors));
|
||||
|
||||
$session->setAttribute('factors', $factors);
|
||||
$dbForProject->updateDocument('sessions', $session->getId(), $session);
|
||||
$dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ use Appwrite\Detector\Detector;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
@@ -15,6 +14,7 @@ use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use libphonenumber\NumberParseException;
|
||||
@@ -104,7 +104,7 @@ class Create extends Action
|
||||
->inject('queueForMessaging')
|
||||
->inject('queueForMails')
|
||||
->inject('timelimit')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('proofForToken')
|
||||
->inject('proofForCode')
|
||||
@@ -124,7 +124,7 @@ class Create extends Action
|
||||
Messaging $queueForMessaging,
|
||||
Mail $queueForMails,
|
||||
callable $timelimit,
|
||||
StatsUsage $queueForStatsUsage,
|
||||
Context $usage,
|
||||
array $plan,
|
||||
ProofsToken $proofForToken,
|
||||
ProofsCode $proofForCode
|
||||
@@ -201,16 +201,12 @@ class Create extends Action
|
||||
$countryCode = $helper->parse($phone)->getCountryCode();
|
||||
|
||||
if (!empty($countryCode)) {
|
||||
$queueForStatsUsage
|
||||
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
|
||||
$usage->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
|
||||
}
|
||||
} catch (NumberParseException $e) {
|
||||
// Ignore invalid phone number for country code stats
|
||||
}
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
$usage->addMetric(METRIC_AUTH_METHOD_PHONE, 1);
|
||||
break;
|
||||
case Type::EMAIL:
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
|
||||
@@ -117,7 +117,7 @@ class Update extends Action
|
||||
$mfaRecoveryCodes = \array_diff($mfaRecoveryCodes, [$otp]);
|
||||
$mfaRecoveryCodes = \array_values($mfaRecoveryCodes);
|
||||
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -147,11 +147,13 @@ class Update extends Action
|
||||
$factors[] = $type;
|
||||
$factors = \array_values(\array_unique($factors));
|
||||
|
||||
$mfaUpdatedAt = DateTime::now();
|
||||
|
||||
$session
|
||||
->setAttribute('factors', $factors)
|
||||
->setAttribute('mfaUpdatedAt', DateTime::now());
|
||||
->setAttribute('mfaUpdatedAt', $mfaUpdatedAt);
|
||||
|
||||
$dbForProject->updateDocument('sessions', $session->getId(), $session);
|
||||
$dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors, 'mfaUpdatedAt' => $mfaUpdatedAt]));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
|
||||
@@ -93,7 +93,7 @@ class Create extends Action
|
||||
|
||||
$mfaRecoveryCodes = Type::generateBackupCodes();
|
||||
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ class Update extends Action
|
||||
|
||||
$mfaRecoveryCodes = Type::generateBackupCodes();
|
||||
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ class Update extends Action
|
||||
): void {
|
||||
$user->setAttribute('mfa', $mfa);
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['mfa' => $mfa]));
|
||||
|
||||
if ($mfa) {
|
||||
$factors = $session->getAttribute('factors', []);
|
||||
@@ -89,7 +89,7 @@ class Update extends Action
|
||||
$factors = \array_values(\array_unique($factors));
|
||||
|
||||
$session->setAttribute('factors', $factors);
|
||||
$dbForProject->updateDocument('sessions', $session->getId(), $session);
|
||||
$dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors]));
|
||||
}
|
||||
|
||||
$queueForEvents->setParam('userId', $user->getId());
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Avatars\Http\Screenshots;
|
||||
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Avatars\Http\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -10,6 +9,7 @@ use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\MethodType;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Domains\Domain;
|
||||
@@ -84,11 +84,11 @@ class Get extends Action
|
||||
->param('quality', -1, new Range(-1, 100), 'Screenshot quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true, example: '85')
|
||||
->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true, example: 'jpeg')
|
||||
->inject('response')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $url, array $headers, int $viewportWidth, int $viewportHeight, float $scale, string $theme, string $userAgent, bool $fullpage, string $locale, string $timezone, float $latitude, float $longitude, float $accuracy, bool $touch, array $permissions, int $sleep, int $width, int $height, int $quality, string $output, Response $response, StatsUsage $queueForStatsUsage)
|
||||
public function action(string $url, array $headers, int $viewportWidth, int $viewportHeight, float $scale, string $theme, string $userAgent, bool $fullpage, string $locale, string $timezone, float $latitude, float $longitude, float $accuracy, bool $touch, array $permissions, int $sleep, int $width, int $height, int $quality, string $output, Response $response, Context $usage)
|
||||
{
|
||||
if (!\extension_loaded('imagick')) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
|
||||
@@ -210,7 +210,7 @@ class Get extends Action
|
||||
$outputs = Config::getParam('storage-outputs');
|
||||
$contentType = $outputs[$output] ?? $outputs['png'];
|
||||
|
||||
$queueForStatsUsage->addMetric(METRIC_AVATARS_SCREENSHOTS_GENERATED, 1);
|
||||
$usage->addMetric(METRIC_AVATARS_SCREENSHOTS_GENERATED, 1);
|
||||
|
||||
$response
|
||||
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
|
||||
|
||||
@@ -143,7 +143,12 @@ class Base extends Action
|
||||
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
|
||||
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
|
||||
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function);
|
||||
$dbForProject->updateDocument('functions', $function->getId(), new Document([
|
||||
'latestDeploymentId' => $deployment->getId(),
|
||||
'latestDeploymentInternalId' => $deployment->getSequence(),
|
||||
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
|
||||
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
|
||||
]));
|
||||
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
@@ -249,7 +254,12 @@ class Base extends Action
|
||||
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
|
||||
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
|
||||
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
|
||||
$dbForProject->updateDocument('sites', $site->getId(), $site);
|
||||
$dbForProject->updateDocument('sites', $site->getId(), new Document([
|
||||
'latestDeploymentId' => $deployment->getId(),
|
||||
'latestDeploymentInternalId' => $deployment->getSequence(),
|
||||
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
|
||||
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
|
||||
]));
|
||||
|
||||
$sitesDomain = $platform['sitesDomain'];
|
||||
$domain = ID::unique() . "." . $sitesDomain;
|
||||
|
||||
+5
-2
@@ -12,6 +12,7 @@ use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\IndexDependency as IndexDependencyValidator;
|
||||
use Utopia\Database\Validator\Key;
|
||||
@@ -98,7 +99,8 @@ class Delete extends Action
|
||||
}
|
||||
|
||||
if ($attribute->getAttribute('status') === 'available') {
|
||||
$attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'deleting'));
|
||||
$attribute->setAttribute('status', 'deleting');
|
||||
$attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), new Document(['status' => 'deleting']));
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
|
||||
@@ -118,7 +120,8 @@ class Delete extends Action
|
||||
}
|
||||
|
||||
if ($relatedAttribute->getAttribute('status') === 'available') {
|
||||
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'deleting'));
|
||||
$relatedAttribute->setAttribute('status', 'deleting');
|
||||
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), new Document(['status' => 'deleting']));
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $options['relatedCollection']);
|
||||
|
||||
+4
-4
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -11,6 +10,7 @@ use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use InvalidArgumentException;
|
||||
@@ -83,13 +83,13 @@ class Decrement extends Action
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization): void
|
||||
{
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
@@ -200,7 +200,7 @@ class Decrement extends Action
|
||||
)
|
||||
);
|
||||
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1);
|
||||
|
||||
|
||||
+4
-4
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -11,6 +10,7 @@ use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use InvalidArgumentException;
|
||||
@@ -83,13 +83,13 @@ class Increment extends Action
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization): void
|
||||
{
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
@@ -200,7 +200,7 @@ class Increment extends Action
|
||||
)
|
||||
);
|
||||
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1);
|
||||
|
||||
|
||||
+5
-5
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Functions\EventProcessor;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
|
||||
@@ -12,6 +11,7 @@ use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
@@ -76,7 +76,7 @@ class Delete extends Action
|
||||
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForRealtime')
|
||||
->inject('queueForFunctions')
|
||||
@@ -86,7 +86,7 @@ class Delete extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
|
||||
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
|
||||
{
|
||||
$database = $dbForProject->getDocument('databases', $databaseId);
|
||||
if ($database->isEmpty()) {
|
||||
@@ -185,10 +185,10 @@ class Delete extends Action
|
||||
|
||||
foreach ($documents as $document) {
|
||||
$document->setAttribute('$databaseId', $database->getId());
|
||||
$document->setAttribute('$'.$this->getCollectionsEventsContext().'Id', $collection->getId());
|
||||
$document->setAttribute('$' . $this->getCollectionsEventsContext() . 'Id', $collection->getId());
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $modified))
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $modified));
|
||||
|
||||
|
||||
+5
-5
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Functions\EventProcessor;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
|
||||
@@ -12,6 +11,7 @@ use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
@@ -80,7 +80,7 @@ class Update extends Action
|
||||
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForRealtime')
|
||||
->inject('queueForFunctions')
|
||||
@@ -90,7 +90,7 @@ class Update extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string|array $data, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
|
||||
public function action(string $databaseId, string $collectionId, string|array $data, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
|
||||
{
|
||||
$data = \is_string($data)
|
||||
? \json_decode($data, true)
|
||||
@@ -216,10 +216,10 @@ class Update extends Action
|
||||
|
||||
foreach ($documents as $document) {
|
||||
$document->setAttribute('$databaseId', $database->getId());
|
||||
$document->setAttribute('$'.$this->getCollectionsEventsContext().'Id', $collection->getId());
|
||||
$document->setAttribute('$' . $this->getCollectionsEventsContext() . 'Id', $collection->getId());
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $modified))
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $modified));
|
||||
|
||||
|
||||
+6
-6
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Functions\EventProcessor;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
|
||||
@@ -12,6 +11,7 @@ use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
@@ -78,7 +78,7 @@ class Upsert extends Action
|
||||
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForRealtime')
|
||||
->inject('queueForFunctions')
|
||||
@@ -88,7 +88,7 @@ class Upsert extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
|
||||
public function action(string $databaseId, string $collectionId, array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
|
||||
{
|
||||
$database = $dbForProject->getDocument('databases', $databaseId);
|
||||
if ($database->isEmpty()) {
|
||||
@@ -106,7 +106,7 @@ class Upsert extends Action
|
||||
);
|
||||
|
||||
if ($hasRelationships) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk upsert is not supported for ' . $this->getSDKNamespace() . ' with relationship attributes');
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk upsert is not supported for ' . $this->getSDKNamespace() . ' with relationship attributes');
|
||||
}
|
||||
|
||||
foreach ($documents as $key => $document) {
|
||||
@@ -191,10 +191,10 @@ class Upsert extends Action
|
||||
|
||||
foreach ($upserted as $document) {
|
||||
$document->setAttribute('$databaseId', $database->getId());
|
||||
$document->setAttribute('$'.$this->getCollectionsEventsContext().'Id', $collection->getId());
|
||||
$document->setAttribute('$' . $this->getCollectionsEventsContext() . 'Id', $collection->getId());
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $modified))
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $modified));
|
||||
|
||||
|
||||
+5
-5
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Functions\EventProcessor;
|
||||
use Appwrite\SDK\AuthType;
|
||||
@@ -12,6 +11,7 @@ use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Parameter;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
@@ -129,7 +129,7 @@ class Create extends Action
|
||||
->inject('dbForProject')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('queueForRealtime')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForWebhooks')
|
||||
@@ -138,7 +138,7 @@ class Create extends Action
|
||||
->inject('eventProcessor')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
{
|
||||
$data = \is_string($data)
|
||||
? \json_decode($data, true)
|
||||
@@ -205,7 +205,7 @@ class Create extends Action
|
||||
);
|
||||
|
||||
if ($isBulk && $hasRelationships) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSDKNamespace() .' with relationship ' . $this->getStructureContext());
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSDKNamespace() . ' with relationship ' . $this->getStructureContext());
|
||||
}
|
||||
|
||||
$setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk, $dbForProject, $authorization) {
|
||||
@@ -489,7 +489,7 @@ class Create extends Action
|
||||
);
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations))
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); // per collection
|
||||
|
||||
|
||||
+4
-4
@@ -4,13 +4,13 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documen
|
||||
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
@@ -80,7 +80,7 @@ class Delete extends Action
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
@@ -96,7 +96,7 @@ class Delete extends Action
|
||||
UtopiaResponse $response,
|
||||
Database $dbForProject,
|
||||
Event $queueForEvents,
|
||||
StatsUsage $queueForStatsUsage,
|
||||
Context $usage,
|
||||
TransactionState $transactionState,
|
||||
array $plan,
|
||||
Authorization $authorization
|
||||
@@ -210,7 +210,7 @@ class Delete extends Action
|
||||
authorization: $authorization
|
||||
);
|
||||
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1); // per collection
|
||||
|
||||
|
||||
+4
-4
@@ -3,13 +3,13 @@
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
|
||||
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
@@ -68,13 +68,13 @@ class Get extends Action
|
||||
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID to read uncommitted changes within the transaction.', true, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Context $usage, TransactionState $transactionState, Authorization $authorization): void
|
||||
{
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
@@ -130,7 +130,7 @@ class Get extends Action
|
||||
operations: $operations
|
||||
);
|
||||
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_READS, max($operations, 1))
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations);
|
||||
|
||||
|
||||
+4
-4
@@ -4,13 +4,13 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documen
|
||||
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
@@ -84,14 +84,14 @@ class Update extends Action
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionState $transactionState, array $plan, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization): void
|
||||
{
|
||||
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
|
||||
|
||||
@@ -246,7 +246,7 @@ class Update extends Action
|
||||
|
||||
$setCollection($collection, $newDocument);
|
||||
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, max($operations, 1))
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations);
|
||||
|
||||
|
||||
+4
-4
@@ -4,13 +4,13 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documen
|
||||
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
@@ -88,14 +88,14 @@ class Upsert extends Action
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionState $transactionState, array $plan, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization): void
|
||||
{
|
||||
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
|
||||
|
||||
@@ -256,7 +256,7 @@ class Upsert extends Action
|
||||
|
||||
$setCollection($collection, $newDocument);
|
||||
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations))
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations));
|
||||
|
||||
|
||||
+74
-7
@@ -3,13 +3,13 @@
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
|
||||
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
@@ -24,6 +24,7 @@ use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class XList extends Action
|
||||
@@ -70,15 +71,17 @@ class XList extends Action
|
||||
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
|
||||
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID to read uncommitted changes within the transaction.', true, ['dbForProject'])
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->param('ttl', 0, new Range(min: 0, max: 86400), 'TTL (seconds) for cached responses when caching is enabled for select queries. Must be between 0 and 86400 (24 hours).', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('user')
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, Document $user, Context $usage, TransactionState $transactionState, Authorization $authorization): void
|
||||
{
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
@@ -129,9 +132,73 @@ class XList extends Action
|
||||
$documents = $transactionState->listDocuments($collectionTableId, $transactionId, $queries);
|
||||
$total = $includeTotal ? $transactionState->countDocuments($collectionTableId, $transactionId, $queries) : 0;
|
||||
} elseif (! empty($selectQueries)) {
|
||||
// has selects, allow relationship on documents
|
||||
$documents = $dbForProject->find($collectionTableId, $queries);
|
||||
$total = $includeTotal ? $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT) : 0;
|
||||
|
||||
if ((int)$ttl > 0) {
|
||||
$serializedQueries = [];
|
||||
foreach ($queries as $query) {
|
||||
$serializedQueries[] = $query instanceof Query ? $query->toArray() : $query;
|
||||
}
|
||||
|
||||
$hostname = $dbForProject->getAdapter()->getHostname();
|
||||
$roles = $dbForProject->getAuthorization()->getRoles();
|
||||
$schemaHash = \md5(\json_encode($collection->getAttribute('attributes', [])) . \json_encode($collection->getAttribute('indexes', [])));
|
||||
$cacheKeyBase = \sprintf(
|
||||
'%s-cache-%s:%s:%s:collection:%s:%s:user:%s:%s',
|
||||
$dbForProject->getCacheName(),
|
||||
$hostname ?? '',
|
||||
$dbForProject->getNamespace(),
|
||||
$dbForProject->getTenant(),
|
||||
$collectionId,
|
||||
$schemaHash,
|
||||
\md5(\json_encode($roles)),
|
||||
\md5(\json_encode($serializedQueries))
|
||||
);
|
||||
|
||||
$documentsCacheKey = $cacheKeyBase . ':documents';
|
||||
$totalCacheKey = $cacheKeyBase . ':total';
|
||||
|
||||
$documentsCacheHit = $totalDocumentsCacheHit = false;
|
||||
|
||||
$cachedDocuments = $dbForProject->getCache()->load($documentsCacheKey, $ttl);
|
||||
|
||||
if ($cachedDocuments !== null &&
|
||||
$cachedDocuments !== false &&
|
||||
\is_array($cachedDocuments)) {
|
||||
$documents = \array_map(function ($doc) {
|
||||
return new Document($doc);
|
||||
}, $cachedDocuments);
|
||||
$documentsCacheHit = true;
|
||||
} else {
|
||||
$documents = $dbForProject->find($collectionTableId, $queries);
|
||||
|
||||
// Convert Document objects to arrays for caching
|
||||
$documentsArray = \array_map(function ($doc) {
|
||||
return $doc->getArrayCopy();
|
||||
}, $documents);
|
||||
$dbForProject->getCache()->save($documentsCacheKey, $documentsArray);
|
||||
}
|
||||
|
||||
if ($includeTotal) {
|
||||
$cachedTotal = $dbForProject->getCache()->load($totalCacheKey, $ttl);
|
||||
if ($cachedTotal !== null && $cachedTotal !== false) {
|
||||
$total = $cachedTotal;
|
||||
$totalDocumentsCacheHit = true;
|
||||
} else {
|
||||
$total = $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT);
|
||||
$dbForProject->getCache()->save($totalCacheKey, $total);
|
||||
}
|
||||
} else {
|
||||
$total = 0;
|
||||
}
|
||||
|
||||
$response->addHeader('X-Appwrite-Cache', $documentsCacheHit ? 'hit' : 'miss');
|
||||
|
||||
} else {
|
||||
// has selects, allow relationship on documents
|
||||
$documents = $dbForProject->find($collectionTableId, $queries);
|
||||
$total = $includeTotal ? $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT) : 0;
|
||||
}
|
||||
|
||||
} else {
|
||||
// has no selects, disable relationship loading on documents
|
||||
/* @type Document[] $documents */
|
||||
@@ -161,7 +228,7 @@ class XList extends Action
|
||||
);
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_READS, max($operations, 1))
|
||||
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations);
|
||||
|
||||
|
||||
+3
-1
@@ -12,6 +12,7 @@ use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Database\Validator\UID;
|
||||
@@ -96,7 +97,8 @@ class Delete extends Action
|
||||
|
||||
// Only update status if removing available index
|
||||
if ($index->getAttribute('status') === 'available') {
|
||||
$index = $dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting'));
|
||||
$index->setAttribute('status', 'deleting');
|
||||
$index = $dbForProject->updateDocument('indexes', $index->getId(), new Document(['status' => 'deleting']));
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
|
||||
|
||||
@@ -60,7 +60,6 @@ class Delete extends Action
|
||||
->inject('dbForProject')
|
||||
->inject('queueForDatabase')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
|
||||
use Appwrite\Databases\TransactionState;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Functions\EventProcessor;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
@@ -73,7 +73,7 @@ class Update extends Action
|
||||
->inject('transactionState')
|
||||
->inject('queueForDeletes')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('queueForRealtime')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForWebhooks')
|
||||
@@ -92,7 +92,7 @@ class Update extends Action
|
||||
* @param TransactionState $transactionState
|
||||
* @param Delete $queueForDeletes
|
||||
* @param Event $queueForEvents
|
||||
* @param StatsUsage $queueForStatsUsage
|
||||
* @param Context $usage
|
||||
* @param Event $queueForRealtime
|
||||
* @param Event $queueForFunctions
|
||||
* @param Event $queueForWebhooks
|
||||
@@ -106,7 +106,7 @@ class Update extends Action
|
||||
* @throws Structure
|
||||
* @throws \Utopia\Http\Exception
|
||||
*/
|
||||
public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, Document $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, Document $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
{
|
||||
if (!$commit && !$rollback) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true');
|
||||
@@ -142,7 +142,7 @@ class Update extends Action
|
||||
$currentDocumentId = null;
|
||||
|
||||
try {
|
||||
$dbForProject->withTransaction(function () use ($dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $queueForStatsUsage, $queueForRealtime, $queueForFunctions, $queueForWebhooks, $authorization) {
|
||||
$dbForProject->withTransaction(function () use ($dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $usage, $queueForRealtime, $queueForFunctions, $queueForWebhooks, $authorization) {
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'committing',
|
||||
])));
|
||||
@@ -279,11 +279,10 @@ class Update extends Action
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $totalOperations);
|
||||
$usage->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $totalOperations);
|
||||
|
||||
foreach ($databaseOperations as $sequence => $count) {
|
||||
$queueForStatsUsage->addMetric(
|
||||
$usage->addMetric(
|
||||
str_replace('{databaseInternalId}', $sequence, METRIC_DATABASE_ID_OPERATIONS_WRITES),
|
||||
$count
|
||||
);
|
||||
|
||||
@@ -50,7 +50,6 @@ class Delete extends DatabaseDelete
|
||||
->inject('dbForProject')
|
||||
->inject('queueForDatabase')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class Delete extends DocumentsDelete
|
||||
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForRealtime')
|
||||
->inject('queueForFunctions')
|
||||
|
||||
@@ -63,7 +63,7 @@ class Update extends DocumentsUpdate
|
||||
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForRealtime')
|
||||
->inject('queueForFunctions')
|
||||
|
||||
@@ -63,7 +63,7 @@ class Upsert extends DocumentsUpsert
|
||||
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForRealtime')
|
||||
->inject('queueForFunctions')
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ class Decrement extends DecrementDocumentAttribute
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ class Increment extends IncrementDocumentAttribute
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
|
||||
@@ -106,7 +106,7 @@ class Create extends DocumentCreate
|
||||
->inject('dbForProject')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('queueForRealtime')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForWebhooks')
|
||||
|
||||
@@ -68,7 +68,7 @@ class Delete extends DocumentDelete
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
|
||||
@@ -57,7 +57,7 @@ class Get extends DocumentGet
|
||||
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID to read uncommitted changes within the transaction.', true, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
|
||||
@@ -66,7 +66,7 @@ class Update extends DocumentUpdate
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
|
||||
@@ -69,7 +69,7 @@ class Upsert extends DocumentUpsert
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
|
||||
@@ -14,6 +14,7 @@ use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class XList extends DocumentXList
|
||||
@@ -56,9 +57,11 @@ class XList extends DocumentXList
|
||||
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
|
||||
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID to read uncommitted changes within the transaction.', true, ['dbForProject'])
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->param('ttl', 0, new Range(min: 0, max: 86400), 'TTL (seconds) for cached responses when caching is enabled for select queries. Must be between 0 and 86400 (24 hours).', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('user')
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
|
||||
@@ -57,7 +57,7 @@ class Update extends TransactionsUpdate
|
||||
->inject('transactionState')
|
||||
->inject('queueForDeletes')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('queueForRealtime')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForWebhooks')
|
||||
|
||||
@@ -322,13 +322,19 @@ class Databases extends Action
|
||||
$dbForProject->updateDocument(
|
||||
'attributes',
|
||||
$attribute->getId(),
|
||||
$attribute->setAttribute('status', 'stuck')
|
||||
new Document([
|
||||
'error' => $attribute->getAttribute('error'),
|
||||
'status' => 'stuck',
|
||||
])
|
||||
);
|
||||
if (!$relatedAttribute->isEmpty()) {
|
||||
$dbForProject->updateDocument(
|
||||
'attributes',
|
||||
$relatedAttribute->getId(),
|
||||
$relatedAttribute->setAttribute('status', 'stuck')
|
||||
new Document([
|
||||
'error' => $relatedAttribute->getAttribute('error'),
|
||||
'status' => 'stuck',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -382,7 +388,11 @@ class Databases extends Action
|
||||
if ($exists) { // Delete the duplicate if created, else update in db
|
||||
$this->deleteIndex($database, $collection, $index, $project, $dbForPlatform, $dbForProject, $queueForRealtime);
|
||||
} else {
|
||||
$dbForProject->updateDocument('indexes', $index->getId(), $index);
|
||||
$dbForProject->updateDocument('indexes', $index->getId(), new Document([
|
||||
'attributes' => $index->getAttribute('attributes'),
|
||||
'lengths' => $index->getAttribute('lengths'),
|
||||
'orders' => $index->getAttribute('orders'),
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +227,9 @@ class Create extends Action
|
||||
|
||||
foreach ($activeDeployments as $activeDeployment) {
|
||||
$activeDeployment->setAttribute('activate', false);
|
||||
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), $activeDeployment);
|
||||
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), new Document([
|
||||
'activate' => false,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,14 +257,17 @@ class Create extends Action
|
||||
'type' => $type
|
||||
]));
|
||||
|
||||
$function = $function
|
||||
->setAttribute('latestDeploymentId', $deployment->getId())
|
||||
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
|
||||
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
|
||||
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function);
|
||||
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document([
|
||||
'latestDeploymentId' => $deployment->getId(),
|
||||
'latestDeploymentInternalId' => $deployment->getSequence(),
|
||||
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
|
||||
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
|
||||
]));
|
||||
} else {
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceSize', $fileSize)->setAttribute('sourceMetadata', $metadata));
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
|
||||
'sourceSize' => $fileSize,
|
||||
'sourceMetadata' => $metadata,
|
||||
]));
|
||||
}
|
||||
|
||||
// Start the build
|
||||
@@ -295,14 +300,17 @@ class Create extends Action
|
||||
'type' => $type
|
||||
]));
|
||||
|
||||
$function = $function
|
||||
->setAttribute('latestDeploymentId', $deployment->getId())
|
||||
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
|
||||
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
|
||||
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function);
|
||||
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document([
|
||||
'latestDeploymentId' => $deployment->getId(),
|
||||
'latestDeploymentInternalId' => $deployment->getSequence(),
|
||||
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
|
||||
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
|
||||
]));
|
||||
} else {
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceChunksUploaded', $chunksUploaded)->setAttribute('sourceMetadata', $metadata));
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
|
||||
'sourceChunksUploaded' => $chunksUploaded,
|
||||
'sourceMetadata' => $metadata,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -107,11 +107,12 @@ class Delete extends Action
|
||||
$function = $dbForProject->updateDocument(
|
||||
'functions',
|
||||
$function->getId(),
|
||||
$function
|
||||
->setAttribute('latestDeploymentCreatedAt', $latestDeployment->isEmpty() ? '' : $latestDeployment->getCreatedAt())
|
||||
->setAttribute('latestDeploymentInternalId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getSequence())
|
||||
->setAttribute('latestDeploymentId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getId())
|
||||
->setAttribute('latestDeploymentStatus', $latestDeployment->isEmpty() ? '' : $latestDeployment->getAttribute('status', ''))
|
||||
new Document([
|
||||
'latestDeploymentCreatedAt' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getCreatedAt(),
|
||||
'latestDeploymentInternalId' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getSequence(),
|
||||
'latestDeploymentId' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getId(),
|
||||
'latestDeploymentStatus' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getAttribute('status', ''),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ 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\Helpers\ID;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
@@ -119,7 +120,12 @@ class Create extends Action
|
||||
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
|
||||
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
|
||||
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function);
|
||||
$dbForProject->updateDocument('functions', $function->getId(), new Document([
|
||||
'latestDeploymentId' => $function->getAttribute('latestDeploymentId'),
|
||||
'latestDeploymentInternalId' => $function->getAttribute('latestDeploymentInternalId'),
|
||||
'latestDeploymentCreatedAt' => $function->getAttribute('latestDeploymentCreatedAt'),
|
||||
'latestDeploymentStatus' => $function->getAttribute('latestDeploymentStatus'),
|
||||
]));
|
||||
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
|
||||
@@ -91,7 +91,7 @@ class Update extends Action
|
||||
$endTime = new \DateTime('now');
|
||||
$duration = $endTime->getTimestamp() - $startTime->getTimestamp();
|
||||
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttributes([
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
|
||||
'buildEndedAt' => DateTime::now(),
|
||||
'buildDuration' => $duration,
|
||||
'status' => 'canceled'
|
||||
@@ -99,7 +99,9 @@ class Update extends Action
|
||||
|
||||
if ($deployment->getSequence() === $function->getAttribute('latestDeploymentInternalId', '')) {
|
||||
$function = $function->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function);
|
||||
$dbForProject->updateDocument('functions', $function->getId(), new Document([
|
||||
'latestDeploymentStatus' => $function->getAttribute('latestDeploymentStatus'),
|
||||
]));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -174,7 +174,12 @@ class Create extends Base
|
||||
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
|
||||
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
|
||||
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function);
|
||||
$dbForProject->updateDocument('functions', $function->getId(), new Document([
|
||||
'latestDeploymentId' => $function->getAttribute('latestDeploymentId'),
|
||||
'latestDeploymentInternalId' => $function->getAttribute('latestDeploymentInternalId'),
|
||||
'latestDeploymentCreatedAt' => $function->getAttribute('latestDeploymentCreatedAt'),
|
||||
'latestDeploymentStatus' => $function->getAttribute('latestDeploymentStatus'),
|
||||
]));
|
||||
|
||||
|
||||
$this->updateEmptyManualRule($project, $function, $deployment, $dbForPlatform, $authorization);
|
||||
|
||||
@@ -6,7 +6,6 @@ use Ahc\Jwt\JWT;
|
||||
use Appwrite\Event\Delete as DeleteEvent;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\Functions\Validator\Headers;
|
||||
@@ -15,6 +14,7 @@ use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Usage\Context;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Executor\Executor;
|
||||
@@ -93,7 +93,7 @@ class Create extends Base
|
||||
->inject('dbForPlatform')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('usage')
|
||||
->inject('queueForFunctions')
|
||||
->inject('geodb')
|
||||
->inject('store')
|
||||
@@ -121,7 +121,7 @@ class Create extends Base
|
||||
Database $dbForPlatform,
|
||||
Document $user,
|
||||
Event $queueForEvents,
|
||||
StatsUsage $queueForStatsUsage,
|
||||
Context $usage,
|
||||
Func $queueForFunctions,
|
||||
Reader $geodb,
|
||||
Store $store,
|
||||
@@ -180,7 +180,8 @@ class Create extends Base
|
||||
|
||||
$version = $function->getAttribute('version', 'v2');
|
||||
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
|
||||
$spec = Config::getParam('specifications')[$function->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
|
||||
|
||||
$spec = Config::getParam('specifications')[$function->getAttribute('runtimeSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
|
||||
|
||||
$runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null;
|
||||
|
||||
@@ -420,6 +421,11 @@ class Create extends Base
|
||||
try {
|
||||
$version = $function->getAttribute('version', 'v2');
|
||||
$command = $runtime['startCommand'];
|
||||
|
||||
if (!empty($deployment->getAttribute('startCommand', ''))) {
|
||||
$command = 'cd /usr/local/server/src/function/ && ' . $deployment->getAttribute('startCommand', '');
|
||||
}
|
||||
|
||||
$source = $deployment->getAttribute('buildPath', '');
|
||||
$extension = str_ends_with($source, '.tar') ? 'tar' : 'tar.gz';
|
||||
$command = $version === 'v2' ? '' : "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$command\"";
|
||||
@@ -493,7 +499,7 @@ class Create extends Base
|
||||
throw $th;
|
||||
}
|
||||
} finally {
|
||||
$queueForStatsUsage
|
||||
$usage
|
||||
->addMetric(METRIC_EXECUTIONS, 1)
|
||||
->addMetric(str_replace(['{resourceType}'], [RESOURCE_TYPE_FUNCTIONS], METRIC_RESOURCE_TYPE_EXECUTIONS), 1)
|
||||
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS), 1)
|
||||
|
||||
@@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
@@ -90,6 +91,15 @@ class Delete extends Base
|
||||
}
|
||||
$status = $execution->getAttribute('status');
|
||||
|
||||
// Treat timed-out executions as failed so they can be deleted.
|
||||
if ($status === 'waiting' || $status === 'processing') {
|
||||
$timeout = $function->getAttribute('timeout', 900);
|
||||
$elapsed = \time() - \strtotime($execution->getCreatedAt());
|
||||
if ($elapsed >= $timeout) {
|
||||
$status = 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
if (!in_array($status, ['completed', 'failed', 'scheduled'])) {
|
||||
throw new Exception(Exception::EXECUTION_IN_PROGRESS);
|
||||
}
|
||||
@@ -110,7 +120,10 @@ class Delete extends Base
|
||||
->setAttribute('resourceUpdatedAt', DateTime::now())
|
||||
->setAttribute('active', false);
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([
|
||||
'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'),
|
||||
'active' => $schedule->getAttribute('active'),
|
||||
])));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,16 @@ class Get extends Base
|
||||
throw new Exception(Exception::EXECUTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Override status in response if the execution is stuck in waiting/processing beyond the function timeout.
|
||||
$status = $execution->getAttribute('status', '');
|
||||
if ($status === 'waiting' || $status === 'processing') {
|
||||
$timeout = $function->getAttribute('timeout', 900);
|
||||
$elapsed = \time() - \strtotime($execution->getCreatedAt());
|
||||
if ($elapsed >= $timeout) {
|
||||
$execution->setAttribute('status', 'failed');
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic($execution, Response::MODEL_EXECUTION);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user