mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
286
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bfc7eac48 | ||
|
|
884b8f10bb | ||
|
|
530d681c48 | ||
|
|
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,651 @@
|
||||
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 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"
|
||||
|
||||
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 files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number,
|
||||
});
|
||||
|
||||
const lockFile = files.find(f => f.filename === 'composer.lock');
|
||||
const databaseChanged = lockFile?.patch?.includes('"name": "utopia-php/database"') ?? false;
|
||||
|
||||
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,
|
||||
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|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
|
||||
|
||||
+7
-7
@@ -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
|
||||
@@ -661,7 +661,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 && \
|
||||
|
||||
@@ -318,6 +318,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' => '',
|
||||
|
||||
@@ -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'),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3145,7 +3159,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 +3814,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 +4144,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());
|
||||
|
||||
@@ -4342,7 +4360,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 +4518,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
|
||||
|
||||
@@ -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;
|
||||
@@ -21,12 +22,14 @@ 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;
|
||||
@@ -211,6 +214,8 @@ Http::init()
|
||||
}
|
||||
|
||||
if (!$dbKey) {
|
||||
\var_dump($apiKey);
|
||||
\var_dump($request->getHeader('x-appwrite-key', ''));
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@@ -357,8 +362,9 @@ Http::init()
|
||||
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()
|
||||
])));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,9 +374,13 @@ Http::init()
|
||||
$user->setAttribute('accessedAt', DateTime::now());
|
||||
|
||||
if ($project->getId() !== 'console' && APP_MODE_ADMIN !== $mode) {
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
$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')
|
||||
])));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -646,7 +656,9 @@ Http::init()
|
||||
$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')
|
||||
])));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -745,7 +757,8 @@ 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')
|
||||
->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, Bus $bus) use ($parseLabel) {
|
||||
|
||||
$responsePayload = $response->getPayload();
|
||||
|
||||
@@ -929,17 +942,24 @@ Http::shutdown()
|
||||
$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']);
|
||||
}
|
||||
@@ -952,16 +972,11 @@ 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());
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
response: $response,
|
||||
));
|
||||
}
|
||||
|
||||
$queueForStatsUsage
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
+10
-7
@@ -10,7 +10,6 @@ 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;
|
||||
@@ -160,9 +159,6 @@ Http::setResource('queueForAudits', function (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();
|
||||
}, []);
|
||||
@@ -1239,7 +1235,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());
|
||||
}
|
||||
|
||||
@@ -1256,7 +1254,10 @@ Http::setResource('devKey', function (Request $request, Document $project, array
|
||||
|
||||
/** 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());
|
||||
}
|
||||
}
|
||||
@@ -1413,7 +1414,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([
|
||||
|
||||
+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);
|
||||
|
||||
|
||||
+4
-4
@@ -9,7 +9,6 @@ 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;
|
||||
@@ -355,9 +354,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();
|
||||
@@ -542,6 +538,10 @@ try {
|
||||
|
||||
$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')
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
exec php /usr/src/code/app/cli.php time-travel "$@"
|
||||
+5
-2
@@ -19,7 +19,8 @@
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Appwrite\\": "src/Appwrite",
|
||||
"Executor\\": "src/Executor"
|
||||
"Executor\\": "src/Executor",
|
||||
"Utopia\\Bus\\": "src/Utopia/Bus"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
@@ -69,13 +70,14 @@
|
||||
"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/registry": "0.5.*",
|
||||
"utopia-php/servers": "0.2.5",
|
||||
"utopia-php/storage": "1.0.*",
|
||||
"utopia-php/system": "0.10.*",
|
||||
"utopia-php/telemetry": "0.2.*",
|
||||
@@ -99,6 +101,7 @@
|
||||
"swoole/ide-helper": "6.*",
|
||||
"phpstan/phpstan": "1.12.*",
|
||||
"textalk/websocket": "1.5.*",
|
||||
"czproject/git-php": "4.*",
|
||||
"laravel/pint": "1.*",
|
||||
"phpbench/phpbench": "1.*"
|
||||
},
|
||||
|
||||
Generated
+186
-120
@@ -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": "f6347f8d93e6d17ce90ae0a1e0c8615f",
|
||||
"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",
|
||||
@@ -3606,16 +3606,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/cache",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/cache.git",
|
||||
"reference": "7068870c086a6aea16173563a26b93ef3e408439"
|
||||
"reference": "05ceba981436a4022553f7aaa2a05fa049d0f71c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/cache/zipball/7068870c086a6aea16173563a26b93ef3e408439",
|
||||
"reference": "7068870c086a6aea16173563a26b93ef3e408439",
|
||||
"url": "https://api.github.com/repos/utopia-php/cache/zipball/05ceba981436a4022553f7aaa2a05fa049d0f71c",
|
||||
"reference": "05ceba981436a4022553f7aaa2a05fa049d0f71c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3652,9 +3652,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/cache/issues",
|
||||
"source": "https://github.com/utopia-php/cache/tree/1.0.0"
|
||||
"source": "https://github.com/utopia-php/cache/tree/1.0.1"
|
||||
},
|
||||
"time": "2026-01-28T10:55:44+00:00"
|
||||
"time": "2026-03-12T03:39:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/cli",
|
||||
@@ -3850,16 +3850,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "5.3.4",
|
||||
"version": "5.3.12",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "ba1ee9cb2c7624d0fada782b285bd9958a07bbe5"
|
||||
"reference": "7e843989e67b1de3bfb27212334c28694bda4e95"
|
||||
},
|
||||
"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/7e843989e67b1de3bfb27212334c28694bda4e95",
|
||||
"reference": "7e843989e67b1de3bfb27212334c28694bda4e95",
|
||||
"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.12"
|
||||
},
|
||||
"time": "2026-02-24T00:37:36+00:00"
|
||||
"time": "2026-03-13T12:16:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -4058,16 +4058,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/domains",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/domains.git",
|
||||
"reference": "b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6"
|
||||
"reference": "0edf6bb2b07f30db849a267027077bf5abb994c6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/domains/zipball/b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6",
|
||||
"reference": "b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6",
|
||||
"url": "https://api.github.com/repos/utopia-php/domains/zipball/0edf6bb2b07f30db849a267027077bf5abb994c6",
|
||||
"reference": "0edf6bb2b07f30db849a267027077bf5abb994c6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4114,9 +4114,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/domains/issues",
|
||||
"source": "https://github.com/utopia-php/domains/tree/1.0.2"
|
||||
"source": "https://github.com/utopia-php/domains/tree/1.0.5"
|
||||
},
|
||||
"time": "2026-02-25T08:18:25+00:00"
|
||||
"time": "2026-03-03T09:20:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/dsn",
|
||||
@@ -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,22 +4566,22 @@
|
||||
],
|
||||
"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",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/mongo.git",
|
||||
"reference": "45bedf36c2c946ec7a0a3e59b9f12f772de0b01d"
|
||||
"reference": "83dbcde768d5fb40241f5ca8aa5ed8ca140a7469"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/mongo/zipball/45bedf36c2c946ec7a0a3e59b9f12f772de0b01d",
|
||||
"reference": "45bedf36c2c946ec7a0a3e59b9f12f772de0b01d",
|
||||
"url": "https://api.github.com/repos/utopia-php/mongo/zipball/83dbcde768d5fb40241f5ca8aa5ed8ca140a7469",
|
||||
"reference": "83dbcde768d5fb40241f5ca8aa5ed8ca140a7469",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4627,9 +4627,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/mongo/issues",
|
||||
"source": "https://github.com/utopia-php/mongo/tree/1.0.0"
|
||||
"source": "https://github.com/utopia-php/mongo/tree/1.0.1"
|
||||
},
|
||||
"time": "2026-02-12T05:54:06+00:00"
|
||||
"time": "2026-03-13T07:29:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/platform",
|
||||
@@ -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",
|
||||
@@ -5215,16 +5215,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/vcs",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/vcs.git",
|
||||
"reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14"
|
||||
"reference": "92a1650824ba0c5e6a1bc46e622ac87c50a08920"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/058049326e04a2a0c2f0ce8ad00c7e84825aba14",
|
||||
"reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14",
|
||||
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/92a1650824ba0c5e6a1bc46e622ac87c50a08920",
|
||||
"reference": "92a1650824ba0c5e6a1bc46e622ac87c50a08920",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5258,9 +5258,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/vcs/issues",
|
||||
"source": "https://github.com/utopia-php/vcs/tree/2.0.0"
|
||||
"source": "https://github.com/utopia-php/vcs/tree/2.0.1"
|
||||
},
|
||||
"time": "2026-02-25T11:36:45+00:00"
|
||||
"time": "2026-02-27T12:18:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/websocket",
|
||||
@@ -5438,16 +5438,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.11.1",
|
||||
"version": "1.11.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb"
|
||||
"reference": "f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6ff411f26f2750eea05c7598c14bb3a2ada898cb",
|
||||
"reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38",
|
||||
"reference": "f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5483,22 +5483,22 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.11.1"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.11.6"
|
||||
},
|
||||
"time": "2026-02-25T07:15:19+00:00"
|
||||
"time": "2026-03-09T07:12:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
"version": "v7.19.0",
|
||||
"version": "v7.19.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/paratestphp/paratest.git",
|
||||
"reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6"
|
||||
"reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6",
|
||||
"reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6",
|
||||
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9",
|
||||
"reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5512,9 +5512,9 @@
|
||||
"phpunit/php-code-coverage": "^12.5.3 || ^13.0.1",
|
||||
"phpunit/php-file-iterator": "^6.0.1 || ^7",
|
||||
"phpunit/php-timer": "^8 || ^9",
|
||||
"phpunit/phpunit": "^12.5.9 || ^13",
|
||||
"phpunit/phpunit": "^12.5.14 || ^13.0.5",
|
||||
"sebastian/environment": "^8.0.3 || ^9",
|
||||
"symfony/console": "^7.4.4 || ^8.0.4",
|
||||
"symfony/console": "^7.4.7 || ^8.0.7",
|
||||
"symfony/process": "^7.4.5 || ^8.0.5"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -5522,11 +5522,11 @@
|
||||
"ext-pcntl": "*",
|
||||
"ext-pcov": "*",
|
||||
"ext-posix": "*",
|
||||
"phpstan/phpstan": "^2.1.38",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0.3",
|
||||
"phpstan/phpstan-phpunit": "^2.0.12",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.8",
|
||||
"symfony/filesystem": "^7.4.0 || ^8.0.1"
|
||||
"phpstan/phpstan": "^2.1.40",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0.4",
|
||||
"phpstan/phpstan-phpunit": "^2.0.16",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.10",
|
||||
"symfony/filesystem": "^7.4.6 || ^8.0.6"
|
||||
},
|
||||
"bin": [
|
||||
"bin/paratest",
|
||||
@@ -5566,7 +5566,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/paratestphp/paratest/issues",
|
||||
"source": "https://github.com/paratestphp/paratest/tree/v7.19.0"
|
||||
"source": "https://github.com/paratestphp/paratest/tree/v7.19.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5578,7 +5578,71 @@
|
||||
"type": "paypal"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-06T10:53:26+00:00"
|
||||
"time": "2026-03-09T14:33:17+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",
|
||||
@@ -5857,16 +5921,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/pint",
|
||||
"version": "v1.27.1",
|
||||
"version": "v1.29.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/pint.git",
|
||||
"reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5"
|
||||
"reference": "bdec963f53172c5e36330f3a400604c69bf02d39"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/pint/zipball/54cca2de13790570c7b6f0f94f37896bee4abcb5",
|
||||
"reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5",
|
||||
"url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39",
|
||||
"reference": "bdec963f53172c5e36330f3a400604c69bf02d39",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5877,13 +5941,14 @@
|
||||
"php": "^8.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.93.1",
|
||||
"illuminate/view": "^12.51.0",
|
||||
"larastan/larastan": "^3.9.2",
|
||||
"friendsofphp/php-cs-fixer": "^3.94.2",
|
||||
"illuminate/view": "^12.54.1",
|
||||
"larastan/larastan": "^3.9.3",
|
||||
"laravel-zero/framework": "^12.0.5",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"nunomaduro/termwind": "^2.3.3",
|
||||
"pestphp/pest": "^3.8.5"
|
||||
"nunomaduro/termwind": "^2.4.0",
|
||||
"pestphp/pest": "^3.8.6",
|
||||
"shipfastlabs/agent-detector": "^1.1.0"
|
||||
},
|
||||
"bin": [
|
||||
"builds/pint"
|
||||
@@ -5920,7 +5985,7 @@
|
||||
"issues": "https://github.com/laravel/pint/issues",
|
||||
"source": "https://github.com/laravel/pint"
|
||||
},
|
||||
"time": "2026-02-10T20:00:20+00:00"
|
||||
"time": "2026-03-12T15:51:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "matthiasmullie/minify",
|
||||
@@ -6334,16 +6399,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpbench/phpbench",
|
||||
"version": "1.4.3",
|
||||
"version": "1.5.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpbench/phpbench.git",
|
||||
"reference": "b641dde59d969ea42eed70a39f9b51950bc96878"
|
||||
"reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpbench/phpbench/zipball/b641dde59d969ea42eed70a39f9b51950bc96878",
|
||||
"reference": "b641dde59d969ea42eed70a39f9b51950bc96878",
|
||||
"url": "https://api.github.com/repos/phpbench/phpbench/zipball/9a28fd0833f11171b949843c6fd663eb69b6d14c",
|
||||
"reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6354,7 +6419,7 @@
|
||||
"ext-reflection": "*",
|
||||
"ext-spl": "*",
|
||||
"ext-tokenizer": "*",
|
||||
"php": "^8.1",
|
||||
"php": "^8.2",
|
||||
"phpbench/container": "^2.2",
|
||||
"psr/log": "^1.1 || ^2.0 || ^3.0",
|
||||
"seld/jsonlint": "^1.1",
|
||||
@@ -6374,8 +6439,9 @@
|
||||
"phpstan/extension-installer": "^1.1",
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"phpunit/phpunit": "^10.4 || ^11.0",
|
||||
"phpunit/phpunit": "^11.5",
|
||||
"rector/rector": "^1.2",
|
||||
"sebastian/exporter": "^6.3.2",
|
||||
"symfony/error-handler": "^6.1 || ^7.0 || ^8.0",
|
||||
"symfony/var-dumper": "^6.1 || ^7.0 || ^8.0"
|
||||
},
|
||||
@@ -6420,7 +6486,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpbench/phpbench/issues",
|
||||
"source": "https://github.com/phpbench/phpbench/tree/1.4.3"
|
||||
"source": "https://github.com/phpbench/phpbench/tree/1.5.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6428,15 +6494,15 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-06T19:07:31+00:00"
|
||||
"time": "2026-03-05T08:18:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "1.12.32",
|
||||
"version": "1.12.33",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8",
|
||||
"reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/37982d6fc7cbb746dda7773530cda557cdf119e1",
|
||||
"reference": "37982d6fc7cbb746dda7773530cda557cdf119e1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6481,7 +6547,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-09-30T10:16:31+00:00"
|
||||
"time": "2026-02-28T20:30:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
@@ -8031,16 +8097,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v8.0.4",
|
||||
"version": "v8.0.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b"
|
||||
"reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b",
|
||||
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
|
||||
"reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -8097,7 +8163,7 @@
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v8.0.4"
|
||||
"source": "https://github.com/symfony/console/tree/v8.0.7"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8117,20 +8183,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-13T13:06:50+00:00"
|
||||
"time": "2026-03-06T14:06:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v8.0.1",
|
||||
"version": "v8.0.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "d937d400b980523dc9ee946bb69972b5e619058d"
|
||||
"reference": "7bf9162d7a0dff98d079b72948508fa48018a770"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d",
|
||||
"reference": "d937d400b980523dc9ee946bb69972b5e619058d",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770",
|
||||
"reference": "7bf9162d7a0dff98d079b72948508fa48018a770",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -8167,7 +8233,7 @@
|
||||
"description": "Provides basic utilities for the filesystem",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/filesystem/tree/v8.0.1"
|
||||
"source": "https://github.com/symfony/filesystem/tree/v8.0.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8187,20 +8253,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-12-01T09:13:36+00:00"
|
||||
"time": "2026-02-25T16:59:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v8.0.5",
|
||||
"version": "v8.0.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0"
|
||||
"reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0",
|
||||
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c",
|
||||
"reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -8235,7 +8301,7 @@
|
||||
"description": "Finds files and directories via an intuitive fluent interface",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/finder/tree/v8.0.5"
|
||||
"source": "https://github.com/symfony/finder/tree/v8.0.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8255,7 +8321,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-26T15:08:38+00:00"
|
||||
"time": "2026-01-29T09:41:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
@@ -8725,16 +8791,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
"version": "v8.0.4",
|
||||
"version": "v8.0.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "758b372d6882506821ed666032e43020c4f57194"
|
||||
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194",
|
||||
"reference": "758b372d6882506821ed666032e43020c4f57194",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
|
||||
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -8791,7 +8857,7 @@
|
||||
"utf8"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/string/tree/v8.0.4"
|
||||
"source": "https://github.com/symfony/string/tree/v8.0.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8811,7 +8877,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-12T12:37:40+00:00"
|
||||
"time": "2026-02-09T10:14:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "textalk/websocket",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
parameters:
|
||||
level: 8
|
||||
paths:
|
||||
- src/Utopia/Bus
|
||||
- src/Appwrite/Bus
|
||||
- src/Appwrite/Transformation
|
||||
bootstrapFiles:
|
||||
- app/init/constants.php
|
||||
scanDirectories:
|
||||
- vendor/swoole/ide-helper
|
||||
excludePaths:
|
||||
|
||||
@@ -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,114 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Bus\Listeners;
|
||||
|
||||
use Appwrite\Bus\Events\ExecutionCompleted;
|
||||
use Appwrite\Bus\Events\RequestCompleted;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Utopia\Bus\Event;
|
||||
use Utopia\Bus\Listener;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Queue\Publisher;
|
||||
|
||||
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('publisherStatsUsage')
|
||||
->callback($this->handle(...));
|
||||
}
|
||||
|
||||
public function handle(Event $event, Publisher $publisher): void
|
||||
{
|
||||
match (true) {
|
||||
$event instanceof ExecutionCompleted => $this->handleExecutionCompleted($event, $publisher),
|
||||
$event instanceof RequestCompleted => $this->handleRequestCompleted($event, $publisher),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function handleExecutionCompleted(ExecutionCompleted $event, Publisher $publisher): 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));
|
||||
|
||||
$queueForStatsUsage = new StatsUsage($publisher);
|
||||
$queueForStatsUsage
|
||||
->setProject($project)
|
||||
->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)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
private function handleRequestCompleted(RequestCompleted $event, Publisher $publisher): 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'];
|
||||
}
|
||||
|
||||
$project = new Document($event->project);
|
||||
$deployment = new Document($event->deployment);
|
||||
$queueForStatsUsage = new StatsUsage($publisher);
|
||||
|
||||
$inbound = $event->request->getSize() + $fileSize;
|
||||
$outbound = $event->response->getSize();
|
||||
|
||||
$queueForStatsUsage->setProject($project);
|
||||
|
||||
if ($deployment->getAttribute('resourceType') === 'sites') {
|
||||
$siteInternalId = $deployment->getAttribute('resourceInternalId', '');
|
||||
$queueForStatsUsage
|
||||
->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 {
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_NETWORK_REQUESTS, 1)
|
||||
->addMetric(METRIC_NETWORK_INBOUND, $inbound)
|
||||
->addMetric(METRIC_NETWORK_OUTBOUND, $outbound);
|
||||
}
|
||||
|
||||
$queueForStatsUsage->trigger();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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']);
|
||||
|
||||
+71
-4
@@ -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('user')
|
||||
->inject('queueForStatsUsage')
|
||||
->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, StatsUsage $queueForStatsUsage, 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 */
|
||||
|
||||
+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);
|
||||
|
||||
@@ -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,8 +57,10 @@ 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('user')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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\"";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Executions;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Order as OrderException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
@@ -110,6 +111,35 @@ class XList extends Base
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
// Calculate the cutoff datetime before which a waiting/processing execution is considered timed out.
|
||||
$timeout = $function->getAttribute('timeout', 900);
|
||||
$thresholdDate = new \DateTime("-{$timeout} seconds");
|
||||
$threshold = DateTime::format($thresholdDate);
|
||||
|
||||
// Capture what statuses the caller explicitly requested, before we mutate the query.
|
||||
$requestedStatuses = [];
|
||||
foreach ($queries as $query) {
|
||||
if ($query->getMethod() === Query::TYPE_EQUAL && $query->getAttribute() === 'status') {
|
||||
$requestedStatuses = [...$requestedStatuses, ...$query->getValues()];
|
||||
}
|
||||
}
|
||||
|
||||
// If the caller is filtering by 'failed', expand the DB query to also return
|
||||
// waiting/processing executions created before the timeout threshold, so timed-out
|
||||
// executions that were never marked failed in the DB are included in the results.
|
||||
foreach ($queries as $index => $query) {
|
||||
if ($query->getMethod() === Query::TYPE_EQUAL && $query->getAttribute() === 'status' && \in_array('failed', $query->getValues())) {
|
||||
$queries[$index] = Query::or([
|
||||
$query,
|
||||
Query::and([
|
||||
Query::equal('status', ['waiting', 'processing']),
|
||||
Query::createdBefore($threshold),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
try {
|
||||
@@ -119,6 +149,20 @@ class XList extends Base
|
||||
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
|
||||
}
|
||||
|
||||
// Override status in response for timed-out executions, but only when the caller
|
||||
// did not explicitly request a non-failed status (e.g. waiting/processing).
|
||||
if (empty(\array_diff($requestedStatuses, ['failed']))) {
|
||||
foreach ($results as $execution) {
|
||||
$status = $execution->getAttribute('status', '');
|
||||
if ($status === 'waiting' || $status === 'processing') {
|
||||
$elapsed = \time() - \strtotime($execution->getCreatedAt());
|
||||
if ($elapsed >= $timeout) {
|
||||
$execution->setAttribute('status', 'failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'executions' => $results,
|
||||
'total' => $total,
|
||||
|
||||
@@ -93,16 +93,23 @@ class Create extends Base
|
||||
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function.', true)
|
||||
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true)
|
||||
->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true)
|
||||
->param('specification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
|
||||
->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
|
||||
$plan,
|
||||
Config::getParam('specifications', []),
|
||||
System::getEnv('_APP_COMPUTE_CPUS', 0),
|
||||
System::getEnv('_APP_COMPUTE_MEMORY', 0)
|
||||
), 'Runtime specification for the function and builds.', true, ['plan'])
|
||||
), 'Build specification for the function deployments.', true, ['plan'])
|
||||
->param('runtimeSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
|
||||
$plan,
|
||||
Config::getParam('specifications', []),
|
||||
System::getEnv('_APP_COMPUTE_CPUS', 0),
|
||||
System::getEnv('_APP_COMPUTE_MEMORY', 0)
|
||||
), 'Runtime specification for the function executions.', true, ['plan'])
|
||||
->param('templateRepository', '', new Text(128, 0), 'Repository name of the template.', true, deprecated: true)
|
||||
->param('templateOwner', '', new Text(128, 0), 'The name of the owner of the template.', true, deprecated: true)
|
||||
->param('templateRootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.', true, deprecated: true)
|
||||
->param('templateVersion', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true, deprecated: true)
|
||||
->param('deploymentRetention', 0, new Range(0, APP_COMPUTE_DEPLOYMENT_MAX_RETENTION), 'Days to keep non-active deployments before deletion. Value 0 means all deployments will be kept.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('timelimit')
|
||||
@@ -138,11 +145,13 @@ class Create extends Base
|
||||
string $providerBranch,
|
||||
bool $providerSilentMode,
|
||||
string $providerRootDirectory,
|
||||
string $specification,
|
||||
string $buildSpecification,
|
||||
string $runtimeSpecification,
|
||||
string $templateRepository,
|
||||
string $templateOwner,
|
||||
string $templateRootDirectory,
|
||||
string $templateVersion,
|
||||
int $deploymentRetention,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
callable $timelimit,
|
||||
@@ -215,6 +224,7 @@ class Create extends Base
|
||||
'logging' => $logging,
|
||||
'name' => $name,
|
||||
'runtime' => $runtime,
|
||||
'deploymentRetention' => $deploymentRetention,
|
||||
'deploymentInternalId' => '',
|
||||
'deploymentId' => '',
|
||||
'events' => $events,
|
||||
@@ -225,7 +235,6 @@ class Create extends Base
|
||||
'entrypoint' => $entrypoint,
|
||||
'commands' => $commands,
|
||||
'scopes' => $scopes,
|
||||
'deploymentRetention' => 0,
|
||||
'startCommand' => '',
|
||||
'search' => implode(' ', [$functionId, $name, $runtime]),
|
||||
'version' => 'v5',
|
||||
@@ -237,9 +246,8 @@ class Create extends Base
|
||||
'providerBranch' => $providerBranch,
|
||||
'providerRootDirectory' => $providerRootDirectory,
|
||||
'providerSilentMode' => $providerSilentMode,
|
||||
'specification' => $specification,
|
||||
'buildSpecification' => $specification,
|
||||
'runtimeSpecification' => $specification,
|
||||
'buildSpecification' => $buildSpecification,
|
||||
'runtimeSpecification' => $runtimeSpecification,
|
||||
]));
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::FUNCTION_ALREADY_EXISTS);
|
||||
@@ -283,7 +291,12 @@ class Create extends Base
|
||||
$function->setAttribute('repositoryInternalId', $repository->getSequence());
|
||||
}
|
||||
|
||||
$function = $dbForProject->updateDocument('functions', $function->getId(), $function);
|
||||
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document([
|
||||
'scheduleId' => $function->getAttribute('scheduleId'),
|
||||
'scheduleInternalId' => $function->getAttribute('scheduleInternalId'),
|
||||
'repositoryId' => $function->getAttribute('repositoryId'),
|
||||
'repositoryInternalId' => $function->getAttribute('repositoryInternalId'),
|
||||
]));
|
||||
|
||||
// Backwards compatibility with 1.6 behaviour
|
||||
$requestFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
@@ -321,12 +334,12 @@ class Create extends Base
|
||||
referenceType: 'branch'
|
||||
);
|
||||
|
||||
$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', ''),
|
||||
]));
|
||||
} elseif (!$template->isEmpty()) {
|
||||
// Deploy non-VCS from template
|
||||
$deploymentId = ID::unique();
|
||||
@@ -347,12 +360,12 @@ class Create extends Base
|
||||
'activate' => true,
|
||||
]));
|
||||
|
||||
$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', ''),
|
||||
]));
|
||||
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
|
||||
@@ -13,6 +13,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\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
@@ -90,7 +91,10 @@ class Delete extends Base
|
||||
$schedule
|
||||
->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'),
|
||||
])));
|
||||
}
|
||||
|
||||
$queueForDeletes
|
||||
|
||||
@@ -103,7 +103,11 @@ class Update extends Base
|
||||
->setAttribute('resourceUpdatedAt', DateTime::now())
|
||||
->setAttribute('schedule', $function->getAttribute('schedule'))
|
||||
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId')));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([
|
||||
'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'),
|
||||
'schedule' => $schedule->getAttribute('schedule'),
|
||||
'active' => $schedule->getAttribute('active'),
|
||||
])));
|
||||
|
||||
$queries = [
|
||||
Query::equal('trigger', ['manual']),
|
||||
@@ -119,7 +123,10 @@ class Update extends Base
|
||||
->setAttribute('deploymentId', $deployment->getId())
|
||||
->setAttribute('deploymentInternalId', $deployment->getSequence());
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), $rule));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
|
||||
'deploymentId' => $rule->getAttribute('deploymentId'),
|
||||
'deploymentInternalId' => $rule->getAttribute('deploymentInternalId'),
|
||||
])));
|
||||
}, $queries));
|
||||
|
||||
$queueForEvents
|
||||
|
||||
@@ -87,12 +87,19 @@ class Update extends Base
|
||||
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function', true)
|
||||
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true)
|
||||
->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true)
|
||||
->param('specification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
|
||||
->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
|
||||
$plan,
|
||||
Config::getParam('specifications', []),
|
||||
System::getEnv('_APP_COMPUTE_CPUS', 0),
|
||||
System::getEnv('_APP_COMPUTE_MEMORY', 0)
|
||||
), 'Runtime specification for the function and builds.', true, ['plan'])
|
||||
), 'Build specification for the function deployments.', true, ['plan'])
|
||||
->param('runtimeSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
|
||||
$plan,
|
||||
Config::getParam('specifications', []),
|
||||
System::getEnv('_APP_COMPUTE_CPUS', 0),
|
||||
System::getEnv('_APP_COMPUTE_MEMORY', 0)
|
||||
), 'Runtime specification for the function executions.', true, ['plan'])
|
||||
->param('deploymentRetention', 0, new Range(0, APP_COMPUTE_DEPLOYMENT_MAX_RETENTION), 'Days to keep non-active deployments before deletion. Value 0 means all deployments will be kept.', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
@@ -124,7 +131,9 @@ class Update extends Base
|
||||
string $providerBranch,
|
||||
bool $providerSilentMode,
|
||||
string $providerRootDirectory,
|
||||
string $specification,
|
||||
string $buildSpecification,
|
||||
string $runtimeSpecification,
|
||||
int $deploymentRetention,
|
||||
Request $request,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
@@ -209,7 +218,7 @@ class Update extends Base
|
||||
'resourceId' => $function->getId(),
|
||||
'resourceInternalId' => $function->getSequence(),
|
||||
'resourceType' => 'function',
|
||||
'providerPullRequestIds' => []
|
||||
'providerPullRequestIds' => [],
|
||||
]));
|
||||
|
||||
$repositoryId = $repository->getId();
|
||||
@@ -229,13 +238,22 @@ class Update extends Base
|
||||
}
|
||||
|
||||
// Enforce Cold Start if spec limits change.
|
||||
if ($function->getAttribute('specification') !== $specification && !empty($function->getAttribute('deploymentId'))) {
|
||||
try {
|
||||
$executor->deleteRuntime($project->getId(), $function->getAttribute('deploymentId'));
|
||||
} catch (\Throwable $th) {
|
||||
// Don't throw if the deployment doesn't exist
|
||||
if ($th->getCode() !== 404) {
|
||||
throw $th;
|
||||
if (!empty($function->getAttribute('deploymentId'))) {
|
||||
$specsChanged = false;
|
||||
if ($function->getAttribute('runtimeSpecification', '') !== $runtimeSpecification) {
|
||||
$specsChanged = true;
|
||||
} elseif ($function->getAttribute('buildSpecification', '') !== $buildSpecification) {
|
||||
$specsChanged = true;
|
||||
}
|
||||
|
||||
if ($specsChanged) {
|
||||
try {
|
||||
$executor->deleteRuntime($project->getId(), $function->getAttribute('deploymentId'));
|
||||
} catch (\Throwable $th) {
|
||||
// Don't throw if the deployment doesn't exist
|
||||
if ($th->getCode() !== 404) {
|
||||
throw $th;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,8 +271,7 @@ class Update extends Base
|
||||
'entrypoint' => $entrypoint,
|
||||
'commands' => $commands,
|
||||
'scopes' => $scopes,
|
||||
'deploymentRetention' => 0,
|
||||
'startCommand' => '',
|
||||
'deploymentRetention' => $deploymentRetention,
|
||||
'installationId' => $installation->getId(),
|
||||
'installationInternalId' => $installation->getSequence(),
|
||||
'providerRepositoryId' => $providerRepositoryId,
|
||||
@@ -263,9 +280,8 @@ class Update extends Base
|
||||
'providerBranch' => $providerBranch,
|
||||
'providerRootDirectory' => $providerRootDirectory,
|
||||
'providerSilentMode' => $providerSilentMode,
|
||||
'specification' => $specification,
|
||||
'buildSpecification' => $specification,
|
||||
'runtimeSpecification' => $specification,
|
||||
'buildSpecification' => $buildSpecification,
|
||||
'runtimeSpecification' => $runtimeSpecification,
|
||||
'search' => implode(' ', [$functionId, $name, $runtime]),
|
||||
])));
|
||||
|
||||
|
||||
@@ -105,7 +105,8 @@ class Create extends Base
|
||||
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
|
||||
$function->setAttribute('live', false);
|
||||
$dbForProject->updateDocument('functions', $function->getId(), new Document(['live' => false]));
|
||||
|
||||
// Inform scheduler to pull the latest changes
|
||||
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
|
||||
@@ -113,7 +114,11 @@ class Create extends Base
|
||||
->setAttribute('resourceUpdatedAt', DateTime::now())
|
||||
->setAttribute('schedule', $function->getAttribute('schedule'))
|
||||
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId')));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([
|
||||
'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'),
|
||||
'schedule' => $schedule->getAttribute('schedule'),
|
||||
'active' => $schedule->getAttribute('active'),
|
||||
])));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
|
||||
@@ -11,6 +11,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\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
@@ -86,7 +87,8 @@ class Delete extends Base
|
||||
|
||||
$dbForProject->deleteDocument('variables', $variable->getId());
|
||||
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
|
||||
$function->setAttribute('live', false);
|
||||
$dbForProject->updateDocument('functions', $function->getId(), new Document(['live' => false]));
|
||||
|
||||
// Inform scheduler to pull the latest changes
|
||||
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
|
||||
@@ -94,7 +96,11 @@ class Delete extends Base
|
||||
->setAttribute('resourceUpdatedAt', DateTime::now())
|
||||
->setAttribute('schedule', $function->getAttribute('schedule'))
|
||||
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId')));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([
|
||||
'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'),
|
||||
'schedule' => $schedule->getAttribute('schedule'),
|
||||
'active' => $schedule->getAttribute('active'),
|
||||
])));
|
||||
|
||||
$response->noContent();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,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\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
@@ -99,12 +100,18 @@ class Update extends Base
|
||||
->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function']));
|
||||
|
||||
try {
|
||||
$dbForProject->updateDocument('variables', $variable->getId(), $variable);
|
||||
$dbForProject->updateDocument('variables', $variable->getId(), new Document([
|
||||
'key' => $key,
|
||||
'value' => $value ?? $variable->getAttribute('value'),
|
||||
'secret' => $secret ?? $variable->getAttribute('secret'),
|
||||
'search' => implode(' ', [$variableId, $function->getId(), $key, 'function']),
|
||||
]));
|
||||
} catch (DuplicateException $th) {
|
||||
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
|
||||
$function->setAttribute('live', false);
|
||||
$dbForProject->updateDocument('functions', $function->getId(), new Document(['live' => false]));
|
||||
|
||||
// Inform scheduler to pull the latest changes
|
||||
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
|
||||
@@ -112,7 +119,11 @@ class Update extends Base
|
||||
->setAttribute('resourceUpdatedAt', DateTime::now())
|
||||
->setAttribute('schedule', $function->getAttribute('schedule'))
|
||||
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId')));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([
|
||||
'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'),
|
||||
'schedule' => $schedule->getAttribute('schedule'),
|
||||
'active' => $schedule->getAttribute('active'),
|
||||
])));
|
||||
|
||||
$response->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ class Builds extends Action
|
||||
$version = $this->getVersion($resource);
|
||||
$runtime = $this->getRuntime($resource, $version);
|
||||
|
||||
$spec = Config::getParam('specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
|
||||
$spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
|
||||
|
||||
if ($resource->getCollection() === 'functions' && \is_null($runtime)) {
|
||||
throw new \Exception('Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported');
|
||||
@@ -279,7 +279,10 @@ class Builds extends Action
|
||||
|
||||
$deployment->setAttribute('buildStartedAt', $startTime);
|
||||
$deployment->setAttribute('status', 'processing');
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
|
||||
'buildStartedAt' => $startTime,
|
||||
'status' => 'processing',
|
||||
]));
|
||||
|
||||
if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) {
|
||||
$resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')]));
|
||||
@@ -366,7 +369,11 @@ class Builds extends Action
|
||||
->setAttribute('sourcePath', $source)
|
||||
->setAttribute('sourceSize', $directorySize)
|
||||
->setAttribute('totalSize', $directorySize);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
|
||||
'sourcePath' => $deployment->getAttribute('sourcePath'),
|
||||
'sourceSize' => $deployment->getAttribute('sourceSize'),
|
||||
'totalSize' => $deployment->getAttribute('totalSize'),
|
||||
]));
|
||||
|
||||
$queueForRealtime
|
||||
->setPayload($deployment->getArrayCopy())
|
||||
@@ -480,7 +487,13 @@ class Builds extends Action
|
||||
$deployment->setAttribute('providerCommitAuthor', APP_VCS_GITHUB_USERNAME);
|
||||
$deployment->setAttribute('providerCommitMessage', "Create '" . $resource->getAttribute('name', '') . "' function");
|
||||
$deployment->setAttribute('providerCommitUrl', "https://github.com/$cloneOwner/$cloneRepository/commit/$providerCommitHash");
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
|
||||
'providerCommitHash' => $deployment->getAttribute('providerCommitHash'),
|
||||
'providerCommitAuthorUrl' => $deployment->getAttribute('providerCommitAuthorUrl'),
|
||||
'providerCommitAuthor' => $deployment->getAttribute('providerCommitAuthor'),
|
||||
'providerCommitMessage' => $deployment->getAttribute('providerCommitMessage'),
|
||||
'providerCommitUrl' => $deployment->getAttribute('providerCommitUrl'),
|
||||
]));
|
||||
|
||||
$queueForRealtime
|
||||
->setPayload($deployment->getArrayCopy())
|
||||
@@ -528,7 +541,11 @@ class Builds extends Action
|
||||
->setAttribute('sourcePath', $source)
|
||||
->setAttribute('sourceSize', $directorySize)
|
||||
->setAttribute('totalSize', $directorySize);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
|
||||
'sourcePath' => $deployment->getAttribute('sourcePath'),
|
||||
'sourceSize' => $deployment->getAttribute('sourceSize'),
|
||||
'totalSize' => $deployment->getAttribute('totalSize'),
|
||||
]));
|
||||
|
||||
$queueForRealtime
|
||||
->setPayload($deployment->getArrayCopy())
|
||||
@@ -543,7 +560,9 @@ class Builds extends Action
|
||||
|
||||
/** Request the executor to build the code... */
|
||||
$deployment->setAttribute('status', 'building');
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
|
||||
'status' => 'building',
|
||||
]));
|
||||
|
||||
if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) {
|
||||
$resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')]));
|
||||
@@ -819,7 +838,9 @@ class Builds extends Action
|
||||
|
||||
if ($affected) {
|
||||
$deployment = $deployment->setAttribute('buildLogs', $currentLogs);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
|
||||
'buildLogs' => $currentLogs,
|
||||
]));
|
||||
|
||||
$queueForRealtime
|
||||
->setPayload($deployment->getArrayCopy())
|
||||
@@ -905,7 +926,14 @@ class Builds extends Action
|
||||
}
|
||||
}
|
||||
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
|
||||
'buildPath' => $deployment->getAttribute('buildPath'),
|
||||
'buildSize' => $deployment->getAttribute('buildSize'),
|
||||
'totalSize' => $deployment->getAttribute('totalSize'),
|
||||
'buildLogs' => $deployment->getAttribute('buildLogs'),
|
||||
'adapter' => $deployment->getAttribute('adapter'),
|
||||
'fallbackFile' => $deployment->getAttribute('fallbackFile'),
|
||||
]));
|
||||
$queueForRealtime
|
||||
->setPayload($deployment->getArrayCopy())
|
||||
->trigger();
|
||||
@@ -916,12 +944,15 @@ class Builds extends Action
|
||||
|
||||
$logs = $deployment->getAttribute('buildLogs', '');
|
||||
$date = \date('H:i:s');
|
||||
$logs .= "[90m[$date] [90m[[0mappwrite[90m][32m Deployment finished. [0m\n";
|
||||
$logs .= "\033[90m[$date] \033[90m[\033[0mappwrite\033[90m]\033[32m Deployment finished. \033[0m\n";
|
||||
$deployment->setAttribute('buildLogs', $logs);
|
||||
|
||||
/** Update the status */
|
||||
$deployment->setAttribute('status', 'ready');
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
|
||||
'buildLogs' => $deployment->getAttribute('buildLogs'),
|
||||
'status' => 'ready',
|
||||
]));
|
||||
|
||||
Console::log('Status marked as ready');
|
||||
|
||||
@@ -1109,7 +1140,11 @@ class Builds extends Action
|
||||
->setAttribute('resourceUpdatedAt', DateTime::now())
|
||||
->setAttribute('schedule', $resource->getAttribute('schedule'))
|
||||
->setAttribute('active', !empty($resource->getAttribute('schedule')) && !empty($resource->getAttribute('deploymentId')));
|
||||
$dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule);
|
||||
$dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([
|
||||
'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'),
|
||||
'schedule' => $schedule->getAttribute('schedule'),
|
||||
'active' => $schedule->getAttribute('active'),
|
||||
]));
|
||||
}
|
||||
|
||||
/** Screenshot site */
|
||||
@@ -1160,7 +1195,12 @@ class Builds extends Action
|
||||
$deployment->setAttribute('status', 'failed');
|
||||
|
||||
$deployment->setAttribute('buildLogs', $message);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
|
||||
'buildEndedAt' => $deployment->getAttribute('buildEndedAt'),
|
||||
'buildDuration' => $deployment->getAttribute('buildDuration'),
|
||||
'status' => 'failed',
|
||||
'buildLogs' => $message,
|
||||
]));
|
||||
|
||||
if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) {
|
||||
$resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')]));
|
||||
@@ -1189,7 +1229,7 @@ class Builds extends Action
|
||||
|
||||
protected function sendUsage(Document $resource, Document $deployment, Document $project, StatsUsage $queue): void
|
||||
{
|
||||
$spec = Config::getParam('specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
|
||||
$spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
|
||||
|
||||
switch ($deployment->getAttribute('status')) {
|
||||
case 'ready':
|
||||
@@ -1467,7 +1507,9 @@ class Builds extends Action
|
||||
$logs .= "[90m[$date] [90m[[0mappwrite[90m][33m Git action failed. Deployment will continue. [0m\n";
|
||||
|
||||
$deployment->setAttribute('buildLogs', $logs);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
|
||||
'buildLogs' => $deployment->getAttribute('buildLogs'),
|
||||
]));
|
||||
|
||||
$queueForRealtime
|
||||
->setPayload($deployment->getArrayCopy())
|
||||
@@ -1483,10 +1525,12 @@ class Builds extends Action
|
||||
|
||||
$logs = $deployment->getAttribute('buildLogs', '');
|
||||
$date = \date('H:i:s');
|
||||
$logs .= "[90m[$date] [90m[[0mappwrite[90m][33m Build has been canceled. [0m\n";
|
||||
$logs .= "\033[90m[$date] \033[90m[\033[0mappwrite\033[90m]\033[33m Build has been canceled. \033[0m\n";
|
||||
|
||||
$deployment->setAttribute('buildLogs', $logs);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
|
||||
'buildLogs' => $deployment->getAttribute('buildLogs'),
|
||||
]));
|
||||
|
||||
$queueForRealtime
|
||||
->setPayload($deployment->getArrayCopy())
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Health\Http\Health\Storage;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
@@ -58,33 +57,21 @@ class Get extends Action
|
||||
$checkStart = \microtime(true);
|
||||
|
||||
foreach ($devices as $device) {
|
||||
$uniqueFileName = \uniqid('health', true);
|
||||
$filePath = $device->getPath($uniqueFileName);
|
||||
$path = $device->getPath(\uniqid('health', true));
|
||||
|
||||
if (!$device->write($filePath, 'test', 'text/plain')) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed writing test file to ' . $device->getRoot());
|
||||
}
|
||||
|
||||
$readError = null;
|
||||
try {
|
||||
if ($device->read($filePath) !== 'test') {
|
||||
$readError = new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed reading test file from ' . $device->getRoot());
|
||||
if (!$device->write($path, 'test', 'text/plain')) {
|
||||
throw new \Exception("Failed writing test file to {$device->getRoot()}");
|
||||
}
|
||||
|
||||
$content = $device->read($path);
|
||||
if ($content !== 'test') {
|
||||
throw new \Exception("Failed reading test file from {$device->getRoot()}: content mismatch");
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$readError = $e;
|
||||
} finally {
|
||||
// Always attempt to clean up test file
|
||||
if (!$device->delete($filePath)) {
|
||||
if ($readError !== null) {
|
||||
// If read already failed, wrap delete error but preserve original
|
||||
\error_log('Failed deleting test file from ' . $device->getRoot() . ' during read error recovery');
|
||||
} else {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed deleting test file from ' . $device->getRoot());
|
||||
}
|
||||
}
|
||||
// Re-throw read error if it occurred
|
||||
if ($readError !== null) {
|
||||
throw $readError;
|
||||
try {
|
||||
$device->delete($path);
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,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\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
@@ -73,7 +74,7 @@ class Update extends Action
|
||||
->setAttribute('name', $name)
|
||||
->setAttribute('expire', $expire);
|
||||
|
||||
$dbForPlatform->updateDocument('devKeys', $key->getId(), $key);
|
||||
$dbForPlatform->updateDocument('devKeys', $key->getId(), new Document(['name' => $name, 'expire' => $expire]));
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Projects;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator;
|
||||
@@ -77,9 +78,9 @@ class Update extends Action
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$project->setAttribute('labels', (array) \array_values(\array_unique($labels)));
|
||||
$labels = (array) \array_values(\array_unique($labels));
|
||||
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project);
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document(['labels' => $labels]));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Projects;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
@@ -72,34 +73,31 @@ class Update extends Action
|
||||
|
||||
$permissions = $this->getPermissions($teamId, $projectId);
|
||||
|
||||
$project
|
||||
->setAttribute('teamId', $teamId)
|
||||
->setAttribute('teamInternalId', $team->getSequence())
|
||||
->setAttribute('$permissions', $permissions);
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project);
|
||||
$project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
|
||||
'teamId' => $teamId,
|
||||
'teamInternalId' => $team->getSequence(),
|
||||
'$permissions' => $permissions,
|
||||
]));
|
||||
|
||||
$installations = $dbForPlatform->find('installations', [
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
foreach ($installations as $installation) {
|
||||
$installation->setAttribute('$permissions', $permissions);
|
||||
$dbForPlatform->updateDocument('installations', $installation->getId(), $installation);
|
||||
$dbForPlatform->updateDocument('installations', $installation->getId(), new Document(['$permissions' => $permissions]));
|
||||
}
|
||||
|
||||
$repositories = $dbForPlatform->find('repositories', [
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
foreach ($repositories as $repository) {
|
||||
$repository->setAttribute('$permissions', $permissions);
|
||||
$dbForPlatform->updateDocument('repositories', $repository->getId(), $repository);
|
||||
$dbForPlatform->updateDocument('repositories', $repository->getId(), new Document(['$permissions' => $permissions]));
|
||||
}
|
||||
|
||||
$vcsComments = $dbForPlatform->find('vcsComments', [
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
]);
|
||||
foreach ($vcsComments as $vcsComment) {
|
||||
$vcsComment->setAttribute('$permissions', $permissions);
|
||||
$dbForPlatform->updateDocument('vcsComments', $vcsComment->getId(), $vcsComment);
|
||||
$dbForPlatform->updateDocument('vcsComments', $vcsComment->getId(), new Document(['$permissions' => $permissions]));
|
||||
}
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
|
||||
@@ -75,7 +75,7 @@ class Create extends Action
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $domain, string $siteId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
|
||||
public function action(string $domain, string $siteId, ?string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
|
||||
{
|
||||
$this->validateDomainRestrictions($domain, $platform);
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ 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]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,7 +272,12 @@ class Create 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;
|
||||
@@ -302,7 +307,10 @@ class Create extends Action
|
||||
]))
|
||||
);
|
||||
} 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
|
||||
@@ -342,7 +350,12 @@ class Create 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' => $site->getAttribute('latestDeploymentId'),
|
||||
'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'),
|
||||
'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'),
|
||||
'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'),
|
||||
]));
|
||||
|
||||
$sitesDomain = $platform['sitesDomain'];
|
||||
$domain = ID::unique() . "." . $sitesDomain;
|
||||
@@ -368,7 +381,10 @@ class Create extends Action
|
||||
]))
|
||||
);
|
||||
} 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
|
||||
$site = $dbForProject->updateDocument(
|
||||
'sites',
|
||||
$site->getId(),
|
||||
$site
|
||||
->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', ''),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -142,7 +142,12 @@ class Create 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' => $site->getAttribute('latestDeploymentId'),
|
||||
'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'),
|
||||
'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'),
|
||||
'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'),
|
||||
]));
|
||||
|
||||
// Preview deployments for sites
|
||||
$sitesDomain = $platform['sitesDomain'];
|
||||
|
||||
@@ -89,7 +89,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'
|
||||
@@ -97,7 +97,9 @@ class Update extends Action
|
||||
|
||||
if ($deployment->getSequence() === $site->getAttribute('latestDeploymentInternalId', '')) {
|
||||
$site = $site->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
|
||||
$dbForProject->updateDocument('sites', $site->getId(), $site);
|
||||
$dbForProject->updateDocument('sites', $site->getId(), new Document([
|
||||
'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'),
|
||||
]));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -187,7 +187,12 @@ class Create extends Base
|
||||
->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' => $site->getAttribute('latestDeploymentId'),
|
||||
'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'),
|
||||
'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'),
|
||||
'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'),
|
||||
]));
|
||||
|
||||
$sitesDomain = $platform['sitesDomain'];
|
||||
$domain = ID::unique() . "." . $sitesDomain;
|
||||
|
||||
@@ -71,6 +71,16 @@ class Get extends Base
|
||||
throw new Exception(Exception::LOG_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Override status in response if the log is stuck in waiting/processing beyond the site timeout.
|
||||
$status = $log->getAttribute('status', '');
|
||||
if ($status === 'waiting' || $status === 'processing') {
|
||||
$timeout = $site->getAttribute('timeout', 30);
|
||||
$elapsed = \time() - \strtotime($log->getCreatedAt());
|
||||
if ($elapsed >= $timeout) {
|
||||
$log->setAttribute('status', 'failed');
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic($log, Response::MODEL_EXECUTION); //TODO: Change to model log, but model log already exists - decide what to do
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use Appwrite\Utopia\Database\Validator\Queries\Executions;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Logs;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Order as OrderException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
@@ -99,6 +100,35 @@ class XList extends Base
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
// Calculate the cutoff datetime before which a waiting/processing log is considered timed out.
|
||||
$timeout = $site->getAttribute('timeout', 30);
|
||||
$thresholdDate = new \DateTime("-{$timeout} seconds");
|
||||
$threshold = DateTime::format($thresholdDate);
|
||||
|
||||
// Capture what statuses the caller explicitly requested, before we mutate the query.
|
||||
$requestedStatuses = [];
|
||||
foreach ($queries as $query) {
|
||||
if ($query->getMethod() === Query::TYPE_EQUAL && $query->getAttribute() === 'status') {
|
||||
$requestedStatuses = [...$requestedStatuses, ...$query->getValues()];
|
||||
}
|
||||
}
|
||||
|
||||
// If the caller is filtering by 'failed', expand the DB query to also return
|
||||
// waiting/processing logs created before the timeout threshold, so timed-out
|
||||
// logs that were never marked failed in the DB are included in the results.
|
||||
foreach ($queries as $index => $query) {
|
||||
if ($query->getMethod() === Query::TYPE_EQUAL && $query->getAttribute() === 'status' && \in_array('failed', $query->getValues())) {
|
||||
$queries[$index] = Query::or([
|
||||
$query,
|
||||
Query::and([
|
||||
Query::equal('status', ['waiting', 'processing']),
|
||||
Query::createdBefore($threshold),
|
||||
]),
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
try {
|
||||
@@ -108,6 +138,20 @@ class XList extends Base
|
||||
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
|
||||
}
|
||||
|
||||
// Override status in response for timed-out logs, but only when the caller
|
||||
// did not explicitly request a non-failed status (e.g. waiting/processing).
|
||||
if (empty(\array_diff($requestedStatuses, ['failed']))) {
|
||||
foreach ($results as $log) {
|
||||
$status = $log->getAttribute('status', '');
|
||||
if ($status === 'waiting' || $status === 'processing') {
|
||||
$elapsed = \time() - \strtotime($log->getCreatedAt());
|
||||
if ($elapsed >= $timeout) {
|
||||
$log->setAttribute('status', 'failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'executions' => $results,
|
||||
'total' => $total,
|
||||
|
||||
@@ -14,6 +14,7 @@ use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
@@ -67,6 +68,7 @@ class Create extends Base
|
||||
->param('timeout', 30, new Range(1, (int) System::getEnv('_APP_SITES_TIMEOUT', 30)), 'Maximum request time in seconds.', true)
|
||||
->param('installCommand', '', new Text(8192, 0), 'Install Command.', true)
|
||||
->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true)
|
||||
->param('startCommand', '', new Text(8192, 0), 'Custom start command. Leave empty to use default.', true)
|
||||
->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true)
|
||||
->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.')
|
||||
->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true)
|
||||
@@ -76,12 +78,19 @@ class Create extends Base
|
||||
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true)
|
||||
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true)
|
||||
->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true)
|
||||
->param('specification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
|
||||
->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
|
||||
$plan,
|
||||
Config::getParam('specifications', []),
|
||||
System::getEnv('_APP_COMPUTE_CPUS', 0),
|
||||
System::getEnv('_APP_COMPUTE_MEMORY', 0)
|
||||
), 'Framework specification for the site and builds.', true, ['plan'])
|
||||
), 'Build specification for the site deployments.', true, ['plan'])
|
||||
->param('runtimeSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
|
||||
$plan,
|
||||
Config::getParam('specifications', []),
|
||||
System::getEnv('_APP_COMPUTE_CPUS', 0),
|
||||
System::getEnv('_APP_COMPUTE_MEMORY', 0)
|
||||
), 'Runtime specification for the SSR executions.', true, ['plan'])
|
||||
->param('deploymentRetention', 0, new Range(0, APP_COMPUTE_DEPLOYMENT_MAX_RETENTION), 'Days to keep non-active deployments before deletion. Value 0 means all deployments will be kept.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
@@ -99,6 +108,7 @@ class Create extends Base
|
||||
int $timeout,
|
||||
string $installCommand,
|
||||
string $buildCommand,
|
||||
string $startCommand,
|
||||
string $outputDirectory,
|
||||
string $buildRuntime,
|
||||
string $adapter,
|
||||
@@ -108,7 +118,9 @@ class Create extends Base
|
||||
string $providerBranch,
|
||||
bool $providerSilentMode,
|
||||
string $providerRootDirectory,
|
||||
string $specification,
|
||||
string $buildSpecification,
|
||||
string $runtimeSpecification,
|
||||
int $deploymentRetention,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Document $project,
|
||||
@@ -136,20 +148,20 @@ class Create extends Base
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".');
|
||||
}
|
||||
|
||||
$site = $dbForProject->createDocument('sites', new Document([
|
||||
$site = new Document([
|
||||
'$id' => $siteId,
|
||||
'enabled' => $enabled,
|
||||
'live' => true,
|
||||
'logging' => $logging,
|
||||
'name' => $name,
|
||||
'framework' => $framework,
|
||||
'deploymentRetention' => $deploymentRetention,
|
||||
'deploymentInternalId' => '',
|
||||
'deploymentId' => '',
|
||||
'timeout' => $timeout,
|
||||
'installCommand' => $installCommand,
|
||||
'buildCommand' => $buildCommand,
|
||||
'deploymentRetention' => 0,
|
||||
'startCommand' => '',
|
||||
'startCommand' => $startCommand,
|
||||
'outputDirectory' => $outputDirectory,
|
||||
'search' => implode(' ', [$siteId, $name, $framework]),
|
||||
'fallbackFile' => $fallbackFile,
|
||||
@@ -161,18 +173,21 @@ class Create extends Base
|
||||
'providerBranch' => $providerBranch,
|
||||
'providerRootDirectory' => $providerRootDirectory,
|
||||
'providerSilentMode' => $providerSilentMode,
|
||||
'specification' => $specification,
|
||||
'buildSpecification' => $specification,
|
||||
'runtimeSpecification' => $specification,
|
||||
'buildSpecification' => $buildSpecification,
|
||||
'runtimeSpecification' => $runtimeSpecification,
|
||||
'buildRuntime' => $buildRuntime,
|
||||
'adapter' => $adapter,
|
||||
]));
|
||||
]);
|
||||
|
||||
try {
|
||||
$site = $dbForProject->createDocument('sites', $site);
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::SITE_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
// Git connect logic
|
||||
if (!empty($providerRepositoryId)) {
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
|
||||
$repository = $dbForPlatform->createDocument('repositories', new Document([
|
||||
$repository = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => $this->getPermissions($teamId, $project->getId()),
|
||||
'installationId' => $installation->getId(),
|
||||
@@ -184,13 +199,16 @@ class Create extends Base
|
||||
'resourceInternalId' => $site->getSequence(),
|
||||
'resourceType' => 'site',
|
||||
'providerPullRequestIds' => []
|
||||
]));
|
||||
|
||||
]);
|
||||
$repository = $dbForPlatform->createDocument('repositories', $repository);
|
||||
$site->setAttribute('repositoryId', $repository->getId());
|
||||
$site->setAttribute('repositoryInternalId', $repository->getSequence());
|
||||
}
|
||||
|
||||
$site = $dbForProject->updateDocument('sites', $site->getId(), $site);
|
||||
$site = $dbForProject->updateDocument('sites', $site->getId(), new Document([
|
||||
'repositoryId' => $repository->getId(),
|
||||
'repositoryInternalId' => $repository->getSequence(),
|
||||
]));
|
||||
}
|
||||
|
||||
$queueForEvents->setParam('siteId', $site->getId());
|
||||
|
||||
|
||||
@@ -111,7 +111,10 @@ class Update extends Base
|
||||
->setAttribute('deploymentId', $deployment->getId())
|
||||
->setAttribute('deploymentInternalId', $deployment->getSequence());
|
||||
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), $rule));
|
||||
$authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
|
||||
'deploymentId' => $rule->getAttribute('deploymentId'),
|
||||
'deploymentInternalId' => $rule->getAttribute('deploymentInternalId'),
|
||||
])));
|
||||
}, $queries));
|
||||
|
||||
$queueForEvents
|
||||
|
||||
@@ -71,6 +71,7 @@ class Update extends Base
|
||||
->param('timeout', 30, new Range(1, (int) System::getEnv('_APP_SITES_TIMEOUT', 30)), 'Maximum request time in seconds.', true)
|
||||
->param('installCommand', '', new Text(8192, 0), 'Install Command.', true)
|
||||
->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true)
|
||||
->param('startCommand', '', new Text(8192, 0), 'Custom start command. Leave empty to use default.', true)
|
||||
->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true)
|
||||
->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.', true)
|
||||
->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true)
|
||||
@@ -80,12 +81,19 @@ class Update extends Base
|
||||
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true)
|
||||
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true)
|
||||
->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true)
|
||||
->param('specification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
|
||||
->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
|
||||
$plan,
|
||||
Config::getParam('specifications', []),
|
||||
System::getEnv('_APP_COMPUTE_CPUS', 0),
|
||||
System::getEnv('_APP_COMPUTE_MEMORY', 0)
|
||||
), 'Framework specification for the site and builds.', true, ['plan'])
|
||||
), 'Build specification for the site deployments.', true, ['plan'])
|
||||
->param('runtimeSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
|
||||
$plan,
|
||||
Config::getParam('specifications', []),
|
||||
System::getEnv('_APP_COMPUTE_CPUS', 0),
|
||||
System::getEnv('_APP_COMPUTE_MEMORY', 0)
|
||||
), 'Runtime specification for the SSR executions.', true, ['plan'])
|
||||
->param('deploymentRetention', 0, new Range(0, APP_COMPUTE_DEPLOYMENT_MAX_RETENTION), 'Days to keep non-active deployments before deletion. Value 0 means all deployments will be kept.', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
@@ -107,6 +115,7 @@ class Update extends Base
|
||||
int $timeout,
|
||||
string $installCommand,
|
||||
string $buildCommand,
|
||||
string $startCommand,
|
||||
string $outputDirectory,
|
||||
string $buildRuntime,
|
||||
string $adapter,
|
||||
@@ -116,7 +125,9 @@ class Update extends Base
|
||||
string $providerBranch,
|
||||
bool $providerSilentMode,
|
||||
string $providerRootDirectory,
|
||||
string $specification,
|
||||
string $buildSpecification,
|
||||
string $runtimeSpecification,
|
||||
int $deploymentRetention,
|
||||
Request $request,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
@@ -190,11 +201,9 @@ class Update extends Base
|
||||
$repositoryInternalId = '';
|
||||
}
|
||||
|
||||
// Git connect logic
|
||||
if (!$isConnected && !empty($providerRepositoryId)) {
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
|
||||
$repository = $dbForPlatform->createDocument('repositories', new Document([
|
||||
$repository = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => $this->getPermissions($teamId, $project->getId()),
|
||||
'installationId' => $installation->getId(),
|
||||
@@ -206,8 +215,8 @@ class Update extends Base
|
||||
'resourceInternalId' => $site->getSequence(),
|
||||
'resourceType' => 'site',
|
||||
'providerPullRequestIds' => []
|
||||
]));
|
||||
|
||||
]);
|
||||
$repository = $dbForPlatform->createDocument('repositories', $repository);
|
||||
$repositoryId = $repository->getId();
|
||||
$repositoryInternalId = $repository->getSequence();
|
||||
}
|
||||
@@ -218,6 +227,7 @@ class Update extends Base
|
||||
$site->getAttribute('name') !== $name ||
|
||||
$site->getAttribute('buildCommand') !== $buildCommand ||
|
||||
$site->getAttribute('installCommand') !== $installCommand ||
|
||||
$site->getAttribute('startCommand') !== $startCommand ||
|
||||
$site->getAttribute('outputDirectory') !== $outputDirectory ||
|
||||
$site->getAttribute('providerRootDirectory') !== $providerRootDirectory ||
|
||||
$site->getAttribute('framework') !== $framework
|
||||
@@ -225,14 +235,22 @@ class Update extends Base
|
||||
$live = false;
|
||||
}
|
||||
|
||||
// Enforce Cold Start if spec limits change.
|
||||
if ($site->getAttribute('specification') !== $specification && !empty($site->getAttribute('deploymentId'))) {
|
||||
try {
|
||||
$executor->deleteRuntime($project->getId(), $site->getAttribute('deploymentId'));
|
||||
} catch (\Throwable $th) {
|
||||
// Don't throw if the deployment doesn't exist
|
||||
if ($th->getCode() !== 404) {
|
||||
throw $th;
|
||||
if (!empty($site->getAttribute('deploymentId'))) {
|
||||
$specsChanged = false;
|
||||
if ($site->getAttribute('runtimeSpecification', '') !== $runtimeSpecification) {
|
||||
$specsChanged = true;
|
||||
} elseif ($site->getAttribute('buildSpecification', '') !== $buildSpecification) {
|
||||
$specsChanged = true;
|
||||
}
|
||||
|
||||
if ($specsChanged) {
|
||||
try {
|
||||
$executor->deleteRuntime($project->getId(), $site->getAttribute('deploymentId'));
|
||||
} catch (\Throwable $th) {
|
||||
// Don't throw if the deployment doesn't exist
|
||||
if ($th->getCode() !== 404) {
|
||||
throw $th;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,10 +262,10 @@ class Update extends Base
|
||||
'logging' => $logging,
|
||||
'live' => $live,
|
||||
'timeout' => $timeout,
|
||||
'deploymentRetention' => $deploymentRetention,
|
||||
'installCommand' => $installCommand,
|
||||
'buildCommand' => $buildCommand,
|
||||
'deploymentRetention' => 0,
|
||||
'startCommand' => '',
|
||||
'startCommand' => $startCommand,
|
||||
'outputDirectory' => $outputDirectory,
|
||||
'installationId' => $installation->getId(),
|
||||
'installationInternalId' => $installation->getSequence(),
|
||||
@@ -257,9 +275,8 @@ class Update extends Base
|
||||
'providerBranch' => $providerBranch,
|
||||
'providerRootDirectory' => $providerRootDirectory,
|
||||
'providerSilentMode' => $providerSilentMode,
|
||||
'specification' => $specification,
|
||||
'buildSpecification' => $specification,
|
||||
'runtimeSpecification' => $specification,
|
||||
'buildSpecification' => $buildSpecification,
|
||||
'runtimeSpecification' => $runtimeSpecification,
|
||||
'search' => implode(' ', [$siteId, $name, $framework]),
|
||||
'buildRuntime' => $buildRuntime,
|
||||
'adapter' => $adapter,
|
||||
|
||||
@@ -92,7 +92,9 @@ class Create extends Base
|
||||
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false));
|
||||
$dbForProject->updateDocument('sites', $site->getId(), new Document([
|
||||
'live' => false,
|
||||
]));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
|
||||
@@ -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\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
@@ -76,7 +77,9 @@ class Delete extends Base
|
||||
|
||||
$dbForProject->deleteDocument('variables', $variable->getId());
|
||||
|
||||
$dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false));
|
||||
$dbForProject->updateDocument('sites', $site->getId(), new Document([
|
||||
'live' => false,
|
||||
]));
|
||||
|
||||
$response->noContent();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,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\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
@@ -93,12 +94,19 @@ class Update extends Base
|
||||
->setAttribute('search', implode(' ', [$variableId, $site->getId(), $key, 'site']));
|
||||
|
||||
try {
|
||||
$dbForProject->updateDocument('variables', $variable->getId(), $variable);
|
||||
$dbForProject->updateDocument('variables', $variable->getId(), new Document([
|
||||
'key' => $variable->getAttribute('key'),
|
||||
'value' => $variable->getAttribute('value'),
|
||||
'secret' => $variable->getAttribute('secret'),
|
||||
'search' => $variable->getAttribute('search'),
|
||||
]));
|
||||
} catch (DuplicateException $th) {
|
||||
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false));
|
||||
$dbForProject->updateDocument('sites', $site->getId(), new Document([
|
||||
'live' => false,
|
||||
]));
|
||||
|
||||
$response->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
}
|
||||
|
||||
@@ -266,17 +266,22 @@ class Create extends Action
|
||||
$authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
|
||||
}
|
||||
} elseif ($membership->getAttribute('confirm') === false) {
|
||||
$membership->setAttribute('secret', $proofForToken->hash($secret));
|
||||
$membership->setAttribute('invited', DateTime::now());
|
||||
$secretHash = $proofForToken->hash($secret);
|
||||
$invitedTime = DateTime::now();
|
||||
|
||||
if ($isPrivilegedUser || $isAppUser) {
|
||||
$membership->setAttribute('joined', DateTime::now());
|
||||
$membership->setAttribute('confirm', true);
|
||||
$membership = $authorization->skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), new Document([
|
||||
'secret' => $secretHash,
|
||||
'invited' => $invitedTime,
|
||||
'joined' => DateTime::now(),
|
||||
'confirm' => true
|
||||
])));
|
||||
} else {
|
||||
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), new Document([
|
||||
'secret' => $secretHash,
|
||||
'invited' => $invitedTime
|
||||
]));
|
||||
}
|
||||
|
||||
$membership = ($isPrivilegedUser || $isAppUser) ?
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) :
|
||||
$dbForProject->updateDocument('memberships', $membership->getId(), $membership);
|
||||
} else {
|
||||
throw new Exception(Exception::MEMBERSHIP_ALREADY_CONFIRMED);
|
||||
}
|
||||
|
||||
@@ -131,7 +131,10 @@ class Delete extends Action
|
||||
if (!$membership->isEmpty()) {
|
||||
$team->setAttribute('userId', $membership->getAttribute('userId'));
|
||||
$team->setAttribute('userInternalId', $membership->getAttribute('userInternalId'));
|
||||
$dbForProject->updateDocument('teams', $team->getId(), $team);
|
||||
$dbForProject->updateDocument('teams', $team->getId(), new Document([
|
||||
'userId' => $membership->getAttribute('userId'),
|
||||
'userInternalId' => $membership->getAttribute('userInternalId'),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ class Update extends Action
|
||||
;
|
||||
}
|
||||
|
||||
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership);
|
||||
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), new Document(['joined' => $membership->getAttribute('joined'), 'confirm' => true]));
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ class Update extends Action
|
||||
* Update the roles
|
||||
*/
|
||||
$membership->setAttribute('roles', $roles);
|
||||
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership);
|
||||
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), new Document(['roles' => $roles]));
|
||||
|
||||
/**
|
||||
* Replace membership on profile
|
||||
|
||||
@@ -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\Validator\UID;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Text;
|
||||
@@ -67,7 +68,7 @@ class Update extends Action
|
||||
->setAttribute('name', $name)
|
||||
->setAttribute('search', implode(' ', [$teamId, $name]));
|
||||
|
||||
$team = $dbForProject->updateDocument('teams', $team->getId(), $team);
|
||||
$team = $dbForProject->updateDocument('teams', $team->getId(), new Document(['name' => $name, 'search' => implode(' ', [$teamId, $name])]));
|
||||
|
||||
$queueForEvents->setParam('teamId', $team->getId());
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\VCS\Http\GitHub\Authorize\External;
|
||||
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\Platform\Modules\VCS\Http\GitHub\Deployment;
|
||||
use Appwrite\SDK\AuthType;
|
||||
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\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\VCS\Adapter\Git\GitHub;
|
||||
use Utopia\VCS\Exception\RepositoryNotFound;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
use Deployment;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateExternalDeployment';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/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')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $installationId,
|
||||
string $repositoryId,
|
||||
string $providerPullRequestId,
|
||||
GitHub $github,
|
||||
Response $response,
|
||||
Document $project,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
callable $getProjectDB,
|
||||
Build $queueForBuilds,
|
||||
array $platform
|
||||
) {
|
||||
$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 = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), new Document(['providerPullRequestIds' => $providerPullRequestIds])));
|
||||
|
||||
$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');
|
||||
|
||||
try {
|
||||
$providerRepositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
if (empty($providerRepositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
} catch (RepositoryNotFound $e) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$owner = $github->getOwnerName($providerInstallationId);
|
||||
$pullRequestResponse = $github->getPullRequest($owner, $providerRepositoryName, $providerPullRequestId);
|
||||
|
||||
$providerRepositoryUrl = $pullRequestResponse['head']['repo']['html_url'] ?? '';
|
||||
$providerRepositoryOwner = $pullRequestResponse['head']['repo']['owner']['login'] ?? '';
|
||||
$providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? '';
|
||||
$providerBranchUrl = "$providerRepositoryUrl/tree/$providerBranch";
|
||||
$providerCommitHash = $pullRequestResponse['head']['sha'] ?? '';
|
||||
|
||||
$commitDetails = $github->getCommit($providerRepositoryOwner, $providerRepositoryName, $providerCommitHash);
|
||||
$providerCommitMessage = $commitDetails["commitMessage"] ?? '';
|
||||
$providerCommitUrl = $commitDetails["commitUrl"] ?? '';
|
||||
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
|
||||
$providerCommitAuthorUrl = $commitDetails["commitAuthorUrl"] ?? '';
|
||||
|
||||
$this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform);
|
||||
|
||||
$response->noContent();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user