mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
217
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44adb1dc96 | ||
|
|
df4c36d71b | ||
|
|
793eb53790 | ||
|
|
6b568e8b7f | ||
|
|
fec74ee9e4 | ||
|
|
10ef570485 | ||
|
|
d719fd9be4 | ||
|
|
135023b3ed | ||
|
|
2bd2092263 | ||
|
|
eee7d0422c | ||
|
|
0d314c00a6 | ||
|
|
a5fc2e0ef7 | ||
|
|
e168ab9ffc | ||
|
|
644c18fc69 | ||
|
|
57717f4690 | ||
|
|
c46a9bdb45 | ||
|
|
d431b53434 | ||
|
|
18d9ebaab3 | ||
|
|
40e343d17d | ||
|
|
f6122157c7 | ||
|
|
cbaad2dfb9 | ||
|
|
c509ec56bd | ||
|
|
a25b786fca | ||
|
|
06224aa34e | ||
|
|
4bcbd49d6d | ||
|
|
09e2ded206 | ||
|
|
4a8c063d51 | ||
|
|
2e9e6d38e2 | ||
|
|
53917d6d90 | ||
|
|
d20714eed6 | ||
|
|
2f328bf4cf | ||
|
|
e3c4cdb040 | ||
|
|
c8cf645414 | ||
|
|
0b11e0cc92 | ||
|
|
a5d43ddf71 | ||
|
|
aa47d9f07a | ||
|
|
15a5f8690d | ||
|
|
9901c781b7 | ||
|
|
338559967a | ||
|
|
a736a42e26 | ||
|
|
00c6549610 | ||
|
|
510d871379 | ||
|
|
ee3d34310a | ||
|
|
877a978451 | ||
|
|
43e2d7454a | ||
|
|
d49b0540fd | ||
|
|
c31634e64d | ||
|
|
3a58e5927b | ||
|
|
cb359e40ba | ||
|
|
8e02115828 | ||
|
|
3a748fbd35 | ||
|
|
7e7a4658cd | ||
|
|
abf97e391e | ||
|
|
d8799e5a9c | ||
|
|
217e471a41 | ||
|
|
72e491ac69 | ||
|
|
bdb0b188ef | ||
|
|
191f884772 | ||
|
|
45f85d6b4d | ||
|
|
8f845aef53 | ||
|
|
68e96e9851 | ||
|
|
c6cf27bd6f | ||
|
|
71f2c6c131 | ||
|
|
1db71a46ea | ||
|
|
8124f6998e | ||
|
|
7f06b0ec76 | ||
|
|
ca533f40c9 | ||
|
|
2ad0428727 | ||
|
|
54db5655a1 | ||
|
|
91cc0e627a | ||
|
|
f1af8b330e | ||
|
|
29e7d1f7d0 | ||
|
|
97bcccba85 | ||
|
|
7f3a342940 | ||
|
|
cca5606b70 | ||
|
|
1337ea77b1 | ||
|
|
a460a41fe5 | ||
|
|
b62640489d | ||
|
|
f9e671419b | ||
|
|
fcf018580c | ||
|
|
8cca88f2b8 | ||
|
|
bae6e465fa | ||
|
|
3846903b9d | ||
|
|
d9e2ecff54 | ||
|
|
06e3afcf88 | ||
|
|
0ef28eba17 | ||
|
|
e5730506eb | ||
|
|
3b82719b73 | ||
|
|
06274764c6 | ||
|
|
fb40970eba | ||
|
|
cd0d1d94db | ||
|
|
19667f78a9 | ||
|
|
a6779caa5e | ||
|
|
cddd16b753 | ||
|
|
f91f382baf | ||
|
|
4ee9e9730f | ||
|
|
6874b4f6fb | ||
|
|
eeba650f6c | ||
|
|
54b07e6b99 | ||
|
|
dc8b9478ee | ||
|
|
d97b02add9 | ||
|
|
8427ade0fa | ||
|
|
44d6a0dea1 | ||
|
|
f710ce266b | ||
|
|
45dc9a39d9 | ||
|
|
d703a118c4 | ||
|
|
c20e580a9e | ||
|
|
9b4de5d7bf | ||
|
|
7c9f396b40 | ||
|
|
31b9f2b4f9 | ||
|
|
6ff15f9b53 | ||
|
|
1ebd6c252c | ||
|
|
8424351d49 | ||
|
|
0c4e597819 | ||
|
|
d1917d1a2b | ||
|
|
bb3503f0c4 | ||
|
|
7c22776254 | ||
|
|
2294586371 | ||
|
|
b7f519f86e | ||
|
|
febd5294f4 | ||
|
|
690a23fe89 | ||
|
|
39c1a88dae | ||
|
|
d0808e0c1a | ||
|
|
34fd22e4e9 | ||
|
|
d4fb0d37c2 | ||
|
|
fd02ca00f0 | ||
|
|
8a53d02d0c | ||
|
|
58ee53876b | ||
|
|
853b730def | ||
|
|
be5fdbfd4f | ||
|
|
e6d123b5e8 | ||
|
|
d8a844a2f9 | ||
|
|
dd87cdbebf | ||
|
|
4c1881324e | ||
|
|
bd27a36d16 | ||
|
|
2f999ba3db | ||
|
|
d8ec390cd6 | ||
|
|
7a0e7bfb95 | ||
|
|
da7ae6b953 | ||
|
|
2dead545f0 | ||
|
|
9c566ec17c | ||
|
|
385fe303cc | ||
|
|
81e3709d25 | ||
|
|
7f17efcdd8 | ||
|
|
a566ac1078 | ||
|
|
44e76b4eaa | ||
|
|
e7e1cfa12e | ||
|
|
77a70cad71 | ||
|
|
0c2db53ee8 | ||
|
|
83807ed247 | ||
|
|
8e8d71df35 | ||
|
|
d03d21b0d4 | ||
|
|
302a135e5a | ||
|
|
b79daa2ee9 | ||
|
|
94a5f5fff9 | ||
|
|
6b2160f388 | ||
|
|
86a68c50bc | ||
|
|
e2e7d6578c | ||
|
|
a171e17666 | ||
|
|
d8610e0f2a | ||
|
|
93222bf3f9 | ||
|
|
ea6765c68a | ||
|
|
6ac81dea19 | ||
|
|
e4dd9bc9f9 | ||
|
|
03d4d4c8fa | ||
|
|
fcc51d3352 | ||
|
|
08630cd686 | ||
|
|
9295a4e7ce | ||
|
|
826a1a02a2 | ||
|
|
4ed08cefa7 | ||
|
|
1a33a0c4d1 | ||
|
|
4d3d710211 | ||
|
|
ac93e70dd2 | ||
|
|
dd3ffbb391 | ||
|
|
9d1d101586 | ||
|
|
32da7b8fe2 | ||
|
|
41484113a0 | ||
|
|
5d6a8be66d | ||
|
|
aba3a31663 | ||
|
|
8950f71b0a | ||
|
|
54d3668693 | ||
|
|
b16169f867 | ||
|
|
bbd589f6a8 | ||
|
|
765d8241f2 | ||
|
|
8e041ddeb6 | ||
|
|
a4a7fe0c48 | ||
|
|
a247fc3b48 | ||
|
|
d876085f6d | ||
|
|
bba9c3d57a | ||
|
|
944e971124 | ||
|
|
e512ab1816 | ||
|
|
83b5aecbfd | ||
|
|
2f9d00fc07 | ||
|
|
23b57692ec | ||
|
|
8bda7f1e1e | ||
|
|
12bddc3b1f | ||
|
|
8f5fa84996 | ||
|
|
3af34f078c | ||
|
|
57e596fea2 | ||
|
|
9793cf4ece | ||
|
|
a4bdc23aef | ||
|
|
4240089d41 | ||
|
|
09ebfac307 | ||
|
|
89b6291488 | ||
|
|
1434b6bbff | ||
|
|
de164ff936 | ||
|
|
a8e2fb1ee5 | ||
|
|
7da8b44c63 | ||
|
|
8f0c1a4224 | ||
|
|
c57cb3d2d9 | ||
|
|
5172189e36 | ||
|
|
6c6723e181 | ||
|
|
5b0a05b4b3 | ||
|
|
87cbd8fbf3 | ||
|
|
9060250806 | ||
|
|
bc63c3b383 | ||
|
|
a503803725 |
@@ -18,10 +18,11 @@ _APP_SYSTEM_RESPONSE_FORMAT=
|
||||
_APP_OPTIONS_ABUSE=disabled
|
||||
_APP_OPTIONS_ROUTER_PROTECTION=disabled
|
||||
_APP_OPTIONS_FORCE_HTTPS=disabled
|
||||
_APP_OPTIONS_FUNCTIONS_FORCE_HTTPS=disabled
|
||||
_APP_OPTIONS_COMPUTE_FORCE_HTTPS=disabled
|
||||
_APP_OPENSSL_KEY_V1=your-secret-key
|
||||
_APP_DOMAIN=traefik
|
||||
_APP_DOMAIN_FUNCTIONS=functions.localhost
|
||||
_APP_DOMAIN_SITES=sites.localhost
|
||||
_APP_DOMAIN_TARGET=localhost
|
||||
_APP_REDIS_HOST=redis
|
||||
_APP_REDIS_PORT=6379
|
||||
@@ -67,17 +68,19 @@ _APP_SMS_FROM=+123456789
|
||||
_APP_SMS_PROJECTS_DENY_LIST=
|
||||
_APP_STORAGE_LIMIT=30000000
|
||||
_APP_STORAGE_PREVIEW_LIMIT=20000000
|
||||
_APP_FUNCTIONS_SIZE_LIMIT=30000000
|
||||
_APP_FUNCTIONS_TIMEOUT=900
|
||||
_APP_FUNCTIONS_BUILD_TIMEOUT=900
|
||||
_APP_FUNCTIONS_CPUS=8
|
||||
_APP_FUNCTIONS_MEMORY=8192
|
||||
_APP_FUNCTIONS_INACTIVE_THRESHOLD=600
|
||||
_APP_FUNCTIONS_MAINTENANCE_INTERVAL=600
|
||||
_APP_FUNCTIONS_RUNTIMES_NETWORK=runtimes
|
||||
_APP_COMPUTE_SIZE_LIMIT=30000000
|
||||
_APP_COMPUTE_TIMEOUT=900
|
||||
_APP_COMPUTE_BUILD_TIMEOUT=900
|
||||
_APP_COMPUTE_CPUS=8
|
||||
_APP_COMPUTE_MEMORY=8192
|
||||
_APP_COMPUTE_INACTIVE_THRESHOLD=600
|
||||
_APP_COMPUTE_MAINTENANCE_INTERVAL=600
|
||||
_APP_COMPUTE_RUNTIMES_NETWORK=runtimes
|
||||
_APP_EXECUTOR_SECRET=your-secret-key
|
||||
_APP_EXECUTOR_HOST=http://proxy/v1
|
||||
_APP_FUNCTIONS_RUNTIMES=php-8.0,node-18.0,python-3.9,ruby-3.1
|
||||
_APP_SITES_RUNTIMES=static-1,node-22,flutter-3.24
|
||||
_APP_SITES_FRAMEWORKS=sveltekit,nextjs,nuxt,astro,angular,remix,static
|
||||
_APP_MAINTENANCE_INTERVAL=86400
|
||||
_APP_MAINTENANCE_DELAY=
|
||||
_APP_MAINTENANCE_RETENTION_CACHE=2592000
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
name: "Console SDK Preview"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'app/config/specs/*-latest-console.json'
|
||||
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup & Build Console SDK
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
run: |
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
docker compose exec appwrite sdks --platform=console --sdk=web --version=latest --git=no
|
||||
sudo chown -R $USER:$USER ./app/sdks/console-web
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Build and Publish SDK
|
||||
working-directory: ./app/sdks/console-web
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
npx pkg-pr-new publish --comment=update
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose up -d
|
||||
sleep 30
|
||||
sleep 60
|
||||
|
||||
- name: Run ${{matrix.service}} Tests
|
||||
run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug
|
||||
|
||||
@@ -16,3 +16,4 @@ dev/yasd_init.php
|
||||
.phpunit.result.cache
|
||||
Makefile
|
||||
appwrite.json
|
||||
/.zed/
|
||||
|
||||
+2
-1
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
|
||||
--no-plugins --no-scripts --prefer-dist \
|
||||
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
|
||||
|
||||
FROM appwrite/base:0.9.3 AS final
|
||||
FROM appwrite/base:0.9.5 AS final
|
||||
|
||||
LABEL maintainer="team@appwrite.io"
|
||||
|
||||
@@ -81,6 +81,7 @@ RUN chmod +x /usr/local/bin/doctor && \
|
||||
chmod +x /usr/local/bin/worker-databases && \
|
||||
chmod +x /usr/local/bin/worker-deletes && \
|
||||
chmod +x /usr/local/bin/worker-functions && \
|
||||
chmod +x /usr/local/bin/worker-insights && \
|
||||
chmod +x /usr/local/bin/worker-mails && \
|
||||
chmod +x /usr/local/bin/worker-messaging && \
|
||||
chmod +x /usr/local/bin/worker-migrations && \
|
||||
|
||||
+439
-4
@@ -3074,7 +3074,7 @@ $projectCollections = array_merge([
|
||||
'size' => 128,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => APP_FUNCTION_SPECIFICATION_DEFAULT,
|
||||
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
@@ -3163,6 +3163,364 @@ $projectCollections = array_merge([
|
||||
],
|
||||
],
|
||||
|
||||
'sites' => [
|
||||
'$collection' => ID::custom(Database::METADATA),
|
||||
'$id' => ID::custom('sites'),
|
||||
'name' => 'Sites',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => ID::custom('name'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 2048,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('enabled'),
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'signed' => true,
|
||||
'size' => 0,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => true,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('live'),
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'signed' => true,
|
||||
'size' => 0,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => true,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('installationId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'signed' => true,
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => false,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('installationInternalId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('providerRepositoryId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'signed' => true,
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => false,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('repositoryId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'signed' => true,
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => false,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('repositoryInternalId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('providerBranch'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'signed' => true,
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => false,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('providerRootDirectory'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'signed' => true,
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => false,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('providerSilentMode'),
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'signed' => true,
|
||||
'size' => 0,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => false,
|
||||
'default' => false,
|
||||
'array' => false,
|
||||
],
|
||||
// [
|
||||
// '$id' => ID::custom('logging'),
|
||||
// 'type' => Database::VAR_BOOLEAN,
|
||||
// 'signed' => true,
|
||||
// 'size' => 0,
|
||||
// 'format' => '',
|
||||
// 'filters' => [],
|
||||
// 'required' => true,
|
||||
// 'array' => false,
|
||||
// ],
|
||||
[
|
||||
'$id' => ID::custom('framework'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 2048,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('outputDirectory'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('buildCommand'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('installCommand'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('fallbackFile'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('deploymentInternalId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('deploymentId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('vars'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['subQueryVariables'],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('varsProject'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['subQueryProjectVariables'],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('timeout'),
|
||||
'type' => Database::VAR_INTEGER,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('search'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('specification'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 128,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('serveRuntime'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 2048,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => '',
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('buildRuntime'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 2048,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => '',
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => ID::custom('_key_search'),
|
||||
'type' => Database::INDEX_FULLTEXT,
|
||||
'attributes' => ['search'],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_name'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['name'],
|
||||
'lengths' => [256],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_enabled'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['enabled'],
|
||||
'lengths' => [],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_installationId'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['installationId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_installationInternalId'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['installationInternalId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_providerRepositoryId'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['providerRepositoryId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_repositoryId'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['repositoryId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_repositoryInternalId'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['repositoryInternalId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_framework'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['framework'],
|
||||
'lengths' => [64],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('_key_deploymentId'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['deploymentId'],
|
||||
'lengths' => [],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
]
|
||||
],
|
||||
],
|
||||
|
||||
'deployments' => [
|
||||
'$collection' => ID::custom(Database::METADATA),
|
||||
'$id' => ID::custom('deployments'),
|
||||
@@ -3245,6 +3603,39 @@ $projectCollections = array_merge([
|
||||
'default' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('buildCommand'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('installCommand'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'array' => false,
|
||||
'$id' => ID::custom('outputDirectory'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('path'),
|
||||
'type' => Database::VAR_STRING,
|
||||
@@ -3438,6 +3829,39 @@ $projectCollections = array_merge([
|
||||
'required' => false,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('screenshot'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 265536,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('auditJson'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16777215, // minimum size for LONGTEXT
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('auditHtml'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16777215, // minimum size for MEDIUMTEXT
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('size'),
|
||||
'type' => Database::VAR_INTEGER,
|
||||
@@ -3662,9 +4086,9 @@ $projectCollections = array_merge([
|
||||
'size' => 1000000,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => '',
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
'default' => [],
|
||||
'array' => true,
|
||||
'filters' => ['json'],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('sourceType'),
|
||||
@@ -4031,6 +4455,17 @@ $projectCollections = array_merge([
|
||||
'array' => false,
|
||||
'filters' => ['encrypt']
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('secret'),
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => false,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('search'),
|
||||
'type' => Database::VAR_STRING,
|
||||
|
||||
@@ -540,6 +540,23 @@ return [
|
||||
'code' => 404,
|
||||
],
|
||||
|
||||
/** Sites */
|
||||
Exception::SITE_NOT_FOUND => [
|
||||
'name' => Exception::SITE_NOT_FOUND,
|
||||
'description' => 'Site with the requested ID could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::SITE_FRAMEWORK_UNSUPPORTED => [
|
||||
'name' => Exception::SITE_FRAMEWORK_UNSUPPORTED,
|
||||
'description' => 'The requested framework is either inactive or unsupported. Please check the value of the _APP_SITES_FRAMEWORKS environment variable.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::SITE_TEMPLATE_NOT_FOUND => [
|
||||
'name' => Exception::SITE_TEMPLATE_NOT_FOUND,
|
||||
'description' => 'Site Template with the requested ID could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
|
||||
/** Builds */
|
||||
Exception::BUILD_NOT_FOUND => [
|
||||
'name' => Exception::BUILD_NOT_FOUND,
|
||||
|
||||
@@ -217,6 +217,34 @@ return [
|
||||
],
|
||||
]
|
||||
],
|
||||
'sites' => [
|
||||
'$model' => Response::MODEL_SITE,
|
||||
'$resource' => true,
|
||||
'$description' => 'This event triggers on any sites event.',
|
||||
'deployments' => [
|
||||
'$model' => Response::MODEL_DEPLOYMENT,
|
||||
'$resource' => true,
|
||||
'$description' => 'This event triggers on any deployments event.',
|
||||
'create' => [
|
||||
'$description' => 'This event triggers when a deployment is created.',
|
||||
],
|
||||
'delete' => [
|
||||
'$description' => 'This event triggers when a deployment is deleted.'
|
||||
],
|
||||
'update' => [
|
||||
'$description' => 'This event triggers when a deployment is updated.'
|
||||
],
|
||||
],
|
||||
'create' => [
|
||||
'$description' => 'This event triggers when a site is created.'
|
||||
],
|
||||
'delete' => [
|
||||
'$description' => 'This event triggers when a site is deleted.',
|
||||
],
|
||||
'update' => [
|
||||
'$description' => 'This event triggers when a site is updated.',
|
||||
]
|
||||
],
|
||||
'functions' => [
|
||||
'$model' => Response::MODEL_FUNCTION,
|
||||
'$resource' => true,
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* List of Appwrite Sites supported frameworks
|
||||
*/
|
||||
|
||||
use Utopia\Config\Config;
|
||||
|
||||
$templateRuntimes = Config::getParam('template-runtimes');
|
||||
|
||||
function getVersions(array $versions, string $prefix)
|
||||
{
|
||||
return array_map(function ($version) use ($prefix) {
|
||||
return $prefix . '-' . $version;
|
||||
}, $versions);
|
||||
}
|
||||
|
||||
return [
|
||||
'sveltekit' => [
|
||||
'key' => 'sveltekit',
|
||||
'name' => 'SvelteKit',
|
||||
'defaultServeRuntime' => 'static-1',
|
||||
'serveRuntimes' => [
|
||||
'static-1'
|
||||
],
|
||||
'defaultBuildRuntime' => 'node-22',
|
||||
'buildRuntimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
|
||||
'defaultBuildCommand' => 'npm run build',
|
||||
'defaultInstallCommand' => 'npm install',
|
||||
'defaultOutputDirectory' => './build',
|
||||
],
|
||||
'nextjs' => [
|
||||
'key' => 'nextjs',
|
||||
'name' => 'Next.js',
|
||||
'defaultServeRuntime' => 'static-1',
|
||||
'serveRuntimes' => [
|
||||
'static-1'
|
||||
],
|
||||
'defaultBuildRuntime' => 'node-22',
|
||||
'buildRuntimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
|
||||
'defaultBuildCommand' => 'npm run build',
|
||||
'defaultInstallCommand' => 'npm install',
|
||||
'defaultOutputDirectory' => './out',
|
||||
],
|
||||
'nuxt' => [
|
||||
'key' => 'nuxt',
|
||||
'name' => 'Nuxt',
|
||||
'defaultServeRuntime' => 'static-1',
|
||||
'serveRuntimes' => [
|
||||
'static-1'
|
||||
],
|
||||
'defaultBuildRuntime' => 'node-22',
|
||||
'buildRuntimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
|
||||
'defaultBuildCommand' => 'npm run generate',
|
||||
'defaultInstallCommand' => 'npm install',
|
||||
'defaultOutputDirectory' => './dist',
|
||||
],
|
||||
/*
|
||||
'angular' => [
|
||||
'key' => 'angular',
|
||||
'name' => 'Angular',
|
||||
'defaultServeRuntime' => 'static-1',
|
||||
'serveRuntimes' => [
|
||||
'static-1'
|
||||
],
|
||||
'defaultBuildRuntime' => 'node-22',
|
||||
'buildRuntimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
|
||||
'defaultBuildCommand' => 'npm run build',
|
||||
'defaultInstallCommand' => 'npm install',
|
||||
'defaultOutputDirectory' => './dist/starter/browser',
|
||||
],
|
||||
*/
|
||||
'astro' => [
|
||||
'key' => 'astro',
|
||||
'name' => 'Astro',
|
||||
'defaultServeRuntime' => 'static-1',
|
||||
'serveRuntimes' => [
|
||||
'static-1'
|
||||
],
|
||||
'defaultBuildRuntime' => 'node-22',
|
||||
'buildRuntimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
|
||||
'defaultBuildCommand' => 'npm run build',
|
||||
'defaultInstallCommand' => 'npm install',
|
||||
'defaultOutputDirectory' => './dist',
|
||||
],
|
||||
'remix' => [
|
||||
'key' => 'remix',
|
||||
'name' => 'Remix',
|
||||
'defaultServeRuntime' => 'static-1',
|
||||
'serveRuntimes' => [
|
||||
'static-1'
|
||||
],
|
||||
'defaultBuildRuntime' => 'node-22',
|
||||
'buildRuntimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
|
||||
'defaultBuildCommand' => 'npm run build',
|
||||
'defaultInstallCommand' => 'npm install',
|
||||
'defaultOutputDirectory' => './build/client',
|
||||
],
|
||||
'flutter' => [
|
||||
'key' => 'flutter',
|
||||
'name' => 'Flutter',
|
||||
'defaultServeRuntime' => 'static-1',
|
||||
'serveRuntimes' => [
|
||||
'static-1'
|
||||
],
|
||||
'defaultBuildRuntime' => 'flutter-3.24',
|
||||
'buildRuntimes' => getVersions($templateRuntimes['FLUTTER']['versions'], 'flutter'),
|
||||
'defaultBuildCommand' => 'flutter build web',
|
||||
'defaultInstallCommand' => '',
|
||||
'defaultOutputDirectory' => './build/web',
|
||||
],
|
||||
'static' => [
|
||||
'key' => 'static',
|
||||
'name' => 'Static',
|
||||
'defaultServeRuntime' => 'static-1',
|
||||
'serveRuntimes' => [
|
||||
'static-1'
|
||||
],
|
||||
'defaultBuildRuntime' => 'node-22',
|
||||
'buildRuntimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'),
|
||||
'defaultBuildCommand' => 'npm run build',
|
||||
'defaultInstallCommand' => 'npm install',
|
||||
'defaultOutputDirectory' => './build',
|
||||
]
|
||||
];
|
||||
@@ -1,39 +1,8 @@
|
||||
<?php
|
||||
|
||||
const TEMPLATE_RUNTIMES = [
|
||||
'NODE' => [
|
||||
'name' => 'node',
|
||||
'versions' => ['22', '21.0', '20.0', '19.0', '18.0', '16.0', '14.5']
|
||||
],
|
||||
'PYTHON' => [
|
||||
'name' => 'python',
|
||||
'versions' => ['3.12', '3.11', '3.10', '3.9', '3.8']
|
||||
],
|
||||
'DART' => [
|
||||
'name' => 'dart',
|
||||
'versions' => ['3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16', '2.16']
|
||||
],
|
||||
'GO' => [
|
||||
'name' => 'go',
|
||||
'versions' => ['1.23']
|
||||
],
|
||||
'PHP' => [
|
||||
'name' => 'php',
|
||||
'versions' => ['8.3', '8.2', '8.1', '8.0']
|
||||
],
|
||||
'DENO' => [
|
||||
'name' => 'deno',
|
||||
'versions' => ['2.0', '1.46', '1.40', '1.35', '1.24', '1.21']
|
||||
],
|
||||
'BUN' => [
|
||||
'name' => 'bun',
|
||||
'versions' => ['1.1', '1.0']
|
||||
],
|
||||
'RUBY' => [
|
||||
'name' => 'ruby',
|
||||
'versions' => ['3.3', '3.2', '3.1', '3.0']
|
||||
],
|
||||
];
|
||||
use Utopia\Config\Config;
|
||||
|
||||
$templateRuntimes = Config::getParam('template-runtimes');
|
||||
|
||||
function getRuntimes($runtime, $commands, $entrypoint, $providerRootDirectory, $versionsDenyList = [])
|
||||
{
|
||||
@@ -62,24 +31,24 @@ return [
|
||||
'timeout' => 15,
|
||||
'useCases' => ['starter'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(TEMPLATE_RUNTIMES['NODE'], 'npm install', 'src/main.js', 'node/starter'),
|
||||
...getRuntimes($templateRuntimes['NODE'], 'npm install', 'src/main.js', 'node/starter'),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['PYTHON'],
|
||||
$templateRuntimes['PYTHON'],
|
||||
'pip install -r requirements.txt',
|
||||
'src/main.py',
|
||||
'python/starter'
|
||||
),
|
||||
...getRuntimes(TEMPLATE_RUNTIMES['DART'], 'dart pub get', 'lib/main.dart', 'dart/starter'),
|
||||
...getRuntimes(TEMPLATE_RUNTIMES['GO'], '', 'main.go', 'go/starter'),
|
||||
...getRuntimes($templateRuntimes['DART'], 'dart pub get', 'lib/main.dart', 'dart/starter'),
|
||||
...getRuntimes($templateRuntimes['GO'], '', 'main.go', 'go/starter'),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['PHP'],
|
||||
$templateRuntimes['PHP'],
|
||||
'composer install',
|
||||
'src/index.php',
|
||||
'php/starter'
|
||||
),
|
||||
...getRuntimes(TEMPLATE_RUNTIMES['DENO'], 'deno cache src/main.ts', 'src/main.ts', 'deno/starter'),
|
||||
...getRuntimes(TEMPLATE_RUNTIMES['BUN'], 'bun install', 'src/main.ts', 'bun/starter'),
|
||||
...getRuntimes(TEMPLATE_RUNTIMES['RUBY'], 'bundle install', 'lib/main.rb', 'ruby/starter'),
|
||||
...getRuntimes($templateRuntimes['DENO'], 'deno cache src/main.ts', 'src/main.ts', 'deno/starter'),
|
||||
...getRuntimes($templateRuntimes['BUN'], 'bun install', 'src/main.ts', 'bun/starter'),
|
||||
...getRuntimes($templateRuntimes['RUBY'], 'bundle install', 'lib/main.rb', 'ruby/starter'),
|
||||
],
|
||||
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/starter">file</a>.',
|
||||
'vcsProvider' => 'github',
|
||||
@@ -101,7 +70,7 @@ return [
|
||||
'useCases' => ['databases'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/query-upstash-vector'
|
||||
@@ -145,7 +114,7 @@ return [
|
||||
'useCases' => ['databases'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/query-redis-labs'
|
||||
@@ -188,7 +157,7 @@ return [
|
||||
'useCases' => ['databases'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/query-neo4j-auradb'
|
||||
@@ -240,7 +209,7 @@ return [
|
||||
'useCases' => ['databases'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/query-mongo-atlas'
|
||||
@@ -277,7 +246,7 @@ return [
|
||||
'useCases' => ['databases'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/query-neon-postgres'
|
||||
@@ -344,25 +313,25 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/prompt-chatgpt'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['PYTHON'],
|
||||
$templateRuntimes['PYTHON'],
|
||||
'pip install -r requirements.txt',
|
||||
'src/main.py',
|
||||
'python/prompt_chatgpt'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['PHP'],
|
||||
$templateRuntimes['PHP'],
|
||||
'composer install',
|
||||
'src/index.php',
|
||||
'php/prompt-chatgpt'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['DART'],
|
||||
$templateRuntimes['DART'],
|
||||
'dart pub get',
|
||||
'lib/main.dart',
|
||||
'dart/prompt_chatgpt'
|
||||
@@ -405,19 +374,19 @@ return [
|
||||
'useCases' => ['messaging'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install && npm run setup',
|
||||
'src/main.js',
|
||||
'node/discord-command-bot'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['PYTHON'],
|
||||
$templateRuntimes['PYTHON'],
|
||||
'pip install -r requirements.txt && python src/setup.py',
|
||||
'src/main.py',
|
||||
'python/discord_command_bot'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['GO'],
|
||||
$templateRuntimes['GO'],
|
||||
'',
|
||||
'main.go',
|
||||
'go/discord-command-bot'
|
||||
@@ -468,7 +437,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/analyze-with-perspectiveapi'
|
||||
@@ -504,19 +473,19 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/censor-with-redact'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['PYTHON'],
|
||||
$templateRuntimes['PYTHON'],
|
||||
'pip install -r requirements.txt',
|
||||
'src/main.py',
|
||||
'python/censor_with_redact'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['DART'],
|
||||
$templateRuntimes['DART'],
|
||||
'dart pub get',
|
||||
'lib/main.dart',
|
||||
'dart/censor_with_redact'
|
||||
@@ -550,7 +519,7 @@ return [
|
||||
'timeout' => 15,
|
||||
'useCases' => ['utilities'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(TEMPLATE_RUNTIMES['NODE'], 'npm install', 'src/main.js', 'node/generate-pdf')
|
||||
...getRuntimes($templateRuntimes['NODE'], 'npm install', 'src/main.js', 'node/generate-pdf')
|
||||
],
|
||||
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/generate-pdf">file</a>.',
|
||||
'vcsProvider' => 'github',
|
||||
@@ -573,7 +542,7 @@ return [
|
||||
'useCases' => ['dev-tools'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/github-issue-bot'
|
||||
@@ -616,7 +585,7 @@ return [
|
||||
'useCases' => ['utilities'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/url-shortener'
|
||||
@@ -667,19 +636,19 @@ return [
|
||||
'useCases' => ['databases'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/sync-with-algolia'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['PYTHON'],
|
||||
$templateRuntimes['PYTHON'],
|
||||
'pip install -r requirements.txt',
|
||||
'src/main.py',
|
||||
'python/sync_with_algolia'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['PHP'],
|
||||
$templateRuntimes['PHP'],
|
||||
'composer install',
|
||||
'src/index.php',
|
||||
'php/sync-with-algolia'
|
||||
@@ -748,31 +717,31 @@ return [
|
||||
'useCases' => ['databases'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/sync-with-meilisearch'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['PYTHON'],
|
||||
$templateRuntimes['PYTHON'],
|
||||
'pip install -r requirements.txt',
|
||||
'src/main.py',
|
||||
'python/sync-with-meilisearch'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['PHP'],
|
||||
$templateRuntimes['PHP'],
|
||||
'composer install',
|
||||
'src/index.php',
|
||||
'php/sync-with-meilisearch'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['BUN'],
|
||||
$templateRuntimes['BUN'],
|
||||
'bun install',
|
||||
'src/main.ts',
|
||||
'bun/sync-with-meilisearch'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['RUBY'],
|
||||
$templateRuntimes['RUBY'],
|
||||
'bundle install',
|
||||
'lib/main.rb',
|
||||
'ruby/sync-with-meilisearch'
|
||||
@@ -841,37 +810,37 @@ return [
|
||||
'useCases' => ['messaging'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/whatsapp-with-vonage'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['PYTHON'],
|
||||
$templateRuntimes['PYTHON'],
|
||||
'pip install -r requirements.txt',
|
||||
'src/main.py',
|
||||
'python/whatsapp_with_vonage'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['DART'],
|
||||
$templateRuntimes['DART'],
|
||||
'dart pub get',
|
||||
'lib/main.dart',
|
||||
'dart/whatsapp-with-vonage'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['PHP'],
|
||||
$templateRuntimes['PHP'],
|
||||
'composer install',
|
||||
'src/index.php',
|
||||
'php/whatsapp-with-vonage'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['BUN'],
|
||||
$templateRuntimes['BUN'],
|
||||
'bun install',
|
||||
'src/main.ts',
|
||||
'bun/whatsapp-with-vonage'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['RUBY'],
|
||||
$templateRuntimes['RUBY'],
|
||||
'bundle install',
|
||||
'lib/main.rb',
|
||||
'ruby/whatsapp-with-vonage'
|
||||
@@ -927,7 +896,7 @@ return [
|
||||
'useCases' => ['messaging'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/push-notification-with-fcm'
|
||||
@@ -983,19 +952,19 @@ return [
|
||||
'useCases' => ['utilities'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/email-contact-form'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['PYTHON'],
|
||||
$templateRuntimes['PYTHON'],
|
||||
'pip install -r requirements.txt',
|
||||
'src/main.py',
|
||||
'python/email_contact_form'
|
||||
),
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['PHP'],
|
||||
$templateRuntimes['PHP'],
|
||||
'composer install',
|
||||
'src/index.php',
|
||||
'php/email-contact-form'
|
||||
@@ -1066,7 +1035,7 @@ return [
|
||||
'useCases' => ['utilities'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/subscriptions-with-stripe'
|
||||
@@ -1107,7 +1076,7 @@ return [
|
||||
'useCases' => ['utilities'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/payments-with-stripe'
|
||||
@@ -1164,7 +1133,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/text-generation-with-huggingface'
|
||||
@@ -1198,7 +1167,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/language-translation-with-huggingface'
|
||||
@@ -1232,7 +1201,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install && npm run setup',
|
||||
'src/main.js',
|
||||
'node/image-classification-with-huggingface'
|
||||
@@ -1290,7 +1259,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install && npm run setup',
|
||||
'src/main.js',
|
||||
'node/object-detection-with-huggingface'
|
||||
@@ -1348,7 +1317,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install && npm run setup',
|
||||
'src/main.js',
|
||||
'node/speech-recognition-with-huggingface'
|
||||
@@ -1406,7 +1375,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install && npm run setup',
|
||||
'src/main.js',
|
||||
'node/text-to-speech-with-huggingface'
|
||||
@@ -1464,7 +1433,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/generate-with-replicate'
|
||||
@@ -1499,7 +1468,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/generate-with-together-ai'
|
||||
@@ -1541,7 +1510,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/chat-with-perplexity-ai'
|
||||
@@ -1581,7 +1550,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/generate-with-replicate'
|
||||
@@ -1616,7 +1585,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/sync-with-pinecone'
|
||||
@@ -1679,7 +1648,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/rag-with-langchain'
|
||||
@@ -1742,7 +1711,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/speak-with-elevenlabs'
|
||||
@@ -1797,7 +1766,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/speak-with-lmnt'
|
||||
@@ -1838,7 +1807,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/chat-with-anyscale'
|
||||
@@ -1879,7 +1848,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install && npm run setup',
|
||||
'src/main.js',
|
||||
'node/music-generation-with-huggingface'
|
||||
@@ -1921,7 +1890,7 @@ return [
|
||||
'useCases' => ['ai'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/generate-with-fal-ai'
|
||||
@@ -1956,7 +1925,7 @@ return [
|
||||
'useCases' => ['utilities'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/subscriptions-with-lemon-squeezy'
|
||||
@@ -2011,7 +1980,7 @@ return [
|
||||
'useCases' => ['utilities'],
|
||||
'runtimes' => [
|
||||
...getRuntimes(
|
||||
TEMPLATE_RUNTIMES['NODE'],
|
||||
$templateRuntimes['NODE'],
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/payments-with-lemon-squeezy'
|
||||
|
||||
@@ -58,6 +58,8 @@ $admins = [
|
||||
'health.read',
|
||||
'functions.read',
|
||||
'functions.write',
|
||||
'sites.read',
|
||||
'sites.write',
|
||||
'execution.read',
|
||||
'execution.write',
|
||||
'rules.read',
|
||||
|
||||
@@ -64,6 +64,12 @@ return [ // List of publicly visible scopes
|
||||
'functions.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s functions and code deployments',
|
||||
],
|
||||
'sites.read' => [
|
||||
'description' => 'Access to read your project\'s sites and deployments',
|
||||
],
|
||||
'sites.write' => [
|
||||
'description' => 'Access to create, update, and delete your project\'s sites and deployments',
|
||||
],
|
||||
'execution.read' => [
|
||||
'description' => 'Access to read your project\'s execution logs',
|
||||
],
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
const TEMPLATE_FRAMEWORKS = [
|
||||
'SVELTEKIT' => [
|
||||
'key' => 'sveltekit',
|
||||
'name' => 'SvelteKit',
|
||||
'installCommand' => 'npm install',
|
||||
'buildCommand' => 'npm run build',
|
||||
'outputDirectory' => './build',
|
||||
'buildRuntime' => 'node-22',
|
||||
'serveRuntime' => 'static-1',
|
||||
'fallbackFile' => null,
|
||||
],
|
||||
'NEXTJS' => [
|
||||
'key' => 'nextjs',
|
||||
'name' => 'Next.js',
|
||||
'installCommand' => 'npm install',
|
||||
'buildCommand' => 'npm run build',
|
||||
'outputDirectory' => './out',
|
||||
'buildRuntime' => 'node-22',
|
||||
'serveRuntime' => 'static-1',
|
||||
'fallbackFile' => null,
|
||||
],
|
||||
'NUXT' => [
|
||||
'key' => 'nuxt',
|
||||
'name' => 'Nuxt',
|
||||
'installCommand' => 'npm install',
|
||||
'buildCommand' => 'npm run generate',
|
||||
'outputDirectory' => './dist',
|
||||
'buildRuntime' => 'node-22',
|
||||
'serveRuntime' => 'static-1',
|
||||
'fallbackFile' => null,
|
||||
],
|
||||
'REMIX' => [
|
||||
'key' => 'remix',
|
||||
'name' => 'Remix',
|
||||
'installCommand' => 'npm install',
|
||||
'buildCommand' => 'npm run build',
|
||||
'outputDirectory' => './build/client',
|
||||
'buildRuntime' => 'node-22',
|
||||
'serveRuntime' => 'static-1',
|
||||
'fallbackFile' => null,
|
||||
],
|
||||
'ASTRO' => [
|
||||
'key' => 'astro',
|
||||
'name' => 'Astro',
|
||||
'installCommand' => 'npm install',
|
||||
'buildCommand' => 'npm run build',
|
||||
'outputDirectory' => './dist',
|
||||
'buildRuntime' => 'node-22',
|
||||
'serveRuntime' => 'static-1',
|
||||
'fallbackFile' => null,
|
||||
],
|
||||
/*
|
||||
'ANGULAR' => [
|
||||
'key' => 'angular',
|
||||
'name' => 'Angular',
|
||||
'installCommand' => 'npm install',
|
||||
'buildCommand' => 'npm run build',
|
||||
'outputDirectory' => './dist/starter/browser',
|
||||
'buildRuntime' => 'node-22',
|
||||
'serveRuntime' => 'static-1',
|
||||
'fallbackFile' => null,
|
||||
],
|
||||
*/
|
||||
'FLUTTER' => [
|
||||
'key' => 'flutter',
|
||||
'name' => 'Flutter',
|
||||
'installCommand' => '',
|
||||
'buildCommand' => 'flutter build web',
|
||||
'outputDirectory' => './build/web',
|
||||
'buildRuntime' => 'flutter-3.24',
|
||||
'serveRuntime' => 'static-1',
|
||||
'fallbackFile' => null,
|
||||
],
|
||||
];
|
||||
|
||||
function getFramework(string $frameworkEnum, array $overrides)
|
||||
{
|
||||
$settings = \array_merge(TEMPLATE_FRAMEWORKS[$frameworkEnum], $overrides);
|
||||
return $settings;
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
'key' => 'nextjs-starter',
|
||||
'name' => 'Next.js starter website',
|
||||
'useCases' => ['starter'],
|
||||
'demoUrl' => 'https://nextjs-starter.sites.qa17.appwrite.org/',
|
||||
'demoImage' => 'https://qa17.appwrite.org/console/images/sites/templates/nextjs-starter.png',
|
||||
'frameworks' => [
|
||||
getFramework('NEXTJS', [
|
||||
'providerRootDirectory' => './nextjs/starter',
|
||||
]),
|
||||
],
|
||||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates-for-sites',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerVersion' => '0.1.*',
|
||||
'variables' => [],
|
||||
],
|
||||
[
|
||||
'key' => 'nuxt-starter',
|
||||
'name' => 'Nuxt starter website',
|
||||
'useCases' => ['starter'],
|
||||
'demoUrl' => 'https://nuxt-starter.sites.qa17.appwrite.org/',
|
||||
'demoImage' => 'https://qa17.appwrite.org/console/images/sites/templates/nuxt-starter.png',
|
||||
'frameworks' => [
|
||||
getFramework('NUXT', [
|
||||
'providerRootDirectory' => './nuxt/starter',
|
||||
]),
|
||||
],
|
||||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates-for-sites',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerVersion' => '0.1.*',
|
||||
'variables' => [],
|
||||
],
|
||||
[
|
||||
'key' => 'sveltekit-starter',
|
||||
'name' => 'SvelteKit starter website',
|
||||
'useCases' => ['starter'],
|
||||
'demoUrl' => 'https://sveltekit-starter.sites.qa17.appwrite.org/',
|
||||
'demoImage' => 'https://qa17.appwrite.org/console/images/sites/templates/sveltekit-starter.png',
|
||||
'frameworks' => [
|
||||
getFramework('SVELTEKIT', [
|
||||
'providerRootDirectory' => './sveltekit/starter',
|
||||
]),
|
||||
],
|
||||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates-for-sites',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerVersion' => '0.1.*',
|
||||
'variables' => [],
|
||||
],
|
||||
[
|
||||
'key' => 'astro-starter',
|
||||
'name' => 'Astro starter website',
|
||||
'useCases' => ['starter'],
|
||||
'demoUrl' => 'https://astro-starter.sites.qa17.appwrite.org/',
|
||||
'demoImage' => 'https://qa17.appwrite.org/console/images/sites/templates/astro-starter.png',
|
||||
'frameworks' => [
|
||||
getFramework('ASTRO', [
|
||||
'providerRootDirectory' => './astro/starter',
|
||||
]),
|
||||
],
|
||||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates-for-sites',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerVersion' => '0.1.*',
|
||||
'variables' => [],
|
||||
],
|
||||
[
|
||||
'key' => 'remix-starter',
|
||||
'name' => 'Remix starter website',
|
||||
'useCases' => ['starter'],
|
||||
'demoUrl' => 'https://remix-starter.sites.qa17.appwrite.org/',
|
||||
'demoImage' => 'https://qa17.appwrite.org/console/images/sites/templates/remix-starter.png',
|
||||
'frameworks' => [
|
||||
getFramework('REMIX', [
|
||||
'providerRootDirectory' => './remix/starter',
|
||||
]),
|
||||
],
|
||||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates-for-sites',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerVersion' => '0.1.*',
|
||||
'variables' => [],
|
||||
],
|
||||
/*
|
||||
[
|
||||
'key' => 'angular-starter',
|
||||
'name' => 'Angular starter website',
|
||||
'useCases' => ['starter'],
|
||||
'demoUrl' => 'https://angular-starter.sites.qa17.appwrite.org/',
|
||||
'demoImage' => 'https://qa17.appwrite.org/console/images/sites/templates/angular-starter.png',
|
||||
'frameworks' => [
|
||||
getFramework('ANGULAR', [
|
||||
'providerRootDirectory' => './angular/starter',
|
||||
]),
|
||||
],
|
||||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates-for-sites',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerVersion' => '0.1.*',
|
||||
'variables' => [],
|
||||
],
|
||||
*/
|
||||
[
|
||||
'key' => 'flutter-starter',
|
||||
'name' => 'Flutter starter website',
|
||||
'useCases' => ['starter'],
|
||||
'demoUrl' => 'https://flutter-starter.sites.qa17.appwrite.org/',
|
||||
'demoImage' => 'https://qa17.appwrite.org/console/images/sites/templates/flutter-starter.png',
|
||||
'frameworks' => [
|
||||
getFramework('FLUTTER', [
|
||||
'providerRootDirectory' => './flutter/starter',
|
||||
]),
|
||||
],
|
||||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates-for-sites',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerVersion' => '0.1.*',
|
||||
'variables' => [],
|
||||
],
|
||||
];
|
||||
@@ -2784,7 +2784,7 @@
|
||||
"tags": [
|
||||
"account"
|
||||
],
|
||||
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
|
||||
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Token",
|
||||
@@ -4945,7 +4945,7 @@
|
||||
},
|
||||
"x-appwrite": {
|
||||
"method": "listExecutions",
|
||||
"weight": 306,
|
||||
"weight": 303,
|
||||
"cookies": false,
|
||||
"type": "",
|
||||
"deprecated": false,
|
||||
@@ -5033,7 +5033,7 @@
|
||||
},
|
||||
"x-appwrite": {
|
||||
"method": "createExecution",
|
||||
"weight": 305,
|
||||
"weight": 302,
|
||||
"cookies": false,
|
||||
"type": "",
|
||||
"deprecated": false,
|
||||
@@ -5150,7 +5150,7 @@
|
||||
},
|
||||
"x-appwrite": {
|
||||
"method": "getExecution",
|
||||
"weight": 307,
|
||||
"weight": 304,
|
||||
"cookies": false,
|
||||
"type": "",
|
||||
"deprecated": false,
|
||||
@@ -5226,7 +5226,7 @@
|
||||
},
|
||||
"x-appwrite": {
|
||||
"method": "query",
|
||||
"weight": 331,
|
||||
"weight": 329,
|
||||
"cookies": false,
|
||||
"type": "graphql",
|
||||
"deprecated": false,
|
||||
@@ -5280,7 +5280,7 @@
|
||||
},
|
||||
"x-appwrite": {
|
||||
"method": "mutation",
|
||||
"weight": 330,
|
||||
"weight": 328,
|
||||
"cookies": false,
|
||||
"type": "graphql",
|
||||
"deprecated": false,
|
||||
@@ -5766,7 +5766,7 @@
|
||||
},
|
||||
"x-appwrite": {
|
||||
"method": "createSubscriber",
|
||||
"weight": 382,
|
||||
"weight": 380,
|
||||
"cookies": false,
|
||||
"type": "",
|
||||
"deprecated": false,
|
||||
@@ -5851,7 +5851,7 @@
|
||||
},
|
||||
"x-appwrite": {
|
||||
"method": "deleteSubscriber",
|
||||
"weight": 386,
|
||||
"weight": 384,
|
||||
"cookies": false,
|
||||
"type": "",
|
||||
"deprecated": false,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2922,7 +2922,7 @@
|
||||
"tags": [
|
||||
"account"
|
||||
],
|
||||
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
|
||||
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Token",
|
||||
@@ -5101,7 +5101,7 @@
|
||||
},
|
||||
"x-appwrite": {
|
||||
"method": "listExecutions",
|
||||
"weight": 306,
|
||||
"weight": 303,
|
||||
"cookies": false,
|
||||
"type": "",
|
||||
"deprecated": false,
|
||||
@@ -5186,7 +5186,7 @@
|
||||
},
|
||||
"x-appwrite": {
|
||||
"method": "createExecution",
|
||||
"weight": 305,
|
||||
"weight": 302,
|
||||
"cookies": false,
|
||||
"type": "",
|
||||
"deprecated": false,
|
||||
@@ -5307,7 +5307,7 @@
|
||||
},
|
||||
"x-appwrite": {
|
||||
"method": "getExecution",
|
||||
"weight": 307,
|
||||
"weight": 304,
|
||||
"cookies": false,
|
||||
"type": "",
|
||||
"deprecated": false,
|
||||
@@ -5381,7 +5381,7 @@
|
||||
},
|
||||
"x-appwrite": {
|
||||
"method": "query",
|
||||
"weight": 331,
|
||||
"weight": 329,
|
||||
"cookies": false,
|
||||
"type": "graphql",
|
||||
"deprecated": false,
|
||||
@@ -5457,7 +5457,7 @@
|
||||
},
|
||||
"x-appwrite": {
|
||||
"method": "mutation",
|
||||
"weight": 330,
|
||||
"weight": 328,
|
||||
"cookies": false,
|
||||
"type": "graphql",
|
||||
"deprecated": false,
|
||||
@@ -5981,7 +5981,7 @@
|
||||
},
|
||||
"x-appwrite": {
|
||||
"method": "createSubscriber",
|
||||
"weight": 382,
|
||||
"weight": 380,
|
||||
"cookies": false,
|
||||
"type": "",
|
||||
"deprecated": false,
|
||||
@@ -6070,7 +6070,7 @@
|
||||
},
|
||||
"x-appwrite": {
|
||||
"method": "deleteSubscriber",
|
||||
"weight": 386,
|
||||
"weight": 384,
|
||||
"cookies": false,
|
||||
"type": "",
|
||||
"deprecated": false,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'NODE' => [
|
||||
'name' => 'node',
|
||||
'versions' => ['22', '21.0', '20.0', '19.0', '18.0', '16.0', '14.5']
|
||||
],
|
||||
'PYTHON' => [
|
||||
'name' => 'python',
|
||||
'versions' => ['3.12', '3.11', '3.10', '3.9', '3.8']
|
||||
],
|
||||
'DART' => [
|
||||
'name' => 'dart',
|
||||
'versions' => ['3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16']
|
||||
],
|
||||
'GO' => [
|
||||
'name' => 'go',
|
||||
'versions' => ['1.23']
|
||||
],
|
||||
'PHP' => [
|
||||
'name' => 'php',
|
||||
'versions' => ['8.3', '8.2', '8.1', '8.0']
|
||||
],
|
||||
'DENO' => [
|
||||
'name' => 'deno',
|
||||
'versions' => ['2.0', '1.46', '1.40', '1.35', '1.24', '1.21']
|
||||
],
|
||||
'BUN' => [
|
||||
'name' => 'bun',
|
||||
'versions' => ['1.1', '1.0']
|
||||
],
|
||||
'RUBY' => [
|
||||
'name' => 'ruby',
|
||||
'versions' => ['3.3', '3.2', '3.1', '3.0']
|
||||
],
|
||||
'FLUTTER' => [
|
||||
'name' => 'flutter',
|
||||
'versions' => ['3.24']
|
||||
],
|
||||
];
|
||||
@@ -46,11 +46,13 @@ App::get('/v1/console/variables')
|
||||
$variables = new Document([
|
||||
'_APP_DOMAIN_TARGET' => System::getEnv('_APP_DOMAIN_TARGET'),
|
||||
'_APP_STORAGE_LIMIT' => +System::getEnv('_APP_STORAGE_LIMIT'),
|
||||
'_APP_FUNCTIONS_SIZE_LIMIT' => +System::getEnv('_APP_FUNCTIONS_SIZE_LIMIT'),
|
||||
'_APP_COMPUTE_SIZE_LIMIT' => +System::getEnv('_APP_COMPUTE_SIZE_LIMIT'),
|
||||
'_APP_USAGE_STATS' => System::getEnv('_APP_USAGE_STATS'),
|
||||
'_APP_VCS_ENABLED' => $isVcsEnabled,
|
||||
'_APP_DOMAIN_ENABLED' => $isDomainEnabled,
|
||||
'_APP_ASSISTANT_ENABLED' => $isAssistantEnabled
|
||||
'_APP_ASSISTANT_ENABLED' => $isAssistantEnabled,
|
||||
'_APP_DOMAIN_SITES' => System::getEnv('_APP_DOMAIN_SITES'),
|
||||
'_APP_OPTIONS_FORCE_HTTPS' => System::getEnv('_APP_OPTIONS_FORCE_HTTPS')
|
||||
]);
|
||||
|
||||
$response->dynamic($variables, Response::MODEL_CONSOLE_VARIABLES);
|
||||
|
||||
@@ -20,7 +20,6 @@ use Utopia\Audit\Audit;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception as DatabaseException;
|
||||
use Utopia\Database\Exception\Authorization as AuthorizationException;
|
||||
use Utopia\Database\Exception\Conflict as ConflictException;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
@@ -397,19 +396,29 @@ function updateAttribute(
|
||||
}
|
||||
|
||||
if (!empty($newKey) && $key !== $newKey) {
|
||||
// Delete attribute and recreate since we can't modify IDs
|
||||
$original = clone $attribute;
|
||||
|
||||
$dbForProject->deleteDocument('attributes', $attribute->getId());
|
||||
$originalUid = $attribute->getId();
|
||||
|
||||
$attribute
|
||||
->setAttribute('$id', ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $newKey))
|
||||
->setAttribute('key', $newKey);
|
||||
|
||||
try {
|
||||
$attribute = $dbForProject->createDocument('attributes', $attribute);
|
||||
} catch (DatabaseException|PDOException) {
|
||||
$attribute = $dbForProject->createDocument('attributes', $original);
|
||||
$dbForProject->updateDocument('attributes', $originalUid, $attribute);
|
||||
|
||||
/**
|
||||
* @var Document $index
|
||||
*/
|
||||
foreach ($collection->getAttribute('indexes') as $index) {
|
||||
/**
|
||||
* @var string[] $attributes
|
||||
*/
|
||||
$attributes = $index->getAttribute('attributes', []);
|
||||
$found = \array_search($key, $attributes);
|
||||
|
||||
if ($found !== false) {
|
||||
$attributes[$found] = $newKey;
|
||||
$index->setAttribute('attributes', $attributes);
|
||||
$dbForProject->updateDocument('indexes', $index->getId(), $index);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$attribute = $dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key, $attribute);
|
||||
@@ -2587,7 +2596,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
|
||||
|
||||
$attributeStatus = $oldAttributes[$attributeIndex]['status'];
|
||||
$attributeType = $oldAttributes[$attributeIndex]['type'];
|
||||
$attributeSize = $oldAttributes[$attributeIndex]['size'];
|
||||
$attributeArray = $oldAttributes[$attributeIndex]['array'] ?? false;
|
||||
|
||||
if ($attributeType === Database::VAR_RELATIONSHIP) {
|
||||
@@ -2601,10 +2609,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
|
||||
|
||||
$lengths[$i] = null;
|
||||
|
||||
if ($attributeType === Database::VAR_STRING) {
|
||||
$lengths[$i] = $attributeSize; // set attribute size as index length only for strings
|
||||
}
|
||||
|
||||
if ($attributeArray === true) {
|
||||
$lengths[$i] = Database::ARRAY_INDEX_LENGTH;
|
||||
$orders[$i] = null;
|
||||
|
||||
+684
-685
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Datetime as DateTimeValidator;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
@@ -322,11 +323,12 @@ App::post('/v1/project/variables')
|
||||
->label('sdk.response.model', Response::MODEL_VARIABLE)
|
||||
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
|
||||
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
|
||||
->param('secret', false, new Boolean(), 'Is secret? Secret variables can only be updated or deleted, they cannot be read.', true)
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $key, string $value, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) {
|
||||
->action(function (string $key, string $value, bool $secret, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) {
|
||||
$variableId = ID::unique();
|
||||
|
||||
$variable = new Document([
|
||||
@@ -341,6 +343,7 @@ App::post('/v1/project/variables')
|
||||
'resourceType' => 'project',
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'secret' => $secret,
|
||||
'search' => implode(' ', [$variableId, $key, 'project']),
|
||||
]);
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ App::post('/v1/proxy/rules')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_PROXY_RULE)
|
||||
->param('domain', null, new ValidatorDomain(), 'Domain name.')
|
||||
->param('resourceType', null, new WhiteList(['api', 'function']), 'Action definition for the rule. Possible values are "api", "function"')
|
||||
->param('resourceType', null, new WhiteList(['api', 'function', 'site']), 'Action definition for the rule. Possible values are "api", "function" and "site"')
|
||||
->param('resourceId', '', new UID(), 'ID of resource for the action type. If resourceType is "api", leave empty. If resourceType is "function", provide ID of the function.', true)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
@@ -50,9 +50,15 @@ App::post('/v1/proxy/rules')
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your main domain to specific resource. Please use subdomain or a different domain.');
|
||||
}
|
||||
|
||||
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
|
||||
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
|
||||
if ($functionsDomain != '' && str_ends_with($domain, $functionsDomain)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your functions domain or it\'s subdomain to specific resource. Please use different domain.');
|
||||
|
||||
if (
|
||||
($functionsDomain !== '' && str_ends_with($domain, $functionsDomain)) ||
|
||||
($sitesDomain !== '' && str_ends_with($domain, $sitesDomain))
|
||||
) {
|
||||
// TODO: Refactor later
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your functions or sites domain or their subdomains to a specific resource. Please use a different domain.');
|
||||
}
|
||||
|
||||
if ($domain === 'localhost' || $domain === APP_HOSTNAME_INTERNAL) {
|
||||
@@ -81,18 +87,33 @@ App::post('/v1/proxy/rules')
|
||||
|
||||
$resourceInternalId = '';
|
||||
|
||||
if ($resourceType == 'function') {
|
||||
if (empty($resourceId)) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
switch ($resourceType) {
|
||||
case 'function':
|
||||
if (empty($resourceId)) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$function = $dbForProject->getDocument('functions', $resourceId);
|
||||
$function = $dbForProject->getDocument('functions', $resourceId);
|
||||
|
||||
if ($function->isEmpty()) {
|
||||
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
|
||||
}
|
||||
if ($function->isEmpty()) {
|
||||
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$resourceInternalId = $function->getInternalId();
|
||||
$resourceInternalId = $function->getInternalId();
|
||||
break;
|
||||
case 'site':
|
||||
if (empty($resourceId)) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$site = $dbForProject->getDocument('sites', $resourceId);
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$resourceInternalId = $site->getInternalId();
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -114,8 +135,8 @@ App::post('/v1/proxy/rules')
|
||||
]);
|
||||
|
||||
$status = 'created';
|
||||
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS');
|
||||
if (!empty($functionsDomain) && \str_ends_with($domain->get(), $functionsDomain)) {
|
||||
|
||||
if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) {
|
||||
$status = 'verified';
|
||||
}
|
||||
|
||||
@@ -348,3 +369,34 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
|
||||
|
||||
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
|
||||
});
|
||||
|
||||
App::get('/v1/proxy/subdomains')
|
||||
->desc('Check if subdomain is available')
|
||||
->groups(['api', 'proxy'])
|
||||
->label('scope', 'rules.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'proxy')
|
||||
->label('sdk.method', 'checkSubdomain')
|
||||
->label('sdk.description', '/docs/references/proxy/check-subdomain.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('resourceType', null, new WhiteList(['function', 'site']), 'Action definition for the rule. Possible values are "function" and "site"')
|
||||
->param('subdomain', '', new Text(256), 'Subdomain name.')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $resourceType, string $subdomain, Response $response, Database $dbForConsole) {
|
||||
//TODO: Add tests for this endpoint
|
||||
$resourceDomain = $resourceType === 'site' ? System::getEnv('_APP_DOMAIN_SITES', '') : System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
|
||||
$domain = $subdomain . '.' . $resourceDomain;
|
||||
|
||||
$document = $dbForConsole->findOne('rules', [
|
||||
Query::equal('domain', [$domain]),
|
||||
]);
|
||||
|
||||
if ($document && !$document->isEmpty()) {
|
||||
throw new Exception(Exception::RULE_ALREADY_EXISTS, 'Subdomain already assigned to different project.');
|
||||
}
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
@@ -805,8 +805,11 @@ App::get('/v1/teams/:teamId/memberships')
|
||||
}, $membershipsPrivacy);
|
||||
|
||||
$memberships = array_map(function ($membership) use ($dbForProject, $team, $membershipsPrivacy) {
|
||||
$user = !empty(array_filter($membershipsPrivacy))
|
||||
? $dbForProject->getDocument('users', $membership->getAttribute('userId'))
|
||||
: new Document();
|
||||
|
||||
if ($membershipsPrivacy['mfa']) {
|
||||
$user = $dbForProject->getDocument('users', $membership->getAttribute('userId'));
|
||||
$mfa = $user->getAttribute('mfa', false);
|
||||
|
||||
if ($mfa) {
|
||||
@@ -888,9 +891,11 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
|
||||
return $privacy || $isPrivilegedUser || $isAppUser;
|
||||
}, $membershipsPrivacy);
|
||||
|
||||
if ($membershipsPrivacy['mfa']) {
|
||||
$user = $dbForProject->getDocument('users', $membership->getAttribute('userId'));
|
||||
$user = !empty(array_filter($membershipsPrivacy))
|
||||
? $dbForProject->getDocument('users', $membership->getAttribute('userId'))
|
||||
: new Document();
|
||||
|
||||
if ($membershipsPrivacy['mfa']) {
|
||||
$mfa = $user->getAttribute('mfa', false);
|
||||
|
||||
if ($mfa) {
|
||||
|
||||
+64
-36
@@ -44,29 +44,30 @@ use function Swoole\Coroutine\batch;
|
||||
|
||||
$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 $dbForConsole, Build $queueForBuilds, callable $getProjectDB, Request $request) {
|
||||
$errors = [];
|
||||
foreach ($repositories as $resource) {
|
||||
foreach ($repositories as $repository) {
|
||||
try {
|
||||
$resourceType = $resource->getAttribute('resourceType');
|
||||
$resourceType = $repository->getAttribute('resourceType');
|
||||
|
||||
if ($resourceType !== "function") {
|
||||
if ($resourceType !== "function" && $resourceType !== "site") {
|
||||
continue;
|
||||
}
|
||||
|
||||
$projectId = $resource->getAttribute('projectId');
|
||||
$projectId = $repository->getAttribute('projectId');
|
||||
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
|
||||
$dbForProject = $getProjectDB($project);
|
||||
|
||||
$functionId = $resource->getAttribute('resourceId');
|
||||
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
|
||||
$functionInternalId = $function->getInternalId();
|
||||
$resourceCollection = $resourceType === "function" ? 'functions' : 'sites';
|
||||
$resourceId = $repository->getAttribute('resourceId');
|
||||
$resource = Authorization::skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
|
||||
$resourceInternalId = $resource->getInternalId();
|
||||
|
||||
$deploymentId = ID::unique();
|
||||
$repositoryId = $resource->getId();
|
||||
$repositoryInternalId = $resource->getInternalId();
|
||||
$providerRepositoryId = $resource->getAttribute('providerRepositoryId');
|
||||
$installationId = $resource->getAttribute('installationId');
|
||||
$installationInternalId = $resource->getAttribute('installationInternalId');
|
||||
$productionBranch = $function->getAttribute('providerBranch');
|
||||
$repositoryId = $repository->getId();
|
||||
$repositoryInternalId = $repository->getInternalId();
|
||||
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
|
||||
$installationId = $repository->getAttribute('installationId');
|
||||
$installationInternalId = $repository->getAttribute('installationInternalId');
|
||||
$productionBranch = $resource->getAttribute('providerBranch');
|
||||
$activate = false;
|
||||
|
||||
if ($providerBranch == $productionBranch && $external === false) {
|
||||
@@ -90,7 +91,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
$isAuthorized = !$external;
|
||||
|
||||
if (!$isAuthorized && !empty($providerPullRequestId)) {
|
||||
if (\in_array($providerPullRequestId, $resource->getAttribute('providerPullRequestIds', []))) {
|
||||
if (\in_array($providerPullRequestId, $repository->getAttribute('providerPullRequestIds', []))) {
|
||||
$isAuthorized = true;
|
||||
}
|
||||
}
|
||||
@@ -103,7 +104,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
|
||||
$latestCommentId = '';
|
||||
|
||||
if (!empty($providerPullRequestId) && $function->getAttribute('providerSilentMode', false) === false) {
|
||||
if (!empty($providerPullRequestId) && $resource->getAttribute('providerSilentMode', false) === false) {
|
||||
$latestComment = Authorization::skip(fn () => $dbForConsole->findOne('vcsComments', [
|
||||
Query::equal('providerRepositoryId', [$providerRepositoryId]),
|
||||
Query::equal('providerPullRequestId', [$providerPullRequestId]),
|
||||
@@ -112,14 +113,15 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
|
||||
if (!$latestComment->isEmpty()) {
|
||||
$latestCommentId = $latestComment->getAttribute('providerCommentId', '');
|
||||
|
||||
$comment = new Comment();
|
||||
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
|
||||
$comment->addBuild($project, $function, $commentStatus, $deploymentId, $action);
|
||||
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '', '');
|
||||
|
||||
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
|
||||
} else {
|
||||
$comment = new Comment();
|
||||
$comment->addBuild($project, $function, $commentStatus, $deploymentId, $action);
|
||||
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '', '');
|
||||
$latestCommentId = \strval($github->createComment($owner, $repositoryName, $providerPullRequestId, $comment->generateComment()));
|
||||
|
||||
if (!empty($latestCommentId)) {
|
||||
@@ -156,19 +158,19 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
$latestCommentId = $comment->getAttribute('providerCommentId', '');
|
||||
$comment = new Comment();
|
||||
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
|
||||
$comment->addBuild($project, $function, $commentStatus, $deploymentId, $action);
|
||||
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '', '');
|
||||
|
||||
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isAuthorized) {
|
||||
$functionName = $function->getAttribute('name');
|
||||
$resourceName = $resource->getAttribute('name');
|
||||
$projectName = $project->getAttribute('name');
|
||||
$name = "{$functionName} ({$projectName})";
|
||||
$name = "{$resourceName} ({$projectName})";
|
||||
$message = 'Authorization required for external contributor.';
|
||||
|
||||
$providerRepositoryId = $resource->getAttribute('providerRepositoryId');
|
||||
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
|
||||
try {
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
if (empty($repositoryName)) {
|
||||
@@ -195,11 +197,14 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceId' => $functionId,
|
||||
'resourceInternalId' => $functionInternalId,
|
||||
'resourceType' => 'functions',
|
||||
'entrypoint' => $function->getAttribute('entrypoint'),
|
||||
'commands' => $function->getAttribute('commands'),
|
||||
'resourceId' => $resourceId,
|
||||
'resourceInternalId' => $resourceInternalId,
|
||||
'resourceType' => $resourceCollection,
|
||||
'entrypoint' => $resource->getAttribute('entrypoint', ''),
|
||||
'commands' => $resource->getAttribute('commands', ''),
|
||||
'installCommand' => $resource->getAttribute('installCommand', ''),
|
||||
'buildCommand' => $resource->getAttribute('buildCommand', ''),
|
||||
'outputDirectory' => $resource->getAttribute('outputDirectory', ''),
|
||||
'type' => 'vcs',
|
||||
'installationId' => $installationId,
|
||||
'installationInternalId' => $installationInternalId,
|
||||
@@ -217,17 +222,40 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
'providerCommitUrl' => $providerCommitUrl,
|
||||
'providerCommentId' => \strval($latestCommentId),
|
||||
'providerBranch' => $providerBranch,
|
||||
'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint')]),
|
||||
'search' => implode(' ', [$deploymentId, $resource->getAttribute('entrypoint', '')]),
|
||||
'activate' => $activate,
|
||||
]));
|
||||
|
||||
if (!empty($providerCommitHash) && $function->getAttribute('providerSilentMode', false) === false) {
|
||||
$functionName = $function->getAttribute('name');
|
||||
// Preview deployments for sites
|
||||
if ($resource->getCollection() === 'sites') {
|
||||
$projectId = $project->getId();
|
||||
|
||||
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
|
||||
$domain = "{$deploymentId}-{$projectId}.{$sitesDomain}";
|
||||
$ruleId = md5($domain);
|
||||
|
||||
$rule = Authorization::skip(
|
||||
fn () => $dbForConsole->createDocument('rules', new Document([
|
||||
'$id' => $ruleId,
|
||||
'projectId' => $project->getId(),
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'domain' => $domain,
|
||||
'resourceType' => 'deployment',
|
||||
'resourceId' => $deploymentId,
|
||||
'resourceInternalId' => $deployment->getInternalId(),
|
||||
'status' => 'verified',
|
||||
'certificateId' => '',
|
||||
]))
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($providerCommitHash) && $resource->getAttribute('providerSilentMode', false) === false) {
|
||||
$resourceName = $resource->getAttribute('name');
|
||||
$projectName = $project->getAttribute('name');
|
||||
$name = "{$functionName} ({$projectName})";
|
||||
$name = "{$resourceName} ({$projectName})";
|
||||
$message = 'Starting...';
|
||||
|
||||
$providerRepositoryId = $resource->getAttribute('providerRepositoryId');
|
||||
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
|
||||
try {
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
if (empty($repositoryName)) {
|
||||
@@ -238,17 +266,17 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
|
||||
}
|
||||
$owner = $github->getOwnerName($providerInstallationId);
|
||||
|
||||
$providerTargetUrl = $request->getProtocol() . '://' . $request->getHostname() . "/console/project-$projectId/functions/function-$functionId";
|
||||
$providerTargetUrl = $request->getProtocol() . '://' . $request->getHostname() . "/console/project-$projectId/$resourceCollection/$resourceType-$resourceId";
|
||||
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'pending', $message, $providerTargetUrl, $name);
|
||||
}
|
||||
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
->setResource($function)
|
||||
->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
|
||||
$queueForBuilds->trigger(); // must trigger here so that we create a build for each function/site
|
||||
|
||||
//TODO: Add event?
|
||||
} catch (Throwable $e) {
|
||||
@@ -934,7 +962,7 @@ App::post('/v1/vcs/github/events')
|
||||
|
||||
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
|
||||
|
||||
//find functionId from functions table
|
||||
//find resourceId from relevant resources table
|
||||
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
|
||||
Query::equal('providerRepositoryId', [$providerRepositoryId]),
|
||||
Query::limit(100),
|
||||
@@ -946,7 +974,7 @@ App::post('/v1/vcs/github/events')
|
||||
}
|
||||
} elseif ($event == $github::EVENT_INSTALLATION) {
|
||||
if ($parsedPayload["action"] == "deleted") {
|
||||
// TODO: Use worker for this job instead (update function as well)
|
||||
// TODO: Use worker for this job instead (update function/site as well)
|
||||
$providerInstallationId = $parsedPayload["installationId"];
|
||||
|
||||
$installations = $dbForConsole->find('installations', [
|
||||
|
||||
+143
-75
@@ -10,6 +10,7 @@ use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Usage;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
|
||||
use Appwrite\Utopia\Request\Filters\V17 as RequestV17;
|
||||
@@ -37,6 +38,7 @@ use Utopia\Logger\Adapter\Sentry;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Logger\Log\User;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Platform\Service;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\Hostname;
|
||||
use Utopia\Validator\Text;
|
||||
@@ -51,14 +53,14 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
|
||||
|
||||
$host = $request->getHostname() ?? '';
|
||||
|
||||
$route = Authorization::skip(fn () => $dbForConsole->getDocument('rules', md5($host)));
|
||||
$rule = Authorization::skip(fn () => $dbForConsole->getDocument('rules', md5($host)));
|
||||
|
||||
if ($route->isEmpty()) {
|
||||
if ($host === System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) {
|
||||
if ($rule->isEmpty()) {
|
||||
if ($host === System::getEnv('_APP_DOMAIN_FUNCTIONS', '') || $host === System::getEnv('_APP_DOMAIN_SITES', '')) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain cannot be used for security reasons. Please use any subdomain instead.');
|
||||
}
|
||||
|
||||
if (\str_ends_with($host, System::getEnv('_APP_DOMAIN_FUNCTIONS', ''))) {
|
||||
if (\str_ends_with($host, System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) || \str_ends_with($host, System::getEnv('_APP_DOMAIN_SITES', ''))) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain is not connected to any Appwrite resource yet. Please configure custom domain or function domain to allow this request.');
|
||||
}
|
||||
|
||||
@@ -70,13 +72,12 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
|
||||
|
||||
// Act as API - no Proxy logic
|
||||
$utopia->getRoute()?->label('error', '');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$projectId = $route->getAttribute('projectId');
|
||||
$project = Authorization::skip(
|
||||
fn () => $dbForConsole->getDocument('projects', $projectId)
|
||||
);
|
||||
$projectId = $rule->getAttribute('projectId');
|
||||
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
|
||||
if (array_key_exists('proxy', $project->getAttribute('services', []))) {
|
||||
$status = $project->getAttribute('services', [])['proxy'];
|
||||
if (!$status) {
|
||||
@@ -90,24 +91,29 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
|
||||
return false;
|
||||
}
|
||||
|
||||
$type = $route->getAttribute('resourceType');
|
||||
$type = $rule->getAttribute('resourceType');
|
||||
|
||||
if ($type === 'function' || $type === 'site' || $type === 'deployment') {
|
||||
$resourceCollection = match($type) {
|
||||
'function' => 'functions',
|
||||
'site' => 'sites',
|
||||
'deployment' => 'deployments',
|
||||
};
|
||||
|
||||
if ($type === 'function') {
|
||||
$utopia->getRoute()?->label('sdk.namespace', 'functions');
|
||||
$utopia->getRoute()?->label('sdk.method', 'createExecution');
|
||||
|
||||
if (System::getEnv('_APP_OPTIONS_FUNCTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
|
||||
if (System::getEnv('_APP_OPTIONS_COMPUTE_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
|
||||
if ($request->getProtocol() !== 'https') {
|
||||
if ($request->getMethod() !== Request::METHOD_GET) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.');
|
||||
}
|
||||
|
||||
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
}
|
||||
}
|
||||
|
||||
$functionId = $route->getAttribute('resourceId');
|
||||
$projectId = $route->getAttribute('projectId');
|
||||
$resourceId = $rule->getAttribute('resourceId');
|
||||
$projectId = $rule->getAttribute('projectId');
|
||||
|
||||
$path = ($swooleRequest->server['request_uri'] ?? '/');
|
||||
$query = ($swooleRequest->server['query_string'] ?? '');
|
||||
@@ -115,7 +121,6 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
|
||||
$path .= '?' . $query;
|
||||
}
|
||||
|
||||
|
||||
$body = $swooleRequest->getContent() ?? '';
|
||||
$method = $swooleRequest->server['request_method'];
|
||||
|
||||
@@ -123,31 +128,53 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
|
||||
|
||||
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
|
||||
|
||||
/** @var Database $dbForProject */
|
||||
$dbForProject = $getProjectDB($project);
|
||||
|
||||
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
|
||||
if ($resourceCollection === 'deployments') {
|
||||
$subResource = Authorization::skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
|
||||
$resource = Authorization::skip(fn () => $dbForProject->getDocument($subResource->getAttribute('resourceType'), $subResource->getAttribute('resourceId')));
|
||||
} else {
|
||||
$resource = Authorization::skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
|
||||
}
|
||||
|
||||
if ($function->isEmpty() || !$function->getAttribute('enabled')) {
|
||||
if ($resource->isEmpty() || !$resource->getAttribute('enabled')) {
|
||||
throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($isResourceBlocked($project, RESOURCE_TYPE_FUNCTIONS, $functionId)) {
|
||||
if ($isResourceBlocked($project, RESOURCE_TYPE_FUNCTIONS, $resourceId)) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_RESOURCE_BLOCKED);
|
||||
}
|
||||
|
||||
$version = $function->getAttribute('version', 'v2');
|
||||
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
|
||||
$spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_FUNCTION_SPECIFICATION_DEFAULT)];
|
||||
$version = match($type) {
|
||||
'function' => $resource->getAttribute('version', 'v2'),
|
||||
'site' => 'v4',
|
||||
'deployment' => 'v4'
|
||||
};
|
||||
|
||||
$runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null;
|
||||
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
|
||||
$spec = Config::getParam('runtime-specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
|
||||
|
||||
$runtime = match($type) {
|
||||
'function' => $runtimes[$resource->getAttribute('runtime')] ?? null,
|
||||
'site' => $runtimes[$resource->getAttribute('serveRuntime')] ?? null,
|
||||
'deployment' => $runtimes[$resource->getAttribute('serveRuntime')] ?? null,
|
||||
default => null
|
||||
};
|
||||
|
||||
if (\is_null($runtime)) {
|
||||
throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
|
||||
throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported');
|
||||
}
|
||||
|
||||
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', '')));
|
||||
$deploymentId = match($type) {
|
||||
'function' => $resource->getAttribute('deployment', ''),
|
||||
'site' => $resource->getAttribute('deploymentId', ''),
|
||||
'deployment' => $subResource->getId()
|
||||
};
|
||||
|
||||
if ($deployment->getAttribute('resourceId') !== $function->getId()) {
|
||||
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $deploymentId));
|
||||
|
||||
if ($deployment->getAttribute('resourceId') !== $resource->getId()) {
|
||||
throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function');
|
||||
}
|
||||
|
||||
@@ -165,28 +192,34 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
|
||||
throw new AppwriteException(AppwriteException::BUILD_NOT_READY);
|
||||
}
|
||||
|
||||
$permissions = $function->getAttribute('execute');
|
||||
//todo: figure out for sites/functions
|
||||
if ($type === 'function') {
|
||||
$permissions = $resource->getAttribute('execute');
|
||||
|
||||
if (!(\in_array('any', $permissions)) && !(\in_array('guests', $permissions))) {
|
||||
throw new AppwriteException(AppwriteException::USER_UNAUTHORIZED, 'To execute function using domain, execute permissions must include "any" or "guests"');
|
||||
if (!(\in_array('any', $permissions)) && !(\in_array('guests', $permissions))) {
|
||||
throw new AppwriteException(AppwriteException::USER_UNAUTHORIZED, 'To execute function using domain, execute permissions must include "any" or "guests"');
|
||||
}
|
||||
}
|
||||
|
||||
$jwtExpiry = $function->getAttribute('timeout', 900);
|
||||
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
|
||||
$apiKey = $jwtObj->encode([
|
||||
'projectId' => $project->getId(),
|
||||
'scopes' => $function->getAttribute('scopes', [])
|
||||
]);
|
||||
|
||||
$headers = \array_merge([], $requestHeaders);
|
||||
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey;
|
||||
$headers['x-appwrite-trigger'] = 'http';
|
||||
$headers['x-appwrite-user-id'] = '';
|
||||
$headers['x-appwrite-user-jwt'] = '';
|
||||
$headers['x-appwrite-country-code'] = '';
|
||||
$headers['x-appwrite-continent-code'] = '';
|
||||
$headers['x-appwrite-continent-eu'] = 'false';
|
||||
|
||||
//todo: check if this would work for sites
|
||||
if ($type === 'function') {
|
||||
$jwtExpiry = $resource->getAttribute('timeout', 900);
|
||||
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
|
||||
$apiKey = $jwtObj->encode([
|
||||
'projectId' => $project->getId(),
|
||||
'scopes' => $resource->getAttribute('scopes', [])
|
||||
]);
|
||||
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey;
|
||||
$headers['x-appwrite-trigger'] = 'http';
|
||||
$headers['x-appwrite-user-jwt'] = '';
|
||||
}
|
||||
|
||||
$ip = $headers['x-real-ip'] ?? '';
|
||||
if (!empty($ip)) {
|
||||
$record = $geodb->get($ip);
|
||||
@@ -212,8 +245,8 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
|
||||
$execution = new Document([
|
||||
'$id' => $executionId,
|
||||
'$permissions' => [],
|
||||
'functionInternalId' => $function->getInternalId(),
|
||||
'functionId' => $function->getId(),
|
||||
'functionInternalId' => $resource->getInternalId(),
|
||||
'functionId' => $resource->getId(),
|
||||
'deploymentInternalId' => $deployment->getInternalId(),
|
||||
'deploymentId' => $deployment->getId(),
|
||||
'trigger' => 'http', // http / schedule / event
|
||||
@@ -226,13 +259,13 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
|
||||
'errors' => '',
|
||||
'logs' => '',
|
||||
'duration' => 0.0,
|
||||
'search' => implode(' ', [$functionId, $executionId]),
|
||||
'search' => implode(' ', [$resourceId, $executionId]),
|
||||
]);
|
||||
|
||||
$queueForEvents
|
||||
->setParam('functionId', $function->getId())
|
||||
->setParam('functionId', $resource->getId())
|
||||
->setParam('executionId', $execution->getId())
|
||||
->setContext('function', $function);
|
||||
->setContext('function', $resource);
|
||||
|
||||
$durationStart = \microtime(true);
|
||||
|
||||
@@ -249,12 +282,12 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
|
||||
}
|
||||
|
||||
// Shared vars
|
||||
foreach ($function->getAttribute('varsProject', []) as $var) {
|
||||
foreach ($resource->getAttribute('varsProject', []) as $var) {
|
||||
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
|
||||
}
|
||||
|
||||
// Function vars
|
||||
foreach ($function->getAttribute('vars', []) as $var) {
|
||||
foreach ($resource->getAttribute('vars', []) as $var) {
|
||||
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
|
||||
}
|
||||
|
||||
@@ -265,14 +298,14 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
|
||||
// Appwrite vars
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
|
||||
'APPWRITE_FUNCTION_ID' => $functionId,
|
||||
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'),
|
||||
'APPWRITE_FUNCTION_ID' => $resourceId,
|
||||
'APPWRITE_FUNCTION_NAME' => $resource->getAttribute('name'),
|
||||
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
|
||||
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
|
||||
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
|
||||
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
|
||||
'APPWRITE_FUNCTION_CPUS' => $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT,
|
||||
'APPWRITE_FUNCTION_MEMORY' => $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT,
|
||||
'APPWRITE_COMPUTE_CPUS' => $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
|
||||
'APPWRITE_COMPUTE_MEMORY' => $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
|
||||
'APPWRITE_VERSION' => APP_VERSION_STABLE,
|
||||
'APPWRITE_REGION' => $project->getAttribute('region'),
|
||||
'APPWRITE_DEPLOYMENT_TYPE' => $deployment->getAttribute('type', ''),
|
||||
@@ -293,26 +326,38 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
|
||||
/** Execute function */
|
||||
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
|
||||
try {
|
||||
$version = $function->getAttribute('version', 'v2');
|
||||
$command = $runtime['startCommand'];
|
||||
$command = $version === 'v2' ? '' : 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"';
|
||||
$version = match($type) {
|
||||
'function' => $resource->getAttribute('version', 'v2'),
|
||||
'site' => 'v4',
|
||||
'deployment' => 'v4'
|
||||
};
|
||||
$entrypoint = match($type) {
|
||||
'function' => $deployment->getAttribute('entrypoint', ''),
|
||||
'site' => '',
|
||||
'deployment' => ''
|
||||
};
|
||||
$runtimeEntrypoint = match ($version) {
|
||||
'v2' => '',
|
||||
default => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $runtime['startCommand'] . '"'
|
||||
};
|
||||
|
||||
$executionResponse = $executor->createExecution(
|
||||
projectId: $project->getId(),
|
||||
deploymentId: $deployment->getId(),
|
||||
body: \strlen($body) > 0 ? $body : null,
|
||||
variables: $vars,
|
||||
timeout: $function->getAttribute('timeout', 0),
|
||||
timeout: $resource->getAttribute('timeout', 30),
|
||||
image: $runtime['image'],
|
||||
source: $build->getAttribute('path', ''),
|
||||
entrypoint: $deployment->getAttribute('entrypoint', ''),
|
||||
entrypoint: $entrypoint,
|
||||
version: $version,
|
||||
path: $path,
|
||||
method: $method,
|
||||
headers: $headers,
|
||||
runtimeEntrypoint: $command,
|
||||
cpus: $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT,
|
||||
memory: $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT,
|
||||
logging: $function->getAttribute('logging', true),
|
||||
runtimeEntrypoint: $runtimeEntrypoint,
|
||||
cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
|
||||
memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
|
||||
logging: $resource->getAttribute('logging', true),
|
||||
requestTimeout: 30
|
||||
);
|
||||
|
||||
@@ -331,7 +376,6 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
|
||||
$execution->setAttribute('logs', $executionResponse['logs']);
|
||||
$execution->setAttribute('errors', $executionResponse['errors']);
|
||||
$execution->setAttribute('duration', $executionResponse['duration']);
|
||||
|
||||
} catch (\Throwable $th) {
|
||||
$durationEnd = \microtime(true);
|
||||
|
||||
@@ -355,22 +399,29 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_NETWORK_REQUESTS, 1)
|
||||
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
|
||||
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
|
||||
->addMetric(METRIC_EXECUTIONS, 1)
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
|
||||
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
|
||||
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
|
||||
->setProject($project)
|
||||
->trigger()
|
||||
;
|
||||
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
|
||||
//todo: add metrics for sites
|
||||
if ($type === 'function') {
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_EXECUTIONS, 1)
|
||||
->addMetric(str_replace('{functionInternalId}', $resource->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
|
||||
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project
|
||||
->addMetric(str_replace('{functionInternalId}', $resource->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
|
||||
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
|
||||
->addMetric(str_replace('{functionInternalId}', $resource->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)));
|
||||
}
|
||||
|
||||
$queueForFunctions
|
||||
->setType(Func::TYPE_ASYNC_WRITE)
|
||||
->setExecution($execution)
|
||||
$queueForUsage
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
|
||||
if ($type === 'function') {
|
||||
$queueForFunctions
|
||||
->setType(Func::TYPE_ASYNC_WRITE)
|
||||
->setExecution($execution)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
$execution->setAttribute('logs', '');
|
||||
@@ -465,6 +516,7 @@ App::init()
|
||||
// Only run Router when external domain
|
||||
if ($host !== $mainDomain) {
|
||||
if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked)) {
|
||||
$utopia->getRoute()?->label('router', 'true');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -683,6 +735,7 @@ App::options()
|
||||
// Only run Router when external domain
|
||||
if ($host !== $mainDomain) {
|
||||
if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked)) {
|
||||
$utopia->getRoute()?->label('router', 'true');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -977,7 +1030,9 @@ App::get('/robots.txt')
|
||||
$template = new View(__DIR__ . '/../views/general/robots.phtml');
|
||||
$response->text($template->render(false));
|
||||
} else {
|
||||
router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked);
|
||||
if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked)) {
|
||||
$utopia->getRoute()?->label('router', 'true');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1004,7 +1059,9 @@ App::get('/humans.txt')
|
||||
$template = new View(__DIR__ . '/../views/general/humans.phtml');
|
||||
$response->text($template->render(false));
|
||||
} else {
|
||||
router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked);
|
||||
if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked)) {
|
||||
$utopia->getRoute()?->label('router', 'true');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1096,10 +1153,21 @@ App::get('/v1/ping')
|
||||
App::wildcard()
|
||||
->groups(['api'])
|
||||
->label('scope', 'global')
|
||||
->action(function () {
|
||||
->inject('utopia')
|
||||
->action(function (App $utopia) {
|
||||
$handeledByRouter = $utopia->getRoute()?->getLabel('router', 'false');
|
||||
if (\boolval($handeledByRouter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
|
||||
});
|
||||
|
||||
foreach (Config::getParam('services', []) as $service) {
|
||||
include_once $service['controller'];
|
||||
}
|
||||
|
||||
// Modules
|
||||
|
||||
$platform = new Appwrite();
|
||||
$platform->init(Service::TYPE_HTTP);
|
||||
|
||||
@@ -163,6 +163,15 @@ $usageDatabaseListener = function (string $event, Document $document, Usage $que
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_FUNCTIONS, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForUsage
|
||||
->addReduce($document);
|
||||
}
|
||||
break;
|
||||
case $document->getCollection() === 'sites':
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_SITES, $value); // per project
|
||||
|
||||
if ($event === Database::EVENT_DOCUMENT_DELETE) {
|
||||
$queueForUsage
|
||||
->addReduce($document);
|
||||
|
||||
+28
-5
@@ -135,6 +135,7 @@ const APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH = 1_073_741_824; // 2^32 bits / 4
|
||||
const APP_DATABASE_TIMEOUT_MILLISECONDS = 15_000;
|
||||
const APP_DATABASE_QUERY_MAX_VALUES = 500;
|
||||
const APP_STORAGE_UPLOADS = '/storage/uploads';
|
||||
const APP_STORAGE_SITES = '/storage/sites';
|
||||
const APP_STORAGE_FUNCTIONS = '/storage/functions';
|
||||
const APP_STORAGE_BUILDS = '/storage/builds';
|
||||
const APP_STORAGE_CACHE = '/storage/cache';
|
||||
@@ -153,9 +154,9 @@ const APP_SOCIAL_DEV = 'https://dev.to/appwrite';
|
||||
const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite';
|
||||
const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=1';
|
||||
const APP_HOSTNAME_INTERNAL = 'appwrite';
|
||||
const APP_FUNCTION_SPECIFICATION_DEFAULT = Specification::S_1VCPU_512MB;
|
||||
const APP_FUNCTION_CPUS_DEFAULT = 0.5;
|
||||
const APP_FUNCTION_MEMORY_DEFAULT = 512;
|
||||
const APP_COMPUTE_CPUS_DEFAULT = 0.5;
|
||||
const APP_COMPUTE_MEMORY_DEFAULT = 512;
|
||||
const APP_COMPUTE_SPECIFICATION_DEFAULT = Specification::S_1VCPU_512MB;
|
||||
const APP_PLATFORM_SERVER = 'server';
|
||||
const APP_PLATFORM_CLIENT = 'client';
|
||||
const APP_PLATFORM_CONSOLE = 'console';
|
||||
@@ -258,6 +259,7 @@ const METRIC_FILES = 'files';
|
||||
const METRIC_FILES_STORAGE = 'files.storage';
|
||||
const METRIC_BUCKET_ID_FILES = '{bucketInternalId}.files';
|
||||
const METRIC_BUCKET_ID_FILES_STORAGE = '{bucketInternalId}.files.storage';
|
||||
const METRIC_SITES = 'sites';
|
||||
const METRIC_FUNCTIONS = 'functions';
|
||||
const METRIC_DEPLOYMENTS = 'deployments';
|
||||
const METRIC_DEPLOYMENTS_STORAGE = 'deployments.storage';
|
||||
@@ -276,15 +278,29 @@ const METRIC_FUNCTION_ID_BUILDS_STORAGE = '{functionInternalId}.builds.storage';
|
||||
const METRIC_FUNCTION_ID_BUILDS_COMPUTE = '{functionInternalId}.builds.compute';
|
||||
const METRIC_FUNCTION_ID_BUILDS_COMPUTE_SUCCESS = '{functionInternalId}.builds.compute.success';
|
||||
const METRIC_FUNCTION_ID_BUILDS_COMPUTE_FAILED = '{functionInternalId}.builds.compute.failed';
|
||||
const METRIC_FUNCTION_ID_BUILDS_MB_SECONDS = '{functionInternalId}.builds.mbSeconds';
|
||||
const METRIC_SITES_ID_BUILDS = 'sites.{siteInternalId}.builds';
|
||||
const METRIC_SITES_ID_BUILDS_SUCCESS = 'sites.{siteInternalId}.builds.success';
|
||||
const METRIC_SITES_ID_BUILDS_FAILED = 'sites.{siteInternalId}.builds.failed';
|
||||
const METRIC_SITES_ID_BUILDS_STORAGE = 'sites.{siteInternalId}.builds.storage';
|
||||
const METRIC_SITES_ID_BUILDS_COMPUTE = 'sites.{siteInternalId}.builds.compute';
|
||||
const METRIC_SITES_ID_BUILDS_COMPUTE_SUCCESS = 'sites.{siteInternalId}.builds.compute.success';
|
||||
const METRIC_SITES_ID_BUILDS_COMPUTE_FAILED = 'sites.{siteInternalId}.builds.compute.failed';
|
||||
const METRIC_SITES_ID_BUILDS_MB_SECONDS = 'sites.{siteInternalId}.builds.mbSeconds';
|
||||
const METRIC_FUNCTION_ID_DEPLOYMENTS = '{resourceType}.{resourceInternalId}.deployments';
|
||||
const METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE = '{resourceType}.{resourceInternalId}.deployments.storage';
|
||||
const METRIC_FUNCTION_ID_BUILDS_MB_SECONDS = '{functionInternalId}.builds.mbSeconds';
|
||||
const METRIC_EXECUTIONS = 'executions';
|
||||
const METRIC_EXECUTIONS_COMPUTE = 'executions.compute';
|
||||
const METRIC_EXECUTIONS_MB_SECONDS = 'executions.mbSeconds';
|
||||
const METRIC_FUNCTION_ID_EXECUTIONS = '{functionInternalId}.executions';
|
||||
const METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE = '{functionInternalId}.executions.compute';
|
||||
const METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS = '{functionInternalId}.executions.mbSeconds';
|
||||
const METRIC_SITE_ID_DEPLOYMENTS = '{resourceType}.{resourceInternalId}.deployments';
|
||||
const METRIC_SITE_ID_DEPLOYMENTS_STORAGE = '{resourceType}.{resourceInternalId}.deployments.storage';
|
||||
const METRIC_SITE_ID_BUILDS = '{siteInternalId}.builds';
|
||||
const METRIC_SITE_ID_BUILDS_STORAGE = '{siteInternalId}.builds.storage';
|
||||
const METRIC_SITE_ID_BUILDS_COMPUTE = '{siteInternalId}.builds.compute';
|
||||
const METRIC_SITE_ID_BUILDS_MB_SECONDS = '{siteInternalId}.builds.mbSeconds';
|
||||
const METRIC_NETWORK_REQUESTS = 'network.requests';
|
||||
const METRIC_NETWORK_INBOUND = 'network.inbound';
|
||||
const METRIC_NETWORK_OUTBOUND = 'network.outbound';
|
||||
@@ -313,6 +329,7 @@ if (!App::isProduction()) {
|
||||
/*
|
||||
* ENV vars
|
||||
*/
|
||||
Config::load('template-runtimes', __DIR__ . '/config/template-runtimes.php');
|
||||
Config::load('events', __DIR__ . '/config/events.php');
|
||||
Config::load('auth', __DIR__ . '/config/auth.php');
|
||||
Config::load('apis', __DIR__ . '/config/apis.php'); // List of APIs
|
||||
@@ -320,6 +337,7 @@ Config::load('errors', __DIR__ . '/config/errors.php');
|
||||
Config::load('oAuthProviders', __DIR__ . '/config/oAuthProviders.php');
|
||||
Config::load('platforms', __DIR__ . '/config/platforms.php');
|
||||
Config::load('collections', __DIR__ . '/config/collections.php');
|
||||
Config::load('frameworks', __DIR__ . '/config/frameworks.php');
|
||||
Config::load('runtimes', __DIR__ . '/config/runtimes.php');
|
||||
Config::load('runtimes-v2', __DIR__ . '/config/runtimes-v2.php');
|
||||
Config::load('usage', __DIR__ . '/config/usage.php');
|
||||
@@ -345,6 +363,7 @@ Config::load('storage-inputs', __DIR__ . '/config/storage/inputs.php');
|
||||
Config::load('storage-outputs', __DIR__ . '/config/storage/outputs.php');
|
||||
Config::load('runtime-specifications', __DIR__ . '/config/runtimes/specifications.php');
|
||||
Config::load('function-templates', __DIR__ . '/config/function-templates.php');
|
||||
Config::load('site-templates', __DIR__ . '/config/site-templates.php');
|
||||
|
||||
/**
|
||||
* New DB Filters
|
||||
@@ -567,7 +586,7 @@ Database::addFilter(
|
||||
return $database
|
||||
->find('variables', [
|
||||
Query::equal('resourceInternalId', [$document->getInternalId()]),
|
||||
Query::equal('resourceType', ['function']),
|
||||
Query::equal('resourceType', ['function', 'site']),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
]);
|
||||
}
|
||||
@@ -1542,6 +1561,10 @@ App::setResource('deviceForFiles', function ($project) {
|
||||
return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId());
|
||||
}, ['project']);
|
||||
|
||||
App::setResource('deviceForSites', function ($project) {
|
||||
return getDevice(APP_STORAGE_SITES . '/app-' . $project->getId());
|
||||
}, ['project']);
|
||||
|
||||
App::setResource('deviceForFunctions', function ($project) {
|
||||
return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId());
|
||||
}, ['project']);
|
||||
|
||||
+27
-4
@@ -29,6 +29,7 @@ use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Telemetry\Adapter\None as NoTelemetry;
|
||||
use Utopia\WebSocket\Adapter;
|
||||
use Utopia\WebSocket\Server;
|
||||
|
||||
@@ -142,6 +143,13 @@ if (!function_exists('getRealtime')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('getTelemetry')) {
|
||||
function getTelemetry(int $workerId): Utopia\Telemetry\Adapter
|
||||
{
|
||||
return new NoTelemetry();
|
||||
}
|
||||
}
|
||||
|
||||
$realtime = getRealtime();
|
||||
|
||||
/**
|
||||
@@ -274,6 +282,12 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
|
||||
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, $logError) {
|
||||
Console::success('Worker ' . $workerId . ' started successfully');
|
||||
|
||||
$telemetry = getTelemetry($workerId);
|
||||
$register->set('telemetry', fn () => $telemetry);
|
||||
$register->set('telemetry.connectionCounter', fn () => $telemetry->createUpDownCounter('realtime.server.open_connections'));
|
||||
$register->set('telemetry.connectionCreatedCounter', fn () => $telemetry->createCounter('realtime.server.connection.created'));
|
||||
$register->set('telemetry.messageSentCounter', fn () => $telemetry->createCounter('realtime.server.message.sent'));
|
||||
|
||||
$attempts = 0;
|
||||
$start = time();
|
||||
|
||||
@@ -416,6 +430,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
);
|
||||
|
||||
if (($num = count($receivers)) > 0) {
|
||||
$register->get('telemetry.messageSentCounter')->add($num);
|
||||
$stats->incr($event['project'], 'messages', $num);
|
||||
}
|
||||
});
|
||||
@@ -519,6 +534,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
]
|
||||
]));
|
||||
|
||||
$register->get('telemetry.connectionCounter')->add(1);
|
||||
$register->get('telemetry.connectionCreatedCounter')->add(1);
|
||||
|
||||
$stats->set($project->getId(), [
|
||||
'projectId' => $project->getId(),
|
||||
'teamId' => $project->getAttribute('teamId')
|
||||
@@ -592,9 +610,12 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
}
|
||||
|
||||
switch ($message['type']) {
|
||||
/**
|
||||
* This type is used to authenticate.
|
||||
*/
|
||||
case 'ping':
|
||||
$server->send([$connection], json_encode([
|
||||
'type' => 'pong'
|
||||
]));
|
||||
|
||||
break;
|
||||
case 'authentication':
|
||||
if (!array_key_exists('session', $message['data'])) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
|
||||
@@ -652,12 +673,14 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
}
|
||||
});
|
||||
|
||||
$server->onClose(function (int $connection) use ($realtime, $stats) {
|
||||
$server->onClose(function (int $connection) use ($realtime, $stats, $register) {
|
||||
if (array_key_exists($connection, $realtime->connections)) {
|
||||
$stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal');
|
||||
}
|
||||
$realtime->unsubscribe($connection);
|
||||
|
||||
$register->get('telemetry.connectionCounter')->add(-1);
|
||||
|
||||
Console::info('Connection close: ' . $connection);
|
||||
});
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ $image = $this->getParam('image', '');
|
||||
- _APP_OPTIONS_ABUSE
|
||||
- _APP_OPTIONS_ROUTER_PROTECTION
|
||||
- _APP_OPTIONS_FORCE_HTTPS
|
||||
- _APP_OPTIONS_FUNCTIONS_FORCE_HTTPS
|
||||
- _APP_OPTIONS_COMPUTE_FORCE_HTTPS
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_DOMAIN
|
||||
- _APP_DOMAIN_TARGET
|
||||
@@ -132,12 +132,14 @@ $image = $this->getParam('image', '');
|
||||
- _APP_STORAGE_WASABI_SECRET
|
||||
- _APP_STORAGE_WASABI_REGION
|
||||
- _APP_STORAGE_WASABI_BUCKET
|
||||
- _APP_FUNCTIONS_SIZE_LIMIT
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_FUNCTIONS_BUILD_TIMEOUT
|
||||
- _APP_FUNCTIONS_CPUS
|
||||
- _APP_FUNCTIONS_MEMORY
|
||||
- _APP_COMPUTE_SIZE_LIMIT
|
||||
- _APP_COMPUTE_TIMEOUT
|
||||
- _APP_COMPUTE_BUILD_TIMEOUT
|
||||
- _APP_COMPUTE_CPUS
|
||||
- _APP_COMPUTE_MEMORY
|
||||
- _APP_FUNCTIONS_RUNTIMES
|
||||
- _APP_SITES_FRAMEWORKS
|
||||
- _APP_SITES_RUNTIMES
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
- _APP_LOGGING_CONFIG
|
||||
@@ -400,13 +402,13 @@ $image = $this->getParam('image', '');
|
||||
- _APP_VCS_GITHUB_APP_NAME
|
||||
- _APP_VCS_GITHUB_PRIVATE_KEY
|
||||
- _APP_VCS_GITHUB_APP_ID
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_FUNCTIONS_BUILD_TIMEOUT
|
||||
- _APP_FUNCTIONS_CPUS
|
||||
- _APP_FUNCTIONS_MEMORY
|
||||
- _APP_FUNCTIONS_SIZE_LIMIT
|
||||
- _APP_COMPUTE_TIMEOUT
|
||||
- _APP_COMPUTE_BUILD_TIMEOUT
|
||||
- _APP_COMPUTE_CPUS
|
||||
- _APP_COMPUTE_MEMORY
|
||||
- _APP_COMPUTE_SIZE_LIMIT
|
||||
- _APP_OPTIONS_FORCE_HTTPS
|
||||
- _APP_OPTIONS_FUNCTIONS_FORCE_HTTPS
|
||||
- _APP_OPTIONS_COMPUTE_FORCE_HTTPS
|
||||
- _APP_DOMAIN
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
@@ -490,10 +492,10 @@ $image = $this->getParam('image', '');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_FUNCTIONS_BUILD_TIMEOUT
|
||||
- _APP_FUNCTIONS_CPUS
|
||||
- _APP_FUNCTIONS_MEMORY
|
||||
- _APP_COMPUTE_TIMEOUT
|
||||
- _APP_COMPUTE_BUILD_TIMEOUT
|
||||
- _APP_COMPUTE_CPUS
|
||||
- _APP_COMPUTE_MEMORY
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
- _APP_USAGE_STATS
|
||||
@@ -796,7 +798,7 @@ $image = $this->getParam('image', '');
|
||||
<<: *x-logging
|
||||
restart: unless-stopped
|
||||
stop_signal: SIGINT
|
||||
image: openruntimes/executor:0.6.11
|
||||
image: openruntimes/executor:0.7.0
|
||||
networks:
|
||||
- appwrite
|
||||
- runtimes
|
||||
@@ -808,9 +810,9 @@ $image = $this->getParam('image', '');
|
||||
# It's not possible to share mount file between 2 containers without host mount (copying is too slow)
|
||||
- /tmp:/tmp:rw
|
||||
environment:
|
||||
- OPR_EXECUTOR_INACTIVE_TRESHOLD=$_APP_FUNCTIONS_INACTIVE_THRESHOLD
|
||||
- OPR_EXECUTOR_MAINTENANCE_INTERVAL=$_APP_FUNCTIONS_MAINTENANCE_INTERVAL
|
||||
- OPR_EXECUTOR_NETWORK=$_APP_FUNCTIONS_RUNTIMES_NETWORK
|
||||
- OPR_EXECUTOR_INACTIVE_TRESHOLD=$_APP_COMPUTE_INACTIVE_THRESHOLD
|
||||
- OPR_EXECUTOR_MAINTENANCE_INTERVAL=$_APP_COMPUTE_MAINTENANCE_INTERVAL
|
||||
- OPR_EXECUTOR_NETWORK=$_APP_COMPUTE_RUNTIMES_NETWORK
|
||||
- OPR_EXECUTOR_DOCKER_HUB_USERNAME=$_APP_DOCKER_HUB_USERNAME
|
||||
- OPR_EXECUTOR_DOCKER_HUB_PASSWORD=$_APP_DOCKER_HUB_PASSWORD
|
||||
- OPR_EXECUTOR_ENV=$_APP_ENV
|
||||
|
||||
+53
-1
@@ -9,6 +9,7 @@ use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Insight;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
use Appwrite\Event\Migration;
|
||||
@@ -16,7 +17,6 @@ use Appwrite\Event\Usage;
|
||||
use Appwrite\Event\UsageDump;
|
||||
use Appwrite\Platform\Appwrite;
|
||||
use Swoole\Runtime;
|
||||
use Utopia\App;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\CLI\Console;
|
||||
@@ -244,6 +244,10 @@ Server::setResource('queueForCertificates', function (Connection $queue) {
|
||||
return new Certificate($queue);
|
||||
}, ['queue']);
|
||||
|
||||
Server::setResource('queueForInsights', function (Connection $queue) {
|
||||
return new Insight($queue);
|
||||
}, ['queue']);
|
||||
|
||||
Server::setResource('queueForMigrations', function (Connection $queue) {
|
||||
return new Migration($queue);
|
||||
}, ['queue']);
|
||||
@@ -256,6 +260,10 @@ Server::setResource('pools', function (Registry $register) {
|
||||
return $register->get('pools');
|
||||
}, ['register']);
|
||||
|
||||
Server::setResource('deviceForSites', function (Document $project) {
|
||||
return getDevice(APP_STORAGE_SITES . '/app-' . $project->getId());
|
||||
}, ['project']);
|
||||
|
||||
Server::setResource('deviceForFunctions', function (Document $project) {
|
||||
return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId());
|
||||
}, ['project']);
|
||||
@@ -277,6 +285,50 @@ Server::setResource(
|
||||
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
|
||||
);
|
||||
|
||||
Server::setResource('logError', function (Registry $register, Document $project) {
|
||||
return function (Throwable $error, string $namespace, string $action, ?array $extras) use ($register, $project) {
|
||||
$logger = $register->get('logger');
|
||||
|
||||
if ($logger) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
$log = new Log();
|
||||
$log->setNamespace($namespace);
|
||||
$log->setServer(\gethostname());
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('projectId', $project->getId() ?? '');
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
|
||||
|
||||
foreach ($extras as $key => $value) {
|
||||
$log->addExtra($key, $value);
|
||||
}
|
||||
|
||||
$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::warning("Failed: {$error->getMessage()}");
|
||||
Console::warning($error->getTraceAsString());
|
||||
};
|
||||
}, ['register', 'project']);
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$platform = new Appwrite();
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/worker.php insights $@
|
||||
@@ -70,6 +70,7 @@
|
||||
"utopia-php/storage": "0.18.*",
|
||||
"utopia-php/swoole": "0.8.*",
|
||||
"utopia-php/system": "0.9.*",
|
||||
"utopia-php/telemetry": "0.1.*",
|
||||
"utopia-php/vcs": "0.8.*",
|
||||
"utopia-php/websocket": "0.1.*",
|
||||
"matomo/device-detector": "6.1.*",
|
||||
@@ -96,6 +97,10 @@
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.3"
|
||||
},
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true,
|
||||
"tbachert/spi": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1869
-364
File diff suppressed because it is too large
Load Diff
+94
-15
@@ -76,6 +76,7 @@ services:
|
||||
- appwrite-config:/storage/config:rw
|
||||
- appwrite-certificates:/storage/certificates:rw
|
||||
- appwrite-functions:/storage/functions:rw
|
||||
- appwrite-builds:/storage/builds:rw
|
||||
- ./phpunit.xml:/usr/src/code/phpunit.xml
|
||||
- ./tests:/usr/src/code/tests
|
||||
- ./app:/usr/src/code/app
|
||||
@@ -110,7 +111,7 @@ services:
|
||||
- _APP_OPTIONS_ABUSE
|
||||
- _APP_OPTIONS_ROUTER_PROTECTION
|
||||
- _APP_OPTIONS_FORCE_HTTPS
|
||||
- _APP_OPTIONS_FUNCTIONS_FORCE_HTTPS
|
||||
- _APP_OPTIONS_COMPUTE_FORCE_HTTPS
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_DOMAIN
|
||||
- _APP_DOMAIN_TARGET
|
||||
@@ -156,12 +157,15 @@ services:
|
||||
- _APP_STORAGE_WASABI_SECRET
|
||||
- _APP_STORAGE_WASABI_REGION
|
||||
- _APP_STORAGE_WASABI_BUCKET
|
||||
- _APP_FUNCTIONS_SIZE_LIMIT
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_FUNCTIONS_BUILD_TIMEOUT
|
||||
- _APP_FUNCTIONS_CPUS
|
||||
- _APP_FUNCTIONS_MEMORY
|
||||
- _APP_COMPUTE_SIZE_LIMIT
|
||||
- _APP_COMPUTE_TIMEOUT
|
||||
- _APP_COMPUTE_BUILD_TIMEOUT
|
||||
- _APP_COMPUTE_CPUS
|
||||
- _APP_COMPUTE_MEMORY
|
||||
- _APP_FUNCTIONS_RUNTIMES
|
||||
- _APP_SITES_RUNTIMES
|
||||
- _APP_SITES_FRAMEWORKS
|
||||
- _APP_DOMAIN_SITES
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
- _APP_LOGGING_CONFIG
|
||||
@@ -419,6 +423,72 @@ services:
|
||||
<<: *x-logging
|
||||
container_name: appwrite-worker-builds
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-functions:/storage/functions:rw
|
||||
- appwrite-builds:/storage/builds:rw
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_VCS_GITHUB_APP_NAME
|
||||
- _APP_VCS_GITHUB_PRIVATE_KEY
|
||||
- _APP_VCS_GITHUB_APP_ID
|
||||
- _APP_COMPUTE_TIMEOUT
|
||||
- _APP_COMPUTE_BUILD_TIMEOUT
|
||||
- _APP_COMPUTE_CPUS
|
||||
- _APP_COMPUTE_MEMORY
|
||||
- _APP_COMPUTE_SIZE_LIMIT
|
||||
- _APP_OPTIONS_FORCE_HTTPS
|
||||
- _APP_OPTIONS_COMPUTE_FORCE_HTTPS
|
||||
- _APP_DOMAIN
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
- _APP_STORAGE_S3_REGION
|
||||
- _APP_STORAGE_S3_BUCKET
|
||||
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
|
||||
- _APP_STORAGE_DO_SPACES_SECRET
|
||||
- _APP_STORAGE_DO_SPACES_REGION
|
||||
- _APP_STORAGE_DO_SPACES_BUCKET
|
||||
- _APP_STORAGE_BACKBLAZE_ACCESS_KEY
|
||||
- _APP_STORAGE_BACKBLAZE_SECRET
|
||||
- _APP_STORAGE_BACKBLAZE_REGION
|
||||
- _APP_STORAGE_BACKBLAZE_BUCKET
|
||||
- _APP_STORAGE_LINODE_ACCESS_KEY
|
||||
- _APP_STORAGE_LINODE_SECRET
|
||||
- _APP_STORAGE_LINODE_REGION
|
||||
- _APP_STORAGE_LINODE_BUCKET
|
||||
- _APP_STORAGE_WASABI_ACCESS_KEY
|
||||
- _APP_STORAGE_WASABI_SECRET
|
||||
- _APP_STORAGE_WASABI_REGION
|
||||
- _APP_STORAGE_WASABI_BUCKET
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
- _APP_DOMAIN_SITES
|
||||
|
||||
appwrite-worker-insights:
|
||||
entrypoint: worker-insights
|
||||
<<: *x-logging
|
||||
container_name: appwrite-worker-insights
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
@@ -543,10 +613,10 @@ services:
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_FUNCTIONS_BUILD_TIMEOUT
|
||||
- _APP_FUNCTIONS_CPUS
|
||||
- _APP_FUNCTIONS_MEMORY
|
||||
- _APP_COMPUTE_TIMEOUT
|
||||
- _APP_COMPUTE_BUILD_TIMEOUT
|
||||
- _APP_COMPUTE_CPUS
|
||||
- _APP_COMPUTE_MEMORY
|
||||
- _APP_EXECUTOR_SECRET
|
||||
- _APP_EXECUTOR_HOST
|
||||
- _APP_USAGE_STATS
|
||||
@@ -870,12 +940,21 @@ services:
|
||||
environment:
|
||||
- _APP_ASSISTANT_OPENAI_API_KEY
|
||||
|
||||
appwrite-browser:
|
||||
container_name: appwrite-browser
|
||||
image: appwrite/browser:0.1.0
|
||||
networks:
|
||||
- appwrite
|
||||
environment:
|
||||
- APPWRITE_BROWSER_SECRET=123 # TODO: Change this to a random secret
|
||||
- PORT=1234 # TODO: Use a different port
|
||||
|
||||
openruntimes-executor:
|
||||
container_name: openruntimes-executor
|
||||
hostname: exc1
|
||||
<<: *x-logging
|
||||
stop_signal: SIGINT
|
||||
image: openruntimes/executor:0.6.11
|
||||
image: openruntimes/executor:0.7.1
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
@@ -888,13 +967,13 @@ services:
|
||||
# It's not possible to share mount file between 2 containers without host mount (copying is too slow)
|
||||
- /tmp:/tmp:rw
|
||||
environment:
|
||||
- OPR_EXECUTOR_INACTIVE_TRESHOLD=$_APP_FUNCTIONS_INACTIVE_THRESHOLD
|
||||
- OPR_EXECUTOR_MAINTENANCE_INTERVAL=$_APP_FUNCTIONS_MAINTENANCE_INTERVAL
|
||||
- OPR_EXECUTOR_NETWORK=$_APP_FUNCTIONS_RUNTIMES_NETWORK
|
||||
- OPR_EXECUTOR_INACTIVE_TRESHOLD=$_APP_COMPUTE_INACTIVE_THRESHOLD
|
||||
- OPR_EXECUTOR_MAINTENANCE_INTERVAL=$_APP_COMPUTE_MAINTENANCE_INTERVAL
|
||||
- OPR_EXECUTOR_NETWORK=$_APP_COMPUTE_RUNTIMES_NETWORK
|
||||
- OPR_EXECUTOR_DOCKER_HUB_USERNAME=$_APP_DOCKER_HUB_USERNAME
|
||||
- OPR_EXECUTOR_DOCKER_HUB_PASSWORD=$_APP_DOCKER_HUB_PASSWORD
|
||||
- OPR_EXECUTOR_ENV=$_APP_ENV
|
||||
- OPR_EXECUTOR_RUNTIMES=$_APP_FUNCTIONS_RUNTIMES
|
||||
- OPR_EXECUTOR_RUNTIMES=$_APP_FUNCTIONS_RUNTIMES,$_APP_SITES_RUNTIMES
|
||||
- OPR_EXECUTOR_SECRET=$_APP_EXECUTOR_SECRET
|
||||
- OPR_EXECUTOR_RUNTIME_VERSIONS=v2,v4
|
||||
- OPR_EXECUTOR_LOGGING_CONFIG=$_APP_LOGGING_CONFIG
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST /v1/account/sessions/token](https://appwrite.io/docs/references/cloud/client-web/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.
|
||||
Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST /v1/account/sessions/token](https://appwrite.io/docs/references/cloud/client-web/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.
|
||||
|
||||
A user is limited to 10 active sessions at a time by default. [Learn more about session limits](https://appwrite.io/docs/authentication-security#limits).
|
||||
|
||||
@@ -24,6 +24,9 @@ class Event
|
||||
public const FUNCTIONS_QUEUE_NAME = 'v1-functions';
|
||||
public const FUNCTIONS_CLASS_NAME = 'FunctionsV1';
|
||||
|
||||
public const SITES_QUEUE_NAME = 'v1-sites';
|
||||
public const SITES_CLASS_NAME = 'SitesV1';
|
||||
|
||||
public const USAGE_QUEUE_NAME = 'v1-usage';
|
||||
public const USAGE_CLASS_NAME = 'UsageV1';
|
||||
|
||||
@@ -45,6 +48,9 @@ class Event
|
||||
public const MIGRATIONS_QUEUE_NAME = 'v1-migrations';
|
||||
public const MIGRATIONS_CLASS_NAME = 'MigrationsV1';
|
||||
|
||||
public const INSIGHTS_QUEUE_NAME = 'v1-insights';
|
||||
public const INSIGHTS_CLASS_NAME = 'InsightsV1';
|
||||
|
||||
protected string $queue = '';
|
||||
protected string $class = '';
|
||||
protected string $event = '';
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event;
|
||||
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Queue\Client;
|
||||
use Utopia\Queue\Connection;
|
||||
|
||||
class Insight extends Event
|
||||
{
|
||||
protected string $type = '';
|
||||
protected ?Document $resource = null;
|
||||
protected ?Document $deployment = null;
|
||||
protected ?Document $build = null;
|
||||
|
||||
public function __construct(protected Connection $connection)
|
||||
{
|
||||
parent::__construct($connection);
|
||||
|
||||
$this
|
||||
->setQueue(Event::INSIGHTS_QUEUE_NAME)
|
||||
->setClass(Event::INSIGHTS_CLASS_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets build for the insights event.
|
||||
*
|
||||
* @param Document $build
|
||||
* @return self
|
||||
*/
|
||||
public function setBuild(Document $build): self
|
||||
{
|
||||
$this->build = $build;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets resource document for the insights event.
|
||||
*
|
||||
* @param Document $resource
|
||||
* @return self
|
||||
*/
|
||||
public function setResource(Document $resource): self
|
||||
{
|
||||
$this->resource = $resource;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns set resource document for the insights event.
|
||||
*
|
||||
* @return null|Document
|
||||
*/
|
||||
public function getResource(): ?Document
|
||||
{
|
||||
return $this->resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets deployment for the insights event.
|
||||
*
|
||||
* @param Document $deployment
|
||||
* @return self
|
||||
*/
|
||||
public function setDeployment(Document $deployment): self
|
||||
{
|
||||
$this->deployment = $deployment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns set deployment for the build event.
|
||||
*
|
||||
* @return null|Document
|
||||
*/
|
||||
public function getDeployment(): ?Document
|
||||
{
|
||||
return $this->deployment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets type for the build event.
|
||||
*
|
||||
* @param string $type Can be `BUILD_TYPE_DEPLOYMENT` or `BUILD_TYPE_RETRY`.
|
||||
* @return self
|
||||
*/
|
||||
public function setType(string $type): self
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns set type for the function event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the function event and sends it to the functions worker.
|
||||
*
|
||||
* @return string|bool
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function trigger(): string|bool
|
||||
{
|
||||
$client = new Client($this->queue, $this->connection);
|
||||
|
||||
return $client->enqueue([
|
||||
'project' => $this->project,
|
||||
'resource' => $this->resource,
|
||||
'deployment' => $this->deployment,
|
||||
'type' => $this->type,
|
||||
'build' => $this->build
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets event.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function reset(): self
|
||||
{
|
||||
$this->type = '';
|
||||
$this->resource = null;
|
||||
$this->deployment = null;
|
||||
$this->build = null;
|
||||
parent::reset();
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -152,6 +152,11 @@ class Exception extends \Exception
|
||||
public const PROVIDER_CONTRIBUTION_CONFLICT = 'provider_contribution_conflict';
|
||||
public const GENERAL_PROVIDER_FAILURE = 'general_provider_failure';
|
||||
|
||||
/** Sites */
|
||||
public const SITE_NOT_FOUND = 'site_not_found';
|
||||
public const SITE_FRAMEWORK_UNSUPPORTED = 'site_framework_unsupported';
|
||||
public const SITE_TEMPLATE_NOT_FOUND = 'site_template_not_found';
|
||||
|
||||
/** Functions */
|
||||
public const FUNCTION_NOT_FOUND = 'function_not_found';
|
||||
public const FUNCTION_RUNTIME_UNSUPPORTED = 'function_runtime_unsupported';
|
||||
|
||||
@@ -349,6 +349,16 @@ class Realtime extends Adapter
|
||||
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'sites':
|
||||
if ($parts[2] === 'deployments') {
|
||||
$channels[] = 'console';
|
||||
$channels[] = 'projects.' . $project->getId();
|
||||
$projectId = 'console';
|
||||
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
|
||||
}
|
||||
|
||||
break;
|
||||
case 'migrations':
|
||||
$channels[] = 'console';
|
||||
|
||||
@@ -186,7 +186,7 @@ class V21 extends Migration
|
||||
$document->setAttribute('scopes', []);
|
||||
|
||||
// Add size attribute
|
||||
$document->setAttribute('specification', APP_FUNCTION_SPECIFICATION_DEFAULT);
|
||||
$document->setAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT);
|
||||
}
|
||||
|
||||
return $document;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace Appwrite\Platform;
|
||||
|
||||
use Appwrite\Platform\Modules\Core;
|
||||
use Appwrite\Platform\Modules\Functions;
|
||||
use Appwrite\Platform\Modules\Sites;
|
||||
use Utopia\Platform\Platform;
|
||||
|
||||
class Appwrite extends Platform
|
||||
@@ -10,5 +12,7 @@ class Appwrite extends Platform
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(new Core());
|
||||
$this->addModule(new Functions\Module());
|
||||
$this->addModule(new Sites\Module());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Compute;
|
||||
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Swoole\Request;
|
||||
use Utopia\System\System;
|
||||
use Utopia\VCS\Adapter\Git\GitHub;
|
||||
use Utopia\VCS\Exception\RepositoryNotFound;
|
||||
|
||||
class Base extends Action
|
||||
{
|
||||
public function redeployVcsFunction(Request $request, Document $function, Document $project, Document $installation, Database $dbForProject, Build $queueForBuilds, Document $template, GitHub $github)
|
||||
{
|
||||
$deploymentId = ID::unique();
|
||||
$entrypoint = $function->getAttribute('entrypoint', '');
|
||||
$providerInstallationId = $installation->getAttribute('providerInstallationId', '');
|
||||
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
|
||||
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
|
||||
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
|
||||
$owner = $github->getOwnerName($providerInstallationId);
|
||||
$providerRepositoryId = $function->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);
|
||||
}
|
||||
$providerBranch = $function->getAttribute('providerBranch', 'main');
|
||||
$authorUrl = "https://github.com/$owner";
|
||||
$repositoryUrl = "https://github.com/$owner/$repositoryName";
|
||||
$branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch";
|
||||
|
||||
$commitDetails = [];
|
||||
if ($template->isEmpty()) {
|
||||
try {
|
||||
$commitDetails = $github->getLatestCommit($owner, $repositoryName, $providerBranch);
|
||||
} catch (\Throwable $error) {
|
||||
Console::warning('Failed to get latest commit details');
|
||||
Console::warning($error->getMessage());
|
||||
Console::warning($error->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
$deployment = $dbForProject->createDocument('deployments', new Document([
|
||||
'$id' => $deploymentId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceId' => $function->getId(),
|
||||
'resourceInternalId' => $function->getInternalId(),
|
||||
'resourceType' => 'functions',
|
||||
'entrypoint' => $entrypoint,
|
||||
'commands' => $function->getAttribute('commands', ''),
|
||||
'type' => 'vcs',
|
||||
'installationId' => $installation->getId(),
|
||||
'installationInternalId' => $installation->getInternalId(),
|
||||
'providerRepositoryId' => $providerRepositoryId,
|
||||
'repositoryId' => $function->getAttribute('repositoryId', ''),
|
||||
'repositoryInternalId' => $function->getAttribute('repositoryInternalId', ''),
|
||||
'providerBranchUrl' => $branchUrl,
|
||||
'providerRepositoryName' => $repositoryName,
|
||||
'providerRepositoryOwner' => $owner,
|
||||
'providerRepositoryUrl' => $repositoryUrl,
|
||||
'providerCommitHash' => $commitDetails['commitHash'] ?? '',
|
||||
'providerCommitAuthorUrl' => $authorUrl,
|
||||
'providerCommitAuthor' => $commitDetails['commitAuthor'] ?? '',
|
||||
'providerCommitMessage' => $commitDetails['commitMessage'] ?? '',
|
||||
'providerCommitUrl' => $commitDetails['commitUrl'] ?? '',
|
||||
'providerBranch' => $providerBranch,
|
||||
'providerRootDirectory' => $function->getAttribute('providerRootDirectory', ''),
|
||||
'search' => implode(' ', [$deploymentId, $entrypoint]),
|
||||
'activate' => true,
|
||||
]));
|
||||
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
->setResource($function)
|
||||
->setDeployment($deployment)
|
||||
->setTemplate($template);
|
||||
}
|
||||
|
||||
public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Database $dbForConsole, Build $queueForBuilds, Document $template, GitHub $github)
|
||||
{
|
||||
$deploymentId = ID::unique();
|
||||
$providerInstallationId = $installation->getAttribute('providerInstallationId', '');
|
||||
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
|
||||
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
|
||||
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
|
||||
$owner = $github->getOwnerName($providerInstallationId);
|
||||
$providerRepositoryId = $site->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);
|
||||
}
|
||||
|
||||
$providerBranch = $site->getAttribute('providerBranch', 'main');
|
||||
$authorUrl = "https://github.com/$owner";
|
||||
$repositoryUrl = "https://github.com/$owner/$repositoryName";
|
||||
$branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch";
|
||||
|
||||
$commitDetails = [];
|
||||
if ($template->isEmpty()) {
|
||||
try {
|
||||
$commitDetails = $github->getLatestCommit($owner, $repositoryName, $providerBranch);
|
||||
} catch (\Throwable $error) {
|
||||
Console::warning('Failed to get latest commit details');
|
||||
Console::warning($error->getMessage());
|
||||
Console::warning($error->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
$deployment = $dbForProject->createDocument('deployments', new Document([
|
||||
'$id' => $deploymentId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceId' => $site->getId(),
|
||||
'resourceInternalId' => $site->getInternalId(),
|
||||
'resourceType' => 'sites',
|
||||
'buildCommand' => $site->getAttribute('buildCommand', ''),
|
||||
'installCommand' => $site->getAttribute('installCommand', ''),
|
||||
'outputDirectory' => $site->getAttribute('outputDirectory', ''),
|
||||
'type' => 'vcs',
|
||||
'installationId' => $installation->getId(),
|
||||
'installationInternalId' => $installation->getInternalId(),
|
||||
'providerRepositoryId' => $providerRepositoryId,
|
||||
'repositoryId' => $site->getAttribute('repositoryId', ''),
|
||||
'repositoryInternalId' => $site->getAttribute('repositoryInternalId', ''),
|
||||
'providerBranchUrl' => $branchUrl,
|
||||
'providerRepositoryName' => $repositoryName,
|
||||
'providerRepositoryOwner' => $owner,
|
||||
'providerRepositoryUrl' => $repositoryUrl,
|
||||
'providerCommitHash' => $commitDetails['commitHash'] ?? '',
|
||||
'providerCommitAuthorUrl' => $authorUrl,
|
||||
'providerCommitAuthor' => $commitDetails['commitAuthor'] ?? '',
|
||||
'providerCommitMessage' => $commitDetails['commitMessage'] ?? '',
|
||||
'providerCommitUrl' => $commitDetails['commitUrl'] ?? '',
|
||||
'providerBranch' => $providerBranch,
|
||||
'providerRootDirectory' => $site->getAttribute('providerRootDirectory', ''),
|
||||
'search' => implode(' ', [$deploymentId]),
|
||||
'activate' => true,
|
||||
]));
|
||||
|
||||
// Preview deployments for sites
|
||||
$projectId = $project->getId();
|
||||
|
||||
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
|
||||
$domain = "{$deploymentId}-{$projectId}.{$sitesDomain}";
|
||||
$ruleId = md5($domain);
|
||||
|
||||
$rule = Authorization::skip(
|
||||
fn () => $dbForConsole->createDocument('rules', new Document([
|
||||
'$id' => $ruleId,
|
||||
'projectId' => $project->getId(),
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'domain' => $domain,
|
||||
'resourceType' => 'deployment',
|
||||
'resourceId' => $deploymentId,
|
||||
'resourceInternalId' => $deployment->getInternalId(),
|
||||
'status' => 'verified',
|
||||
'certificateId' => '',
|
||||
]))
|
||||
);
|
||||
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
->setResource($site)
|
||||
->setDeployment($deployment)
|
||||
->setTemplate($template);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Functions\Http\Deployments;
|
||||
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Storage\Validator\File;
|
||||
use Utopia\Storage\Validator\FileExt;
|
||||
use Utopia\Storage\Validator\FileSize;
|
||||
use Utopia\Storage\Validator\Upload;
|
||||
use Utopia\Swoole\Request;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class CreateDeployment extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'createDeployment';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/functions/:functionId/deployments')
|
||||
->desc('Create deployment')
|
||||
->groups(['api', 'functions'])
|
||||
->label('scope', 'functions.write')
|
||||
->label('event', 'functions.[functionId].deployments.[deploymentId].create')
|
||||
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
|
||||
->label('audits.event', 'deployment.create')
|
||||
->label('audits.resource', 'function/{request.functionId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'functions')
|
||||
->label('sdk.method', 'createDeployment')
|
||||
->label('sdk.methodType', 'upload')
|
||||
->label('sdk.description', '/docs/references/functions/create-deployment.md')
|
||||
->label('sdk.packaging', true)
|
||||
->label('sdk.request.type', 'multipart/form-data')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_DEPLOYMENT)
|
||||
->param('functionId', '', new UID(), 'Function ID.')
|
||||
->param('entrypoint', null, new Text(1028), 'Entrypoint File.', true)
|
||||
->param('commands', null, new Text(8192, 0), 'Build Commands.', true)
|
||||
->param('code', [], new File(), 'Gzip file with your code package. When used with the Appwrite CLI, pass the path to your code directory, and the CLI will automatically package your code. Use a path that is within the current directory.', skipValidation: true)
|
||||
->param('activate', false, new Boolean(true), 'Automatically activate the deployment when it is finished building.')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('project')
|
||||
->inject('deviceForFunctions')
|
||||
->inject('deviceForLocal')
|
||||
->inject('queueForBuilds')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $functionId, ?string $entrypoint, ?string $commands, mixed $code, mixed $activate, Request $request, Response $response, Database $dbForProject, Event $queueForEvents, Document $project, Device $deviceForFunctions, Device $deviceForLocal, Build $queueForBuilds)
|
||||
{
|
||||
$activate = \strval($activate) === 'true' || \strval($activate) === '1';
|
||||
|
||||
$function = $dbForProject->getDocument('functions', $functionId);
|
||||
|
||||
if ($function->isEmpty()) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($entrypoint === null) {
|
||||
$entrypoint = $function->getAttribute('entrypoint', '');
|
||||
}
|
||||
|
||||
if ($commands === null) {
|
||||
$commands = $function->getAttribute('commands', '');
|
||||
}
|
||||
|
||||
if (empty($entrypoint)) {
|
||||
throw new Exception(Exception::FUNCTION_ENTRYPOINT_MISSING);
|
||||
}
|
||||
|
||||
$file = $request->getFiles('code');
|
||||
|
||||
// GraphQL multipart spec adds files with index keys
|
||||
if (empty($file)) {
|
||||
$file = $request->getFiles(0);
|
||||
}
|
||||
|
||||
if (empty($file)) {
|
||||
throw new Exception(Exception::STORAGE_FILE_EMPTY, 'No file sent');
|
||||
}
|
||||
|
||||
$fileExt = new FileExt([FileExt::TYPE_GZIP]);
|
||||
$fileSizeValidator = new FileSize(System::getEnv('_APP_COMPUTE_SIZE_LIMIT', '30000000'));
|
||||
$upload = new Upload();
|
||||
|
||||
// Make sure we handle a single file and multiple files the same way
|
||||
$fileName = (\is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name'];
|
||||
$fileTmpName = (\is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name'];
|
||||
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
|
||||
|
||||
if (!$fileExt->isValid($file['name'])) { // Check if file type is allowed
|
||||
throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED);
|
||||
}
|
||||
|
||||
$contentRange = $request->getHeader('content-range');
|
||||
$deploymentId = ID::unique();
|
||||
$chunk = 1;
|
||||
$chunks = 1;
|
||||
|
||||
if (!empty($contentRange)) {
|
||||
$start = $request->getContentRangeStart();
|
||||
$end = $request->getContentRangeEnd();
|
||||
$fileSize = $request->getContentRangeSize();
|
||||
$deploymentId = $request->getHeader('x-appwrite-id', $deploymentId);
|
||||
// TODO make `end >= $fileSize` in next breaking version
|
||||
if (is_null($start) || is_null($end) || is_null($fileSize) || $end > $fileSize) {
|
||||
throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE);
|
||||
}
|
||||
|
||||
// TODO remove the condition that checks `$end === $fileSize` in next breaking version
|
||||
if ($end === $fileSize - 1 || $end === $fileSize) {
|
||||
//if it's a last chunks the chunk size might differ, so we set the $chunks and $chunk to notify it's last chunk
|
||||
$chunks = $chunk = -1;
|
||||
} else {
|
||||
// Calculate total number of chunks based on the chunk size i.e ($rangeEnd - $rangeStart)
|
||||
$chunks = (int) ceil($fileSize / ($end + 1 - $start));
|
||||
$chunk = (int) ($start / ($end + 1 - $start)) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$fileSizeValidator->isValid($fileSize)) { // Check if file size is exceeding allowed limit
|
||||
throw new Exception(Exception::STORAGE_INVALID_FILE_SIZE);
|
||||
}
|
||||
|
||||
if (!$upload->isValid($fileTmpName)) {
|
||||
throw new Exception(Exception::STORAGE_INVALID_FILE);
|
||||
}
|
||||
|
||||
// Save to storage
|
||||
$fileSize ??= $deviceForLocal->getFileSize($fileTmpName);
|
||||
$path = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION));
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
|
||||
$metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)];
|
||||
if (!$deployment->isEmpty()) {
|
||||
$chunks = $deployment->getAttribute('chunksTotal', 1);
|
||||
$metadata = $deployment->getAttribute('metadata', []);
|
||||
if ($chunk === -1) {
|
||||
$chunk = $chunks;
|
||||
}
|
||||
}
|
||||
|
||||
$chunksUploaded = $deviceForFunctions->upload($fileTmpName, $path, $chunk, $chunks, $metadata);
|
||||
|
||||
if (empty($chunksUploaded)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed moving file');
|
||||
}
|
||||
|
||||
$type = $request->getHeader('x-sdk-language') === 'cli' ? 'cli' : 'manual';
|
||||
|
||||
if ($chunksUploaded === $chunks) {
|
||||
if ($activate) {
|
||||
// Remove deploy for all other deployments.
|
||||
$activeDeployments = $dbForProject->find('deployments', [
|
||||
Query::equal('activate', [true]),
|
||||
Query::equal('resourceId', [$functionId]),
|
||||
Query::equal('resourceType', ['functions'])
|
||||
]);
|
||||
|
||||
foreach ($activeDeployments as $activeDeployment) {
|
||||
$activeDeployment->setAttribute('activate', false);
|
||||
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), $activeDeployment);
|
||||
}
|
||||
}
|
||||
|
||||
$fileSize = $deviceForFunctions->getFileSize($path);
|
||||
|
||||
if ($deployment->isEmpty()) {
|
||||
$deployment = $dbForProject->createDocument('deployments', new Document([
|
||||
'$id' => $deploymentId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceInternalId' => $function->getInternalId(),
|
||||
'resourceId' => $function->getId(),
|
||||
'resourceType' => 'functions',
|
||||
'buildInternalId' => '',
|
||||
'entrypoint' => $entrypoint,
|
||||
'commands' => $commands,
|
||||
'path' => $path,
|
||||
'size' => $fileSize,
|
||||
'search' => implode(' ', [$deploymentId, $entrypoint]),
|
||||
'activate' => $activate,
|
||||
'metadata' => $metadata,
|
||||
'type' => $type
|
||||
]));
|
||||
} else {
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('size', $fileSize)->setAttribute('metadata', $metadata));
|
||||
}
|
||||
|
||||
// Start the build
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
->setResource($function)
|
||||
->setDeployment($deployment);
|
||||
} else {
|
||||
if ($deployment->isEmpty()) {
|
||||
$deployment = $dbForProject->createDocument('deployments', new Document([
|
||||
'$id' => $deploymentId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceInternalId' => $function->getInternalId(),
|
||||
'resourceId' => $function->getId(),
|
||||
'resourceType' => 'functions',
|
||||
'buildInternalId' => '',
|
||||
'entrypoint' => $entrypoint,
|
||||
'commands' => $commands,
|
||||
'path' => $path,
|
||||
'size' => $fileSize,
|
||||
'chunksTotal' => $chunks,
|
||||
'chunksUploaded' => $chunksUploaded,
|
||||
'search' => implode(' ', [$deploymentId, $entrypoint]),
|
||||
'activate' => $activate,
|
||||
'metadata' => $metadata,
|
||||
'type' => $type
|
||||
]));
|
||||
} else {
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('chunksUploaded', $chunksUploaded)->setAttribute('metadata', $metadata));
|
||||
}
|
||||
}
|
||||
|
||||
$metadata = null;
|
||||
|
||||
$queueForEvents
|
||||
->setParam('functionId', $function->getId())
|
||||
->setParam('deploymentId', $deployment->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Functions\Http\Functions;
|
||||
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Validator\FunctionEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Functions\Validator\RuntimeSpecification;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Task\Validator\Cron;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model\Rule;
|
||||
use Utopia\App;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Swoole\Request;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\VCS\Adapter\Git\GitHub;
|
||||
|
||||
class CreateFunction extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'createFunction';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/functions')
|
||||
->desc('Create function')
|
||||
->groups(['api', 'functions'])
|
||||
->label('scope', 'functions.write')
|
||||
->label('event', 'functions.[functionId].create')
|
||||
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
|
||||
->label('audits.event', 'function.create')
|
||||
->label('audits.resource', 'function/{response.$id}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'functions')
|
||||
->label('sdk.method', 'create')
|
||||
->label('sdk.description', '/docs/references/functions/create-function.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_FUNCTION)
|
||||
->param('functionId', '', new CustomId(), 'Function ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
|
||||
->param('name', '', new Text(128), 'Function name. Max length: 128 chars.')
|
||||
->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.')
|
||||
->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true)
|
||||
->param('events', [], new ArrayList(new FunctionEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true)
|
||||
->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true)
|
||||
->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_COMPUTE_TIMEOUT', 900)), 'Function maximum execution time in seconds.', true)
|
||||
->param('enabled', true, new Boolean(), 'Is function enabled? When set to \'disabled\', users cannot access the function but Server SDKs with and API key can still access the function. No data is lost when this is toggled.', true)
|
||||
->param('logging', true, new Boolean(), 'Whether executions will be logged. When set to false, executions will not be logged, but will reduce resource used by your Appwrite project.', true)
|
||||
->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true)
|
||||
->param('commands', '', new Text(8192, 0), 'Build Commands.', true)
|
||||
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true)
|
||||
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true)
|
||||
->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the function.', true)
|
||||
->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('templateRepository', '', new Text(128, 0), 'Repository name of the template.', true)
|
||||
->param('templateOwner', '', new Text(128, 0), 'The name of the owner of the template.', true)
|
||||
->param('templateRootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.', true)
|
||||
->param('templateVersion', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true)
|
||||
->param('specification', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification(
|
||||
$plan,
|
||||
Config::getParam('runtime-specifications', []),
|
||||
App::getEnv('_APP_COMPUTE_CPUS', APP_COMPUTE_CPUS_DEFAULT),
|
||||
App::getEnv('_APP_COMPUTE_MEMORY', APP_COMPUTE_MEMORY_DEFAULT)
|
||||
), 'Runtime specification for the function and builds.', true, ['plan'])
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForBuilds')
|
||||
->inject('dbForConsole')
|
||||
->inject('gitHub')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github)
|
||||
{
|
||||
$functionId = ($functionId == 'unique()') ? ID::unique() : $functionId;
|
||||
|
||||
$allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', '')));
|
||||
|
||||
if (!empty($allowList) && !\in_array($runtime, $allowList)) {
|
||||
throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $runtime . '" is not supported');
|
||||
}
|
||||
|
||||
// build from template
|
||||
$template = new Document([]);
|
||||
if (
|
||||
!empty($templateRepository)
|
||||
&& !empty($templateOwner)
|
||||
&& !empty($templateRootDirectory)
|
||||
&& !empty($templateVersion)
|
||||
) {
|
||||
$template->setAttribute('repositoryName', $templateRepository)
|
||||
->setAttribute('ownerName', $templateOwner)
|
||||
->setAttribute('rootDirectory', $templateRootDirectory)
|
||||
->setAttribute('version', $templateVersion);
|
||||
}
|
||||
|
||||
$installation = $dbForConsole->getDocument('installations', $installationId);
|
||||
|
||||
if (!empty($installationId) && $installation->isEmpty()) {
|
||||
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!empty($providerRepositoryId) && (empty($installationId) || empty($providerBranch))) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".');
|
||||
}
|
||||
|
||||
$function = $dbForProject->createDocument('functions', new Document([
|
||||
'$id' => $functionId,
|
||||
'execute' => $execute,
|
||||
'enabled' => $enabled,
|
||||
'live' => true,
|
||||
'logging' => $logging,
|
||||
'name' => $name,
|
||||
'runtime' => $runtime,
|
||||
'deploymentInternalId' => '',
|
||||
'deployment' => '',
|
||||
'events' => $events,
|
||||
'schedule' => $schedule,
|
||||
'scheduleInternalId' => '',
|
||||
'scheduleId' => '',
|
||||
'timeout' => $timeout,
|
||||
'entrypoint' => $entrypoint,
|
||||
'commands' => $commands,
|
||||
'scopes' => $scopes,
|
||||
'search' => implode(' ', [$functionId, $name, $runtime]),
|
||||
'version' => 'v4',
|
||||
'installationId' => $installation->getId(),
|
||||
'installationInternalId' => $installation->getInternalId(),
|
||||
'providerRepositoryId' => $providerRepositoryId,
|
||||
'repositoryId' => '',
|
||||
'repositoryInternalId' => '',
|
||||
'providerBranch' => $providerBranch,
|
||||
'providerRootDirectory' => $providerRootDirectory,
|
||||
'providerSilentMode' => $providerSilentMode,
|
||||
'specification' => $specification
|
||||
]));
|
||||
|
||||
$schedule = Authorization::skip(
|
||||
fn () => $dbForConsole->createDocument('schedules', new Document([
|
||||
'region' => System::getEnv('_APP_REGION', 'default'), // Todo replace with projects region
|
||||
'resourceType' => 'function',
|
||||
'resourceId' => $function->getId(),
|
||||
'resourceInternalId' => $function->getInternalId(),
|
||||
'resourceUpdatedAt' => DateTime::now(),
|
||||
'projectId' => $project->getId(),
|
||||
'schedule' => $function->getAttribute('schedule'),
|
||||
'active' => false,
|
||||
]))
|
||||
);
|
||||
|
||||
$function->setAttribute('scheduleId', $schedule->getId());
|
||||
$function->setAttribute('scheduleInternalId', $schedule->getInternalId());
|
||||
|
||||
// Git connect logic
|
||||
if (!empty($providerRepositoryId)) {
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
|
||||
$repository = $dbForConsole->createDocument('repositories', 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')),
|
||||
],
|
||||
'installationId' => $installation->getId(),
|
||||
'installationInternalId' => $installation->getInternalId(),
|
||||
'projectId' => $project->getId(),
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'providerRepositoryId' => $providerRepositoryId,
|
||||
'resourceId' => $function->getId(),
|
||||
'resourceInternalId' => $function->getInternalId(),
|
||||
'resourceType' => 'function',
|
||||
'providerPullRequestIds' => []
|
||||
]));
|
||||
|
||||
$function->setAttribute('repositoryId', $repository->getId());
|
||||
$function->setAttribute('repositoryInternalId', $repository->getInternalId());
|
||||
}
|
||||
|
||||
$function = $dbForProject->updateDocument('functions', $function->getId(), $function);
|
||||
|
||||
if (!empty($providerRepositoryId)) {
|
||||
// Deploy VCS
|
||||
$this->redeployVcsFunction($request, $function, $project, $installation, $dbForProject, $queueForBuilds, $template, $github);
|
||||
} elseif (!$template->isEmpty()) {
|
||||
// Deploy non-VCS from template
|
||||
$deploymentId = ID::unique();
|
||||
$deployment = $dbForProject->createDocument('deployments', new Document([
|
||||
'$id' => $deploymentId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceId' => $function->getId(),
|
||||
'resourceInternalId' => $function->getInternalId(),
|
||||
'resourceType' => 'functions',
|
||||
'entrypoint' => $function->getAttribute('entrypoint', ''),
|
||||
'commands' => $function->getAttribute('commands', ''),
|
||||
'type' => 'manual',
|
||||
'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint', '')]),
|
||||
'activate' => true,
|
||||
]));
|
||||
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
->setResource($function)
|
||||
->setDeployment($deployment)
|
||||
->setTemplate($template);
|
||||
}
|
||||
|
||||
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
|
||||
if (!empty($functionsDomain)) {
|
||||
$routeSubdomain = ID::unique();
|
||||
$domain = "{$routeSubdomain}.{$functionsDomain}";
|
||||
$ruleId = md5($domain);
|
||||
|
||||
$rule = Authorization::skip(
|
||||
fn () => $dbForConsole->createDocument('rules', new Document([
|
||||
'$id' => $ruleId,
|
||||
'projectId' => $project->getId(),
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'domain' => $domain,
|
||||
'resourceType' => 'function',
|
||||
'resourceId' => $function->getId(),
|
||||
'resourceInternalId' => $function->getInternalId(),
|
||||
'status' => 'verified',
|
||||
'certificateId' => '',
|
||||
]))
|
||||
);
|
||||
|
||||
/** Trigger Webhook */
|
||||
$ruleModel = new Rule();
|
||||
$ruleCreate =
|
||||
$queueForEvents
|
||||
->setClass(Event::WEBHOOK_CLASS_NAME)
|
||||
->setQueue(Event::WEBHOOK_QUEUE_NAME);
|
||||
|
||||
$ruleCreate
|
||||
->setProject($project)
|
||||
->setEvent('rules.[ruleId].create')
|
||||
->setParam('ruleId', $rule->getId())
|
||||
->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules())))
|
||||
->trigger();
|
||||
|
||||
/** Trigger Functions */
|
||||
$ruleCreate
|
||||
->setClass(Event::FUNCTIONS_CLASS_NAME)
|
||||
->setQueue(Event::FUNCTIONS_QUEUE_NAME)
|
||||
->trigger();
|
||||
|
||||
/** Trigger realtime event */
|
||||
$allEvents = Event::generateEvents('rules.[ruleId].create', [
|
||||
'ruleId' => $rule->getId(),
|
||||
]);
|
||||
$target = Realtime::fromPayload(
|
||||
// Pass first, most verbose event pattern
|
||||
event: $allEvents[0],
|
||||
payload: $rule,
|
||||
project: $project
|
||||
);
|
||||
Realtime::send(
|
||||
projectId: 'console',
|
||||
payload: $rule->getArrayCopy(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles']
|
||||
);
|
||||
Realtime::send(
|
||||
projectId: $project->getId(),
|
||||
payload: $rule->getArrayCopy(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles']
|
||||
);
|
||||
}
|
||||
|
||||
$queueForEvents->setParam('functionId', $function->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($function, Response::MODEL_FUNCTION);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Functions\Http\Functions;
|
||||
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Validator\FunctionEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Functions\Validator\RuntimeSpecification;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Task\Validator\Cron;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Executor\Executor;
|
||||
use Utopia\App;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Roles;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Swoole\Request;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\VCS\Adapter\Git\GitHub;
|
||||
|
||||
class UpdateFunction extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateFunction';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
|
||||
->setHttpPath('/v1/functions/:functionId')
|
||||
->desc('Update function')
|
||||
->groups(['api', 'functions'])
|
||||
->label('scope', 'functions.write')
|
||||
->label('event', 'functions.[functionId].update')
|
||||
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
|
||||
->label('audits.event', 'function.update')
|
||||
->label('audits.resource', 'function/{response.$id}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'functions')
|
||||
->label('sdk.method', 'update')
|
||||
->label('sdk.description', '/docs/references/functions/update-function.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_FUNCTION)
|
||||
->param('functionId', '', new UID(), 'Function ID.')
|
||||
->param('name', '', new Text(128), 'Function name. Max length: 128 chars.')
|
||||
->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.', true)
|
||||
->param('execute', [], new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true)
|
||||
->param('events', [], new ArrayList(new FunctionEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true)
|
||||
->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true)
|
||||
->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_COMPUTE_TIMEOUT', 900)), 'Maximum execution time in seconds.', true)
|
||||
->param('enabled', true, new Boolean(), 'Is function enabled? When set to \'disabled\', users cannot access the function but Server SDKs with and API key can still access the function. No data is lost when this is toggled.', true)
|
||||
->param('logging', true, new Boolean(), 'Whether executions will be logged. When set to false, executions will not be logged, but will reduce resource used by your Appwrite project.', true)
|
||||
->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true)
|
||||
->param('commands', '', new Text(8192, 0), 'Build Commands.', true)
|
||||
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API Key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true)
|
||||
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Controle System) deployment.', true)
|
||||
->param('providerRepositoryId', null, new Nullable(new Text(128, 0)), 'Repository ID of the repo linked to the function', true)
|
||||
->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', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification(
|
||||
$plan,
|
||||
Config::getParam('runtime-specifications', []),
|
||||
App::getEnv('_APP_COMPUTE_CPUS', APP_COMPUTE_CPUS_DEFAULT),
|
||||
App::getEnv('_APP_COMPUTE_MEMORY', APP_COMPUTE_MEMORY_DEFAULT)
|
||||
), 'Runtime specification for the function and builds.', true, ['plan'])
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForBuilds')
|
||||
->inject('dbForConsole')
|
||||
->inject('gitHub')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github)
|
||||
{
|
||||
// TODO: If only branch changes, re-deploy
|
||||
$function = $dbForProject->getDocument('functions', $functionId);
|
||||
|
||||
if ($function->isEmpty()) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$installation = $dbForConsole->getDocument('installations', $installationId);
|
||||
|
||||
if (!empty($installationId) && $installation->isEmpty()) {
|
||||
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!empty($providerRepositoryId) && (empty($installationId) || empty($providerBranch))) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".');
|
||||
}
|
||||
|
||||
if ($function->isEmpty()) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (empty($runtime)) {
|
||||
$runtime = $function->getAttribute('runtime');
|
||||
}
|
||||
|
||||
$enabled ??= $function->getAttribute('enabled', true);
|
||||
|
||||
$repositoryId = $function->getAttribute('repositoryId', '');
|
||||
$repositoryInternalId = $function->getAttribute('repositoryInternalId', '');
|
||||
|
||||
if (empty($entrypoint)) {
|
||||
$entrypoint = $function->getAttribute('entrypoint', '');
|
||||
}
|
||||
|
||||
$isConnected = !empty($function->getAttribute('providerRepositoryId', ''));
|
||||
|
||||
// Git disconnect logic. Disconnecting only when providerRepositoryId is empty, allowing for continue updates without disconnecting git
|
||||
if ($isConnected && ($providerRepositoryId !== null && empty($providerRepositoryId))) {
|
||||
$repositories = $dbForConsole->find('repositories', [
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
Query::equal('resourceInternalId', [$function->getInternalId()]),
|
||||
Query::equal('resourceType', ['function']),
|
||||
Query::limit(100),
|
||||
]);
|
||||
|
||||
foreach ($repositories as $repository) {
|
||||
$dbForConsole->deleteDocument('repositories', $repository->getId());
|
||||
}
|
||||
|
||||
$providerRepositoryId = '';
|
||||
$installationId = '';
|
||||
$providerBranch = '';
|
||||
$providerRootDirectory = '';
|
||||
$providerSilentMode = true;
|
||||
$repositoryId = '';
|
||||
$repositoryInternalId = '';
|
||||
}
|
||||
|
||||
// Git connect logic
|
||||
if (!$isConnected && !empty($providerRepositoryId)) {
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
|
||||
$repository = $dbForConsole->createDocument('repositories', 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')),
|
||||
],
|
||||
'installationId' => $installation->getId(),
|
||||
'installationInternalId' => $installation->getInternalId(),
|
||||
'projectId' => $project->getId(),
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'providerRepositoryId' => $providerRepositoryId,
|
||||
'resourceId' => $function->getId(),
|
||||
'resourceInternalId' => $function->getInternalId(),
|
||||
'resourceType' => 'function',
|
||||
'providerPullRequestIds' => []
|
||||
]));
|
||||
|
||||
$repositoryId = $repository->getId();
|
||||
$repositoryInternalId = $repository->getInternalId();
|
||||
}
|
||||
|
||||
$live = true;
|
||||
|
||||
if (
|
||||
$function->getAttribute('name') !== $name ||
|
||||
$function->getAttribute('entrypoint') !== $entrypoint ||
|
||||
$function->getAttribute('commands') !== $commands ||
|
||||
$function->getAttribute('providerRootDirectory') !== $providerRootDirectory ||
|
||||
$function->getAttribute('runtime') !== $runtime
|
||||
) {
|
||||
$live = false;
|
||||
}
|
||||
|
||||
$spec = Config::getParam('runtime-specifications')[$specification] ?? [];
|
||||
|
||||
// Enforce Cold Start if spec limits change.
|
||||
if ($function->getAttribute('specification') !== $specification && !empty($function->getAttribute('deployment'))) {
|
||||
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
|
||||
try {
|
||||
$executor->deleteRuntime($project->getId(), $function->getAttribute('deployment'));
|
||||
} catch (\Throwable $th) {
|
||||
// Don't throw if the deployment doesn't exist
|
||||
if ($th->getCode() !== 404) {
|
||||
throw $th;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [
|
||||
'execute' => $execute,
|
||||
'name' => $name,
|
||||
'runtime' => $runtime,
|
||||
'events' => $events,
|
||||
'schedule' => $schedule,
|
||||
'timeout' => $timeout,
|
||||
'enabled' => $enabled,
|
||||
'live' => $live,
|
||||
'logging' => $logging,
|
||||
'entrypoint' => $entrypoint,
|
||||
'commands' => $commands,
|
||||
'scopes' => $scopes,
|
||||
'installationId' => $installation->getId(),
|
||||
'installationInternalId' => $installation->getInternalId(),
|
||||
'providerRepositoryId' => $providerRepositoryId,
|
||||
'repositoryId' => $repositoryId,
|
||||
'repositoryInternalId' => $repositoryInternalId,
|
||||
'providerBranch' => $providerBranch,
|
||||
'providerRootDirectory' => $providerRootDirectory,
|
||||
'providerSilentMode' => $providerSilentMode,
|
||||
'specification' => $specification,
|
||||
'search' => implode(' ', [$functionId, $name, $runtime]),
|
||||
])));
|
||||
|
||||
// Redeploy logic
|
||||
if (!$isConnected && !empty($providerRepositoryId)) {
|
||||
$this->redeployVcsFunction($request, $function, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $github);
|
||||
}
|
||||
|
||||
// Inform scheduler if function is still active
|
||||
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
|
||||
$schedule
|
||||
->setAttribute('resourceUpdatedAt', DateTime::now())
|
||||
->setAttribute('schedule', $function->getAttribute('schedule'))
|
||||
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
|
||||
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
|
||||
|
||||
$queueForEvents->setParam('functionId', $function->getId());
|
||||
|
||||
$response->dynamic($function, Response::MODEL_FUNCTION);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Functions;
|
||||
|
||||
use Appwrite\Platform\Modules\Functions\Services\Http;
|
||||
use Appwrite\Platform\Modules\Functions\Services\Workers;
|
||||
use Utopia\Platform;
|
||||
|
||||
class Module extends Platform\Module
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->addService('http', new Http());
|
||||
$this->addService('workers', new Workers());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Functions\Services;
|
||||
|
||||
use Appwrite\Platform\Modules\Functions\Http\Deployments\CreateDeployment;
|
||||
use Appwrite\Platform\Modules\Functions\Http\Functions\CreateFunction;
|
||||
use Appwrite\Platform\Modules\Functions\Http\Functions\UpdateFunction;
|
||||
use Utopia\Platform\Service;
|
||||
|
||||
class Http extends Service
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->type = Service::TYPE_HTTP;
|
||||
$this->addAction(CreateFunction::getName(), new CreateFunction());
|
||||
$this->addAction(UpdateFunction::getName(), new UpdateFunction());
|
||||
$this->addAction(CreateDeployment::getName(), new CreateDeployment());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Functions\Services;
|
||||
|
||||
use Appwrite\Platform\Modules\Functions\Workers\Builds;
|
||||
use Utopia\Platform\Service;
|
||||
|
||||
class Workers extends Service
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->type = Service::TYPE_WORKER;
|
||||
$this->addAction(Builds::getName(), new Builds());
|
||||
}
|
||||
}
|
||||
+304
-108
@@ -1,10 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Workers;
|
||||
namespace Appwrite\Platform\Modules\Functions\Workers;
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Insight;
|
||||
use Appwrite\Event\Usage;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Utopia\Response\Model\Deployment;
|
||||
@@ -22,6 +23,7 @@ use Utopia\Database\Exception\Conflict;
|
||||
use Utopia\Database\Exception\Restricted;
|
||||
use Utopia\Database\Exception\Structure;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Platform\Action;
|
||||
@@ -47,6 +49,7 @@ class Builds extends Action
|
||||
->desc('Builds worker')
|
||||
->inject('message')
|
||||
->inject('dbForConsole')
|
||||
->inject('queueForInsights')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForUsage')
|
||||
@@ -54,12 +57,13 @@ class Builds extends Action
|
||||
->inject('dbForProject')
|
||||
->inject('deviceForFunctions')
|
||||
->inject('log')
|
||||
->callback(fn ($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log));
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Message $message
|
||||
* @param Database $dbForConsole
|
||||
* @param Insight $queueForInsights
|
||||
* @param Event $queueForEvents
|
||||
* @param Func $queueForFunctions
|
||||
* @param Usage $queueForUsage
|
||||
@@ -70,7 +74,7 @@ class Builds extends Action
|
||||
* @return void
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log): void
|
||||
public function action(Message $message, Database $dbForConsole, Insight $queueForInsights, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log): void
|
||||
{
|
||||
$payload = $message->getPayload() ?? [];
|
||||
|
||||
@@ -92,7 +96,7 @@ class Builds extends Action
|
||||
case BUILD_TYPE_RETRY:
|
||||
Console::info('Creating build for deployment: ' . $deployment->getId());
|
||||
$github = new GitHub($cache);
|
||||
$this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log);
|
||||
$this->buildDeployment($deviceForFunctions, $queueForInsights, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -102,6 +106,7 @@ class Builds extends Action
|
||||
|
||||
/**
|
||||
* @param Device $deviceForFunctions
|
||||
* @param Insight $queueForInsights
|
||||
* @param Func $queueForFunctions
|
||||
* @param Event $queueForEvents
|
||||
* @param Usage $queueForUsage
|
||||
@@ -109,50 +114,51 @@ class Builds extends Action
|
||||
* @param Database $dbForProject
|
||||
* @param GitHub $github
|
||||
* @param Document $project
|
||||
* @param Document $function
|
||||
* @param Document $resource
|
||||
* @param Document $deployment
|
||||
* @param Document $template
|
||||
* @param Log $log
|
||||
* @return void
|
||||
* @throws \Utopia\Database\Exception
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void
|
||||
protected function buildDeployment(Device $deviceForFunctions, Insight $queueForInsights, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $resource, Document $deployment, Document $template, Log $log): void
|
||||
{
|
||||
$resourceKey = match($resource->getCollection()) {
|
||||
'functions' => 'functionId',
|
||||
'sites' => 'siteId',
|
||||
default => throw new \Exception('Invalid resource type')
|
||||
};
|
||||
|
||||
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
|
||||
|
||||
$functionId = $function->getId();
|
||||
$log->addTag('functionId', $function->getId());
|
||||
$log->addTag($resourceKey, $resource->getId());
|
||||
|
||||
$function = $dbForProject->getDocument('functions', $functionId);
|
||||
if ($function->isEmpty()) {
|
||||
$resource = $dbForProject->getDocument($resource->getCollection(), $resource->getId());
|
||||
if ($resource->isEmpty()) {
|
||||
throw new \Exception('Function not found', 404);
|
||||
}
|
||||
|
||||
$deploymentId = $deployment->getId();
|
||||
$log->addTag('deploymentId', $deploymentId);
|
||||
$log->addTag('deploymentId', $deployment->getId());
|
||||
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
$deployment = $dbForProject->getDocument('deployments', $deployment->getId());
|
||||
if ($deployment->isEmpty()) {
|
||||
throw new \Exception('Deployment not found', 404);
|
||||
}
|
||||
|
||||
if (empty($deployment->getAttribute('entrypoint', ''))) {
|
||||
// todo: figure out a better way, entrypoint is not required for sites
|
||||
if ($resource->getCollection() === 'functions' && empty($deployment->getAttribute('entrypoint', ''))) {
|
||||
throw new \Exception('Entrypoint for your Appwrite Function is missing. Please specify it when making deployment or update the entrypoint under your function\'s "Settings" > "Configuration" > "Entrypoint".', 500);
|
||||
}
|
||||
|
||||
$version = $function->getAttribute('version', 'v2');
|
||||
$spec = Config::getParam('runtime-specifications')[$function->getAttribute('specifications', APP_FUNCTION_SPECIFICATION_DEFAULT)];
|
||||
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
|
||||
$key = $function->getAttribute('runtime');
|
||||
$runtime = $runtimes[$key] ?? null;
|
||||
if (\is_null($runtime)) {
|
||||
throw new \Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
|
||||
}
|
||||
$version = $this->getVersion($resource);
|
||||
$runtime = $this->getRuntime($resource, $version);
|
||||
$spec = Config::getParam('runtime-specifications')[$resource->getAttribute('specifications', APP_COMPUTE_SPECIFICATION_DEFAULT)];
|
||||
|
||||
// Realtime preparation
|
||||
$allEvents = Event::generateEvents('functions.[functionId].deployments.[deploymentId].update', [
|
||||
'functionId' => $function->getId(),
|
||||
$allEvents = Event::generateEvents("{$resource->getCollection()}.[{$resourceKey}].deployments.[deploymentId].update", [
|
||||
$resourceKey => $resource->getId(),
|
||||
'deploymentId' => $deployment->getId()
|
||||
]);
|
||||
|
||||
@@ -171,10 +177,10 @@ class Builds extends Action
|
||||
'deploymentId' => $deployment->getId(),
|
||||
'status' => 'processing',
|
||||
'path' => '',
|
||||
'runtime' => $function->getAttribute('runtime'),
|
||||
'runtime' => $resource->getAttribute('runtime'),
|
||||
'source' => $deployment->getAttribute('path', ''),
|
||||
'sourceType' => strtolower($deviceForFunctions->getType()),
|
||||
'logs' => '',
|
||||
'logs' => [],
|
||||
'endTime' => null,
|
||||
'duration' => 0,
|
||||
'size' => 0
|
||||
@@ -263,7 +269,7 @@ class Builds extends Action
|
||||
} elseif ($isNewBuild && $isVcsEnabled) {
|
||||
// VCS and VCS+Temaplte
|
||||
$tmpDirectory = '/tmp/builds/' . $buildId . '/code';
|
||||
$rootDirectory = $function->getAttribute('providerRootDirectory', '');
|
||||
$rootDirectory = $resource->getAttribute('providerRootDirectory', '');
|
||||
$rootDirectory = \rtrim($rootDirectory, '/');
|
||||
$rootDirectory = \ltrim($rootDirectory, '.');
|
||||
$rootDirectory = \ltrim($rootDirectory, '/');
|
||||
@@ -344,7 +350,7 @@ class Builds extends Action
|
||||
Console::execute('rsync -av --exclude \'.git\' ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory . '/') . ' ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $stdout, $stderr);
|
||||
|
||||
// Commit and push
|
||||
$exit = Console::execute('git config --global user.email "team@appwrite.io" && git config --global user.name "Appwrite" && cd ' . \escapeshellarg($tmpDirectory) . ' && git add . && git commit -m "Create ' . \escapeshellarg($function->getAttribute('name', '')) . ' function" && git push origin ' . \escapeshellarg($branchName), '', $stdout, $stderr);
|
||||
$exit = Console::execute('git config --global user.email "team@appwrite.io" && git config --global user.name "Appwrite" && cd ' . \escapeshellarg($tmpDirectory) . ' && git add . && git commit -m "Create ' . \escapeshellarg($resource->getAttribute('name', '')) . ' function" && git push origin ' . \escapeshellarg($branchName), '', $stdout, $stderr);
|
||||
|
||||
if ($exit !== 0) {
|
||||
throw new \Exception('Unable to push code repository: ' . $stderr);
|
||||
@@ -362,7 +368,7 @@ class Builds extends Action
|
||||
$deployment->setAttribute('providerCommitHash', $providerCommitHash ?? '');
|
||||
$deployment->setAttribute('providerCommitAuthorUrl', $authorUrl);
|
||||
$deployment->setAttribute('providerCommitAuthor', 'Appwrite');
|
||||
$deployment->setAttribute('providerCommitMessage', "Create '" . $function->getAttribute('name', '') . "' function");
|
||||
$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);
|
||||
|
||||
@@ -393,9 +399,10 @@ class Builds extends Action
|
||||
}
|
||||
|
||||
$directorySize = $localDevice->getDirectorySize($tmpDirectory);
|
||||
$functionsSizeLimit = (int)System::getEnv('_APP_FUNCTIONS_SIZE_LIMIT', '30000000');
|
||||
if ($directorySize > $functionsSizeLimit) {
|
||||
throw new \Exception('Repository directory size should be less than ' . number_format($functionsSizeLimit / 1048576, 2) . ' MBs.');
|
||||
$sizeLimit = (int)System::getEnv('_APP_COMPUTE_SIZE_LIMIT', '30000000');
|
||||
|
||||
if ($directorySize > $sizeLimit) {
|
||||
throw new \Exception('Repository directory size should be less than ' . number_format($sizeLimit / 1048576, 2) . ' MBs.');
|
||||
}
|
||||
|
||||
$tarParamDirectory = '/tmp/builds/' . $buildId . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory);
|
||||
@@ -415,7 +422,7 @@ class Builds extends Action
|
||||
$directorySize = $deviceForFunctions->getFileSize($source);
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttribute('path', $source)->setAttribute('size', $directorySize));
|
||||
|
||||
$this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
|
||||
$this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForConsole);
|
||||
}
|
||||
|
||||
/** Request the executor to build the code... */
|
||||
@@ -423,7 +430,7 @@ class Builds extends Action
|
||||
$build = $dbForProject->updateDocument('builds', $buildId, $build);
|
||||
|
||||
if ($isVcsEnabled) {
|
||||
$this->runGitAction('building', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
|
||||
$this->runGitAction('building', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForConsole);
|
||||
}
|
||||
|
||||
/** Trigger Webhook */
|
||||
@@ -433,8 +440,8 @@ class Builds extends Action
|
||||
->setQueue(Event::WEBHOOK_QUEUE_NAME)
|
||||
->setClass(Event::WEBHOOK_CLASS_NAME)
|
||||
->setProject($project)
|
||||
->setEvent('functions.[functionId].deployments.[deploymentId].update')
|
||||
->setParam('functionId', $function->getId())
|
||||
->setEvent("{$resource->getCollection()}.[{$resourceKey}].deployments.[deploymentId].update")
|
||||
->setParam($resourceKey, $resource->getId())
|
||||
->setParam('deploymentId', $deployment->getId())
|
||||
->setPayload($deployment->getArrayCopy(array_keys($deploymentModel->getRules())));
|
||||
|
||||
@@ -464,24 +471,31 @@ class Builds extends Action
|
||||
$vars = [];
|
||||
|
||||
// Shared vars
|
||||
foreach ($function->getAttribute('varsProject', []) as $var) {
|
||||
foreach ($resource->getAttribute('varsProject', []) as $var) {
|
||||
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
|
||||
}
|
||||
|
||||
// Function vars
|
||||
foreach ($function->getAttribute('vars', []) as $var) {
|
||||
foreach ($resource->getAttribute('vars', []) as $var) {
|
||||
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
|
||||
}
|
||||
|
||||
$cpus = $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT;
|
||||
$memory = max($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT, 1024); // We have a minimum of 1024MB here because some runtimes can't compile with less memory than this.
|
||||
$cpus = $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT;
|
||||
$memory = max($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, 1024); // We have a minimum of 1024MB here because some runtimes can't compile with less memory than this.
|
||||
$timeout = (int) System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT', 900);
|
||||
|
||||
$jwtExpiry = (int)System::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900);
|
||||
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
|
||||
$apiKey = $jwtObj->encode([
|
||||
'projectId' => $project->getId(),
|
||||
'scopes' => $function->getAttribute('scopes', [])
|
||||
]);
|
||||
// JWT and API key generation for functions only
|
||||
if ($resource->getCollection() === 'functions') {
|
||||
$jwtExpiry = (int)System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT', 900);
|
||||
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
|
||||
|
||||
$apiKey = $jwtObj->encode([
|
||||
'projectId' => $project->getId(),
|
||||
'scopes' => $resource->getAttribute('scopes', [])
|
||||
]);
|
||||
|
||||
$vars = array_merge($vars, ['APPWRITE_FUNCTION_API_KEY' => API_KEY_DYNAMIC . '_' . $apiKey]);
|
||||
}
|
||||
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$hostname = System::getEnv('_APP_DOMAIN');
|
||||
@@ -489,16 +503,6 @@ class Builds extends Action
|
||||
|
||||
// Appwrite vars
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
|
||||
'APPWRITE_FUNCTION_API_KEY' => API_KEY_DYNAMIC . '_' . $apiKey,
|
||||
'APPWRITE_FUNCTION_ID' => $function->getId(),
|
||||
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'),
|
||||
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
|
||||
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
|
||||
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
|
||||
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
|
||||
'APPWRITE_FUNCTION_CPUS' => $cpus,
|
||||
'APPWRITE_FUNCTION_MEMORY' => $memory,
|
||||
'APPWRITE_VERSION' => APP_VERSION_STABLE,
|
||||
'APPWRITE_REGION' => $project->getAttribute('region'),
|
||||
'APPWRITE_DEPLOYMENT_TYPE' => $deployment->getAttribute('type', ''),
|
||||
@@ -516,7 +520,40 @@ class Builds extends Action
|
||||
'APPWRITE_VCS_ROOT_DIRECTORY' => $deployment->getAttribute('providerRootDirectory', ''),
|
||||
]);
|
||||
|
||||
$command = $deployment->getAttribute('commands', '');
|
||||
switch ($resource->getCollection()) {
|
||||
case 'functions':
|
||||
$vars = [
|
||||
...$vars,
|
||||
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
|
||||
'APPWRITE_FUNCTION_ID' => $resource->getId(),
|
||||
'APPWRITE_FUNCTION_NAME' => $resource->getAttribute('name'),
|
||||
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
|
||||
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
|
||||
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
|
||||
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
|
||||
'APPWRITE_COMPUTE_CPUS' => $cpus,
|
||||
'APPWRITE_COMPUTE_MEMORY' => $memory
|
||||
];
|
||||
break;
|
||||
case 'sites':
|
||||
$vars = [
|
||||
...$vars,
|
||||
'APPWRITE_SITE_ID' => $resource->getId(),
|
||||
'APPWRITE_SITE_NAME' => $resource->getAttribute('name'),
|
||||
'APPWRITE_SITE_DEPLOYMENT' => $deployment->getId(),
|
||||
'APPWRITE_SITE_PROJECT_ID' => $project->getId(),
|
||||
'APPWRITE_SITE_RUNTIME_NAME' => $runtime['name'] ?? '',
|
||||
'APPWRITE_SITE_RUNTIME_VERSION' => $runtime['version'] ?? '',
|
||||
'APPWRITE_COMPUTE_CPUS' => $cpus,
|
||||
'APPWRITE_COMPUTE_MEMORY' => $memory
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
$command = $this->getCommand(
|
||||
resource: $resource,
|
||||
deployment: $deployment
|
||||
);
|
||||
|
||||
$response = null;
|
||||
$err = null;
|
||||
@@ -529,9 +566,8 @@ class Builds extends Action
|
||||
$isCanceled = false;
|
||||
|
||||
Co::join([
|
||||
Co\go(function () use ($executor, &$response, $project, $deployment, $source, $function, $runtime, $vars, $command, $cpus, $memory, &$err) {
|
||||
Co\go(function () use ($executor, &$response, $project, $deployment, $source, $resource, $runtime, $vars, $command, $cpus, $memory, $timeout, &$err, $version) {
|
||||
try {
|
||||
$version = $function->getAttribute('version', 'v2');
|
||||
$command = $version === 'v2' ? 'tar -zxf /tmp/code.tar.gz -C /usr/code && cd /usr/local/src/ && ./build.sh' : 'tar -zxf /tmp/code.tar.gz -C /mnt/code && helpers/build.sh "' . \trim(\escapeshellarg($command), "\'") . '"';
|
||||
|
||||
$response = $executor->createRuntime(
|
||||
@@ -542,21 +578,24 @@ class Builds extends Action
|
||||
version: $version,
|
||||
cpus: $cpus,
|
||||
memory: $memory,
|
||||
timeout: $timeout,
|
||||
remove: true,
|
||||
entrypoint: $deployment->getAttribute('entrypoint'),
|
||||
entrypoint: $deployment->getAttribute('entrypoint', 'package.json'), // TODO: change this later so that sites don't need to have an entrypoint
|
||||
destination: APP_STORAGE_BUILDS . "/app-{$project->getId()}",
|
||||
variables: $vars,
|
||||
command: $command
|
||||
command: $command,
|
||||
outputDirectory: $resource->getAttribute('outputDirectory', '')
|
||||
);
|
||||
} catch (\Throwable $error) {
|
||||
$err = $error;
|
||||
}
|
||||
}),
|
||||
Co\go(function () use ($executor, $project, $deployment, &$response, &$build, $dbForProject, $allEvents, &$err, &$isCanceled) {
|
||||
Co\go(function () use ($executor, $project, $deployment, &$response, &$build, $dbForProject, $allEvents, $timeout, &$err, &$isCanceled) {
|
||||
try {
|
||||
$executor->getLogs(
|
||||
deploymentId: $deployment->getId(),
|
||||
projectId: $project->getId(),
|
||||
timeout: $timeout,
|
||||
callback: function ($logs) use (&$response, &$err, &$build, $dbForProject, $allEvents, $project, &$isCanceled) {
|
||||
if ($isCanceled) {
|
||||
return;
|
||||
@@ -576,9 +615,29 @@ class Builds extends Action
|
||||
return;
|
||||
}
|
||||
|
||||
$logs = \mb_substr($logs, 0, null, 'UTF-8'); // Get only valid UTF8 part - removes leftover half-multibytes causing SQL errors
|
||||
// Get only valid UTF8 part - removes leftover half-multibytes causing SQL errors
|
||||
$logs = \mb_substr($logs, 0, null, 'UTF-8');
|
||||
|
||||
$build = $build->setAttribute('logs', $build->getAttribute('logs', '') . $logs);
|
||||
$currentChunks = $build->getAttribute('logs', []);
|
||||
|
||||
// Parse styled×tamped logs
|
||||
$streamChunks = [];
|
||||
$streamLogs = \str_replace("\\n", "{APPWRITE_LINEBREAK_PLACEHOLDER}", $logs);
|
||||
foreach (\explode("\n", $streamLogs) as $streamLog) {
|
||||
if (empty($streamLog)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$streamLog = \str_replace("{APPWRITE_LINEBREAK_PLACEHOLDER}", "\n", $streamLog);
|
||||
$streamParts = \explode(" ", $streamLog, 2);
|
||||
|
||||
$currentChunks[] = [
|
||||
'timestamp' => $streamParts[0] ?? '',
|
||||
'content' => $streamParts[1] ?? ''
|
||||
];
|
||||
}
|
||||
|
||||
$build = $build->setAttribute('logs', $currentChunks);
|
||||
$build = $dbForProject->updateDocument('builds', $build->getId(), $build);
|
||||
|
||||
/**
|
||||
@@ -620,7 +679,7 @@ class Builds extends Action
|
||||
$endTime = DateTime::now();
|
||||
$durationEnd = \microtime(true);
|
||||
|
||||
$buildSizeLimit = (int)System::getEnv('_APP_FUNCTIONS_BUILD_SIZE_LIMIT', '2000000000');
|
||||
$buildSizeLimit = (int)System::getEnv('_APP_COMPUTE_BUILD_SIZE_LIMIT', '2000000000');
|
||||
if ($response['size'] > $buildSizeLimit) {
|
||||
throw new \Exception('Build size should be less than ' . number_format($buildSizeLimit / 1048576, 2) . ' MBs.');
|
||||
}
|
||||
@@ -632,22 +691,30 @@ class Builds extends Action
|
||||
$build->setAttribute('status', 'ready');
|
||||
$build->setAttribute('path', $response['path']);
|
||||
$build->setAttribute('size', $response['size']);
|
||||
$build->setAttribute('logs', $response['output']);
|
||||
// $build->setAttribute('logs', $response['output']); // TODO: Figure out how to write them all at the end
|
||||
|
||||
$build = $dbForProject->updateDocument('builds', $buildId, $build);
|
||||
|
||||
if ($isVcsEnabled) {
|
||||
$this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
|
||||
$this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForConsole);
|
||||
}
|
||||
|
||||
Console::success("Build id: $buildId created");
|
||||
|
||||
/** Set auto deploy */
|
||||
if ($deployment->getAttribute('activate') === true) {
|
||||
$function->setAttribute('deploymentInternalId', $deployment->getInternalId());
|
||||
$function->setAttribute('deployment', $deployment->getId());
|
||||
$function->setAttribute('live', true);
|
||||
$function = $dbForProject->updateDocument('functions', $function->getId(), $function);
|
||||
$resource->setAttribute('deploymentInternalId', $deployment->getInternalId());
|
||||
$resource->setAttribute('live', true);
|
||||
switch ($resource->getCollection()) {
|
||||
case 'functions':
|
||||
$resource->setAttribute('deployment', $deployment->getId());
|
||||
$resource = $dbForProject->updateDocument('functions', $resource->getId(), $resource);
|
||||
break;
|
||||
case 'sites':
|
||||
$resource->setAttribute('deploymentId', $deployment->getId());
|
||||
$resource = $dbForProject->updateDocument('sites', $resource->getId(), $resource);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
|
||||
@@ -658,12 +725,14 @@ class Builds extends Action
|
||||
/** Update function schedule */
|
||||
|
||||
// Inform scheduler if function is still active
|
||||
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
|
||||
$schedule
|
||||
->setAttribute('resourceUpdatedAt', DateTime::now())
|
||||
->setAttribute('schedule', $function->getAttribute('schedule'))
|
||||
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
|
||||
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
|
||||
if ($resource->getCollection() === 'functions') {
|
||||
$schedule = $dbForConsole->getDocument('schedules', $resource->getAttribute('scheduleId'));
|
||||
$schedule
|
||||
->setAttribute('resourceUpdatedAt', DateTime::now())
|
||||
->setAttribute('schedule', $resource->getAttribute('schedule'))
|
||||
->setAttribute('active', !empty($resource->getAttribute('schedule')) && !empty($resource->getAttribute('deployment')));
|
||||
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
|
||||
Console::info('Build has been canceled');
|
||||
@@ -675,12 +744,19 @@ class Builds extends Action
|
||||
$build->setAttribute('endTime', $endTime);
|
||||
$build->setAttribute('duration', \intval(\ceil($durationEnd - $durationStart)));
|
||||
$build->setAttribute('status', 'failed');
|
||||
$build->setAttribute('logs', $th->getMessage());
|
||||
|
||||
$datetime = new \DateTime();
|
||||
$build->setAttribute('logs', [
|
||||
[
|
||||
'timestamp' => $datetime->format('Y-m-d\TH:i:s.vP'),
|
||||
'content' => "[31m" . $th->getMessage() . "[0m"
|
||||
]
|
||||
]);
|
||||
|
||||
$build = $dbForProject->updateDocument('builds', $buildId, $build);
|
||||
|
||||
if ($isVcsEnabled) {
|
||||
$this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
|
||||
$this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForConsole);
|
||||
}
|
||||
} finally {
|
||||
/**
|
||||
@@ -699,34 +775,131 @@ class Builds extends Action
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles']
|
||||
);
|
||||
$this->sendUsage(
|
||||
resource:$resource,
|
||||
build: $build,
|
||||
project: $project,
|
||||
queue: $queueForUsage
|
||||
);
|
||||
|
||||
/** Trigger usage queue */
|
||||
if ($build->getAttribute('status') === 'ready') {
|
||||
$queueForUsage
|
||||
if ($resource->getCollection() === 'sites' && $build->getAttribute('status') === 'ready') {
|
||||
$queueForInsights
|
||||
->setResource($resource)
|
||||
->setBuild($build)
|
||||
->setDeployment($deployment)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function sendUsage(Document $resource, Document $build, Document $project, Usage $queue): void
|
||||
{
|
||||
$key = match($resource->getCollection()) {
|
||||
'functions' => 'functionInternalId',
|
||||
'sites' => 'siteInternalId',
|
||||
default => throw new \Exception('Invalid resource type')
|
||||
};
|
||||
|
||||
$metrics = match($resource->getCollection()) {
|
||||
'functions' => [
|
||||
'builds' => METRIC_FUNCTION_ID_BUILDS,
|
||||
'buildsSuccess' => METRIC_FUNCTION_ID_BUILDS_SUCCESS,
|
||||
'buildsFailed' => METRIC_FUNCTION_ID_BUILDS_FAILED,
|
||||
'buildsComputeSuccess' => METRIC_FUNCTION_ID_BUILDS_COMPUTE_SUCCESS,
|
||||
'buildsComputeFailed' => METRIC_FUNCTION_ID_BUILDS_COMPUTE_FAILED,
|
||||
'buildsStorage' => METRIC_FUNCTION_ID_BUILDS_STORAGE,
|
||||
'buildsCompute' => METRIC_FUNCTION_ID_BUILDS_COMPUTE,
|
||||
'buildsMbSeconds' => METRIC_FUNCTION_ID_BUILDS_MB_SECONDS
|
||||
],
|
||||
'sites' => [
|
||||
'builds' => METRIC_SITES_ID_BUILDS,
|
||||
'buildsSuccess' => METRIC_SITES_ID_BUILDS_SUCCESS,
|
||||
'buildsFailed' => METRIC_SITES_ID_BUILDS_FAILED,
|
||||
'buildsComputeSuccess' => METRIC_SITES_ID_BUILDS_COMPUTE_SUCCESS,
|
||||
'buildsComputeFailed' => METRIC_SITES_ID_BUILDS_COMPUTE_FAILED,
|
||||
'buildsStorage' => METRIC_SITES_ID_BUILDS_STORAGE,
|
||||
'buildsCompute' => METRIC_SITES_ID_BUILDS_COMPUTE,
|
||||
'buildsMbSeconds' => METRIC_SITES_ID_BUILDS_MB_SECONDS
|
||||
]
|
||||
};
|
||||
|
||||
switch ($build->getAttribute('status')) {
|
||||
case 'ready':
|
||||
$queue
|
||||
->addMetric(METRIC_BUILDS_SUCCESS, 1) // per project
|
||||
->addMetric(METRIC_BUILDS_COMPUTE_SUCCESS, (int)$build->getAttribute('duration', 0) * 1000)
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_SUCCESS), 1) // per function
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE_SUCCESS), (int)$build->getAttribute('duration', 0) * 1000);
|
||||
} elseif ($build->getAttribute('status') === 'failed') {
|
||||
$queueForUsage
|
||||
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsSuccess']), 1) // per function
|
||||
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsComputeSuccess']), (int)$build->getAttribute('duration', 0) * 1000);
|
||||
break;
|
||||
case 'failed':
|
||||
$queue
|
||||
->addMetric(METRIC_BUILDS_FAILED, 1) // per project
|
||||
->addMetric(METRIC_BUILDS_COMPUTE_FAILED, (int)$build->getAttribute('duration', 0) * 1000)
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_FAILED), 1) // per function
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE_FAILED), (int)$build->getAttribute('duration', 0) * 1000);
|
||||
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsFailed']), 1) // per function
|
||||
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsComputeFailed']), (int)$build->getAttribute('duration', 0) * 1000);
|
||||
break;
|
||||
}
|
||||
|
||||
$queue
|
||||
->addMetric(METRIC_BUILDS, 1) // per project
|
||||
->addMetric(METRIC_BUILDS_STORAGE, $build->getAttribute('size', 0))
|
||||
->addMetric(METRIC_BUILDS_COMPUTE, (int)$build->getAttribute('duration', 0) * 1000)
|
||||
->addMetric(METRIC_BUILDS_MB_SECONDS, (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
|
||||
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['builds']), 1) // per function
|
||||
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsStorage']), $build->getAttribute('size', 0))
|
||||
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsCompute']), (int)$build->getAttribute('duration', 0) * 1000)
|
||||
->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsMbSeconds']), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
protected function getRuntime(Document $resource, string $version): array
|
||||
{
|
||||
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
|
||||
$key = $resource->getAttribute('runtime');
|
||||
$runtime = match ($resource->getCollection()) {
|
||||
'functions' => $runtimes[$resource->getAttribute('runtime')] ?? null,
|
||||
'sites' => $runtimes[$resource->getAttribute('buildRuntime')] ?? null,
|
||||
default => null
|
||||
};
|
||||
if (\is_null($runtime)) {
|
||||
throw new \Exception('Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported');
|
||||
}
|
||||
|
||||
return $runtime;
|
||||
}
|
||||
|
||||
protected function getVersion(Document $resource): string
|
||||
{
|
||||
return match ($resource->getCollection()) {
|
||||
'functions' => $resource->getAttribute('version', 'v2'),
|
||||
'sites' => 'v4',
|
||||
};
|
||||
}
|
||||
|
||||
protected function getCommand(Document $resource, Document $deployment): string
|
||||
{
|
||||
if ($resource->getCollection() === 'functions') {
|
||||
return $deployment->getAttribute('commands', '');
|
||||
} elseif ($resource->getCollection() === 'sites') {
|
||||
$command = '';
|
||||
|
||||
$installCommand = $deployment->getAttribute('installCommand', '');
|
||||
$buildCommand = $deployment->getAttribute('buildCommand', '');
|
||||
|
||||
$command .= $installCommand;
|
||||
|
||||
if (!empty($installCommand) && !empty($buildCommand)) {
|
||||
$command .= ' && ';
|
||||
}
|
||||
|
||||
$queueForUsage
|
||||
->addMetric(METRIC_BUILDS, 1) // per project
|
||||
->addMetric(METRIC_BUILDS_STORAGE, $build->getAttribute('size', 0))
|
||||
->addMetric(METRIC_BUILDS_COMPUTE, (int)$build->getAttribute('duration', 0) * 1000)
|
||||
->addMetric(METRIC_BUILDS_MB_SECONDS, (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS), 1) // per function
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE), $build->getAttribute('size', 0))
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE), (int)$build->getAttribute('duration', 0) * 1000)
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
$command .= $buildCommand;
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -736,7 +909,7 @@ class Builds extends Action
|
||||
* @param string $owner
|
||||
* @param string $repositoryName
|
||||
* @param Document $project
|
||||
* @param Document $function
|
||||
* @param Document $resource
|
||||
* @param string $deploymentId
|
||||
* @param Database $dbForProject
|
||||
* @param Database $dbForConsole
|
||||
@@ -747,9 +920,9 @@ class Builds extends Action
|
||||
* @throws Conflict
|
||||
* @throws Restricted
|
||||
*/
|
||||
protected function runGitAction(string $status, GitHub $github, string $providerCommitHash, string $owner, string $repositoryName, Document $project, Document $function, string $deploymentId, Database $dbForProject, Database $dbForConsole): void
|
||||
protected function runGitAction(string $status, GitHub $github, string $providerCommitHash, string $owner, string $repositoryName, Document $project, Document $resource, string $deploymentId, Database $dbForProject, Database $dbForConsole): void
|
||||
{
|
||||
if ($function->getAttribute('providerSilentMode', false) === true) {
|
||||
if ($resource->getAttribute('providerSilentMode', false) === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -771,16 +944,14 @@ class Builds extends Action
|
||||
default => $status
|
||||
};
|
||||
|
||||
$functionName = $function->getAttribute('name');
|
||||
$resourceName = $resource->getAttribute('name');
|
||||
$projectName = $project->getAttribute('name');
|
||||
|
||||
$name = "{$functionName} ({$projectName})";
|
||||
$name = "{$resourceName} ({$projectName})";
|
||||
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$hostname = System::getEnv('_APP_DOMAIN');
|
||||
$functionId = $function->getId();
|
||||
$projectId = $project->getId();
|
||||
$providerTargetUrl = $protocol . '://' . $hostname . "/console/project-$projectId/functions/function-$functionId";
|
||||
$providerTargetUrl = "{$protocol}://{$hostname}/console/project-{$project->getId()}/functions/function-{$resource->getId()}";
|
||||
|
||||
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, $state, $message, $providerTargetUrl, $name);
|
||||
}
|
||||
@@ -807,9 +978,34 @@ class Builds extends Action
|
||||
|
||||
// Wrap in try/finally to ensure lock file gets deleted
|
||||
try {
|
||||
$resourceType = match($resource->getCollection()) {
|
||||
'functions' => 'function',
|
||||
'sites' => 'site',
|
||||
default => throw new \Exception('Invalid resource type')
|
||||
};
|
||||
|
||||
$rule = Authorization::skip(fn () => $dbForConsole->findOne('rules', [
|
||||
Query::equal("projectInternalId", [$project->getInternalId()]),
|
||||
Query::equal("resourceType", ["deployment"]),
|
||||
Query::equal("resourceInternalId", [$deployment->getInternalId()])
|
||||
]));
|
||||
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$previewUrl = match($resource->getCollection()) {
|
||||
'functions' => '',
|
||||
'sites' => !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '',
|
||||
default => throw new \Exception('Invalid resource type')
|
||||
};
|
||||
|
||||
$previweQrCode = match($resource->getCollection()) {
|
||||
'functions' => '',
|
||||
'sites' => 'https://cloud.appwrite.io/v1/avatars/qr?text=' . $previewUrl,
|
||||
default => throw new \Exception('Invalid resource type')
|
||||
};
|
||||
|
||||
$comment = new Comment();
|
||||
$comment->parseComment($github->getComment($owner, $repositoryName, $commentId));
|
||||
$comment->addBuild($project, $function, $status, $deployment->getId(), ['type' => 'logs']);
|
||||
$comment->addBuild($project, $resource, $resourceType, $status, $deployment->getId(), ['type' => 'logs'], $previewUrl, $previweQrCode);
|
||||
$github->updateComment($owner, $repositoryName, $commentId, $comment->generateComment());
|
||||
} finally {
|
||||
$dbForConsole->deleteDocument('vcsCommentLocks', $commentId);
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Executor\Executor;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class CancelDeployment extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'cancelDeployment';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/build')
|
||||
->desc('Cancel deployment')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.write')
|
||||
->label('audits.event', 'deployment.update')
|
||||
->label('audits.resource', 'site/{request.siteId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'updateDeploymentBuild')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_BUILD)
|
||||
->param('siteId', '', new UID(), 'Site ID.')
|
||||
->param('deploymentId', '', new UID(), 'Deployment ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('queueForEvents')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
|
||||
if ($deployment->isEmpty()) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
|
||||
|
||||
if ($build->isEmpty()) {
|
||||
$buildId = ID::unique();
|
||||
$build = $dbForProject->createDocument('builds', new Document([
|
||||
'$id' => $buildId,
|
||||
'$permissions' => [],
|
||||
'startTime' => DateTime::now(),
|
||||
'deploymentInternalId' => $deployment->getInternalId(),
|
||||
'deploymentId' => $deployment->getId(),
|
||||
'status' => 'canceled',
|
||||
'path' => '',
|
||||
'runtime' => $site->getAttribute('framework'),
|
||||
'source' => $deployment->getAttribute('path', ''),
|
||||
'sourceType' => '',
|
||||
'logs' => '',
|
||||
'duration' => 0,
|
||||
'size' => 0
|
||||
]));
|
||||
|
||||
$deployment->setAttribute('buildId', $build->getId());
|
||||
$deployment->setAttribute('buildInternalId', $build->getInternalId());
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
} else {
|
||||
if (\in_array($build->getAttribute('status'), ['ready', 'failed'])) {
|
||||
throw new Exception(Exception::BUILD_ALREADY_COMPLETED);
|
||||
}
|
||||
|
||||
$startTime = new \DateTime($build->getAttribute('startTime'));
|
||||
$endTime = new \DateTime('now');
|
||||
$duration = $endTime->getTimestamp() - $startTime->getTimestamp();
|
||||
|
||||
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttributes([
|
||||
'endTime' => DateTime::now(),
|
||||
'duration' => $duration,
|
||||
'status' => 'canceled'
|
||||
]));
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('deployments', $deployment->getId());
|
||||
|
||||
try {
|
||||
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
|
||||
$executor->deleteRuntime($project->getId(), $deploymentId . "-build");
|
||||
} catch (\Throwable $th) {
|
||||
// Don't throw if the deployment doesn't exist
|
||||
if ($th->getCode() !== 404) {
|
||||
throw $th;
|
||||
}
|
||||
}
|
||||
|
||||
$queueForEvents
|
||||
->setParam('siteId', $site->getId())
|
||||
->setParam('deploymentId', $deployment->getId());
|
||||
|
||||
$response->dynamic($build, Response::MODEL_BUILD);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
|
||||
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Storage\Validator\File;
|
||||
use Utopia\Storage\Validator\FileExt;
|
||||
use Utopia\Storage\Validator\FileSize;
|
||||
use Utopia\Storage\Validator\Upload;
|
||||
use Utopia\Swoole\Request;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class CreateDeployment extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'createDeployment';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/sites/:siteId/deployments')
|
||||
->desc('Create deployment')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.write')
|
||||
->label('event', 'sites.[siteId].deployments.[deploymentId].create')
|
||||
->label('audits.event', 'deployment.create')
|
||||
->label('audits.resource', 'site/{request.siteId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'createDeployment')
|
||||
->label('sdk.methodType', 'upload')
|
||||
->label('sdk.description', '/docs/references/sites/create-deployment.md') //TODO: Create new docs
|
||||
->label('sdk.packaging', true)
|
||||
->label('sdk.request.type', 'multipart/form-data')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_DEPLOYMENT)
|
||||
->param('siteId', '', new UID(), 'Site ID.')
|
||||
->param('installCommand', null, new Text(8192, 0), 'Install Commands.', true)
|
||||
->param('buildCommand', null, new Text(8192, 0), 'Build Commands.', true)
|
||||
->param('outputDirectory', null, new Text(8192, 0), 'Output Directory.', true)
|
||||
->param('code', [], new File(), 'Gzip file with your code package. When used with the Appwrite CLI, pass the path to your code directory, and the CLI will automatically package your code. Use a path that is within the current directory.', skipValidation: true)
|
||||
->param('activate', false, new Boolean(true), 'Automatically activate the deployment when it is finished building.')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForConsole')
|
||||
->inject('project')
|
||||
->inject('queueForEvents')
|
||||
->inject('deviceForSites')
|
||||
->inject('deviceForFunctions') // TODO: Remove this later once volume is added to executor
|
||||
->inject('deviceForLocal')
|
||||
->inject('queueForBuilds')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, ?string $installCommand, ?string $buildCommand, ?string $outputDirectory, mixed $code, mixed $activate, Request $request, Response $response, Database $dbForProject, Database $dbForConsole, Document $project, Event $queueForEvents, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForLocal, Build $queueForBuilds)
|
||||
{
|
||||
$activate = \strval($activate) === 'true' || \strval($activate) === '1';
|
||||
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($installCommand === null) {
|
||||
$installCommand = $site->getAttribute('installCommand', '');
|
||||
}
|
||||
|
||||
if ($buildCommand === null) {
|
||||
$buildCommand = $site->getAttribute('buildCommand', '');
|
||||
}
|
||||
|
||||
if ($outputDirectory === null) {
|
||||
$outputDirectory = $site->getAttribute('outputDirectory', '');
|
||||
}
|
||||
|
||||
$file = $request->getFiles('code');
|
||||
|
||||
// GraphQL multipart spec adds files with index keys
|
||||
if (empty($file)) {
|
||||
$file = $request->getFiles(0);
|
||||
}
|
||||
|
||||
if (empty($file)) {
|
||||
throw new Exception(Exception::STORAGE_FILE_EMPTY, 'No file sent');
|
||||
}
|
||||
|
||||
$fileExt = new FileExt([FileExt::TYPE_GZIP]);
|
||||
$fileSizeValidator = new FileSize(System::getEnv('_APP_COMPUTE_SIZE_LIMIT', '30000000'));
|
||||
$upload = new Upload();
|
||||
|
||||
// Make sure we handle a single file and multiple files the same way
|
||||
$fileName = (\is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name'];
|
||||
$fileTmpName = (\is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name'];
|
||||
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
|
||||
|
||||
if (!$fileExt->isValid($file['name'])) { // Check if file type is allowed
|
||||
throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED);
|
||||
}
|
||||
|
||||
$contentRange = $request->getHeader('content-range');
|
||||
$deploymentId = ID::unique();
|
||||
$chunk = 1;
|
||||
$chunks = 1;
|
||||
|
||||
if (!empty($contentRange)) {
|
||||
$start = $request->getContentRangeStart();
|
||||
$end = $request->getContentRangeEnd();
|
||||
$fileSize = $request->getContentRangeSize();
|
||||
$deploymentId = $request->getHeader('x-appwrite-id', $deploymentId);
|
||||
// TODO make `end >= $fileSize` in next breaking version
|
||||
if (is_null($start) || is_null($end) || is_null($fileSize) || $end > $fileSize) {
|
||||
throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE);
|
||||
}
|
||||
|
||||
// TODO remove the condition that checks `$end === $fileSize` in next breaking version
|
||||
if ($end === $fileSize - 1 || $end === $fileSize) {
|
||||
//if it's a last chunks the chunk size might differ, so we set the $chunks and $chunk to notify it's last chunk
|
||||
$chunks = $chunk = -1;
|
||||
} else {
|
||||
// Calculate total number of chunks based on the chunk size i.e ($rangeEnd - $rangeStart)
|
||||
$chunks = (int) ceil($fileSize / ($end + 1 - $start));
|
||||
$chunk = (int) ($start / ($end + 1 - $start)) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$fileSizeValidator->isValid($fileSize)) { // Check if file size is exceeding allowed limit
|
||||
throw new Exception(Exception::STORAGE_INVALID_FILE_SIZE);
|
||||
}
|
||||
|
||||
if (!$upload->isValid($fileTmpName)) {
|
||||
throw new Exception(Exception::STORAGE_INVALID_FILE);
|
||||
}
|
||||
|
||||
// Save to storage
|
||||
$fileSize ??= $deviceForLocal->getFileSize($fileTmpName);
|
||||
$path = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION));
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
|
||||
$metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)];
|
||||
if (!$deployment->isEmpty()) {
|
||||
$chunks = $deployment->getAttribute('chunksTotal', 1);
|
||||
$metadata = $deployment->getAttribute('metadata', []);
|
||||
if ($chunk === -1) {
|
||||
$chunk = $chunks;
|
||||
}
|
||||
}
|
||||
|
||||
$chunksUploaded = $deviceForFunctions->upload($fileTmpName, $path, $chunk, $chunks, $metadata);
|
||||
|
||||
if (empty($chunksUploaded)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed moving file');
|
||||
}
|
||||
|
||||
$type = $request->getHeader('x-sdk-language') === 'cli' ? 'cli' : 'manual';
|
||||
|
||||
if ($chunksUploaded === $chunks) {
|
||||
if ($activate) {
|
||||
// Remove deploy for all other deployments.
|
||||
$activeDeployments = $dbForProject->find('deployments', [
|
||||
Query::equal('activate', [true]),
|
||||
Query::equal('resourceId', [$siteId]),
|
||||
Query::equal('resourceType', ['sites'])
|
||||
]);
|
||||
|
||||
foreach ($activeDeployments as $activeDeployment) {
|
||||
$activeDeployment->setAttribute('activate', false);
|
||||
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), $activeDeployment);
|
||||
}
|
||||
}
|
||||
|
||||
$fileSize = $deviceForFunctions->getFileSize($path);
|
||||
|
||||
if ($deployment->isEmpty()) {
|
||||
$deployment = $dbForProject->createDocument('deployments', new Document([
|
||||
'$id' => $deploymentId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceInternalId' => $site->getInternalId(),
|
||||
'resourceId' => $site->getId(),
|
||||
'resourceType' => 'sites',
|
||||
'buildInternalId' => '',
|
||||
'installCommand' => $installCommand,
|
||||
'buildCommand' => $buildCommand,
|
||||
'outputDirectory' => $outputDirectory,
|
||||
'path' => $path,
|
||||
'size' => $fileSize,
|
||||
'search' => implode(' ', [$deploymentId]),
|
||||
'activate' => $activate,
|
||||
'metadata' => $metadata,
|
||||
'type' => $type
|
||||
]));
|
||||
|
||||
// Preview deployments for sites
|
||||
$projectId = $project->getId();
|
||||
|
||||
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
|
||||
$domain = "{$deploymentId}-{$projectId}.{$sitesDomain}";
|
||||
$ruleId = md5($domain);
|
||||
|
||||
$rule = Authorization::skip(
|
||||
fn () => $dbForConsole->createDocument('rules', new Document([
|
||||
'$id' => $ruleId,
|
||||
'projectId' => $project->getId(),
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'domain' => $domain,
|
||||
'resourceType' => 'deployment',
|
||||
'resourceId' => $deploymentId,
|
||||
'resourceInternalId' => $deployment->getInternalId(),
|
||||
'status' => 'verified',
|
||||
'certificateId' => '',
|
||||
]))
|
||||
);
|
||||
} else {
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('size', $fileSize)->setAttribute('metadata', $metadata));
|
||||
}
|
||||
|
||||
// Start the build
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
->setResource($site)
|
||||
->setDeployment($deployment);
|
||||
} else {
|
||||
if ($deployment->isEmpty()) {
|
||||
$deployment = $dbForProject->createDocument('deployments', new Document([
|
||||
'$id' => $deploymentId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceInternalId' => $site->getInternalId(),
|
||||
'resourceId' => $site->getId(),
|
||||
'resourceType' => 'sites',
|
||||
'buildInternalId' => '',
|
||||
'installCommand' => $installCommand,
|
||||
'buildCommand' => $buildCommand,
|
||||
'outputDirectory' => $outputDirectory,
|
||||
'path' => $path,
|
||||
'size' => $fileSize,
|
||||
'chunksTotal' => $chunks,
|
||||
'chunksUploaded' => $chunksUploaded,
|
||||
'search' => implode(' ', [$deploymentId]),
|
||||
'activate' => $activate,
|
||||
'metadata' => $metadata,
|
||||
'type' => $type
|
||||
]));
|
||||
|
||||
// Preview deployments for sites
|
||||
$projectId = $project->getId();
|
||||
|
||||
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
|
||||
$domain = "{$deploymentId}-{$projectId}.{$sitesDomain}";
|
||||
$ruleId = md5($domain);
|
||||
|
||||
$rule = Authorization::skip(
|
||||
fn () => $dbForConsole->createDocument('rules', new Document([
|
||||
'$id' => $ruleId,
|
||||
'projectId' => $project->getId(),
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'domain' => $domain,
|
||||
'resourceType' => 'deployment',
|
||||
'resourceId' => $deploymentId,
|
||||
'resourceInternalId' => $deployment->getInternalId(),
|
||||
'status' => 'verified',
|
||||
'certificateId' => '',
|
||||
]))
|
||||
);
|
||||
} else {
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('chunksUploaded', $chunksUploaded)->setAttribute('metadata', $metadata));
|
||||
}
|
||||
}
|
||||
|
||||
$metadata = null;
|
||||
|
||||
$queueForEvents
|
||||
->setParam('siteId', $site->getId())
|
||||
->setParam('deploymentId', $deployment->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
|
||||
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
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;
|
||||
use Utopia\Storage\Device;
|
||||
|
||||
class DeleteDeployment extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'deleteDeployment';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
|
||||
->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId')
|
||||
->desc('Delete deployment')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.write')
|
||||
->label('event', 'sites.[siteId].deployments.[deploymentId].delete')
|
||||
->label('audits.event', 'deployment.delete')
|
||||
->label('audits.resource', 'site/{request.siteId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'deleteDeployment')
|
||||
->label('sdk.description', '/docs/references/sites/delete-deployment.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('siteId', '', new UID(), 'Site ID.')
|
||||
->param('deploymentId', '', new UID(), 'Deployment ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForDeletes')
|
||||
->inject('queueForEvents')
|
||||
->inject('deviceForSites')
|
||||
->inject('deviceForFunctions') //TODO: remove it later
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Device $deviceForSites, Device $deviceForFunctions)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
if ($deployment->isEmpty()) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($deployment->getAttribute('resourceId') !== $site->getId()) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$dbForProject->deleteDocument('deployments', $deployment->getId())) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from DB');
|
||||
}
|
||||
|
||||
if (!empty($deployment->getAttribute('path', ''))) {
|
||||
if (!($deviceForFunctions->delete($deployment->getAttribute('path', '')))) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from storage');
|
||||
}
|
||||
}
|
||||
|
||||
if ($site->getAttribute('deployment') === $deployment->getId()) { // Reset site deployment
|
||||
$site = $dbForProject->updateDocument('sites', $site->getId(), new Document(array_merge($site->getArrayCopy(), [
|
||||
'deployment' => '',
|
||||
'deploymentInternalId' => '',
|
||||
])));
|
||||
}
|
||||
|
||||
$queueForEvents
|
||||
->setParam('siteId', $site->getId())
|
||||
->setParam('deploymentId', $deployment->getId());
|
||||
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($deployment);
|
||||
|
||||
$response->noContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Swoole\Request;
|
||||
|
||||
class DownloadBuild extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'downloadBuild';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/build/download')
|
||||
->desc('Download build')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'getBuildDownload')
|
||||
->label('sdk.description', '/docs/references/sites/get-build-download.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', '*/*')
|
||||
->label('sdk.methodType', 'location')
|
||||
->param('siteId', '', new UID(), 'Site ID.')
|
||||
->param('deploymentId', '', new UID(), 'Deployment ID.')
|
||||
->inject('response')
|
||||
->inject('request')
|
||||
->inject('dbForProject')
|
||||
->inject('deviceForBuilds')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForBuilds)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
if ($deployment->isEmpty()) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($deployment->getAttribute('resourceId') !== $site->getId()) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId'));
|
||||
if ($build->isEmpty()) {
|
||||
throw new Exception(Exception::BUILD_NOT_FOUND);
|
||||
}
|
||||
|
||||
$path = $build->getAttribute('path', '');
|
||||
if (!$deviceForBuilds->exists($path)) {
|
||||
throw new Exception(Exception::BUILD_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response
|
||||
->setContentType('application/gzip')
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
|
||||
->addHeader('X-Peak', \memory_get_peak_usage())
|
||||
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"');
|
||||
|
||||
$size = $deviceForBuilds->getFileSize($path);
|
||||
$rangeHeader = $request->getHeader('range');
|
||||
|
||||
if (!empty($rangeHeader)) {
|
||||
$start = $request->getRangeStart();
|
||||
$end = $request->getRangeEnd();
|
||||
$unit = $request->getRangeUnit();
|
||||
|
||||
if ($end === null) {
|
||||
$end = min(($start + MAX_OUTPUT_CHUNK_SIZE - 1), ($size - 1));
|
||||
}
|
||||
|
||||
if ($unit !== 'bytes' || $start >= $end || $end >= $size) {
|
||||
throw new Exception(Exception::STORAGE_INVALID_RANGE);
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Accept-Ranges', 'bytes')
|
||||
->addHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $size)
|
||||
->addHeader('Content-Length', $end - $start + 1)
|
||||
->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT);
|
||||
|
||||
$response->send($deviceForBuilds->read($path, $start, ($end - $start + 1)));
|
||||
}
|
||||
|
||||
if ($size > APP_STORAGE_READ_BUFFER) {
|
||||
for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) {
|
||||
$response->chunk(
|
||||
$deviceForBuilds->read(
|
||||
$path,
|
||||
($i * MAX_OUTPUT_CHUNK_SIZE),
|
||||
min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE))
|
||||
),
|
||||
(($i + 1) * MAX_OUTPUT_CHUNK_SIZE) >= $size
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$response->send($deviceForBuilds->read($path));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\Swoole\Request;
|
||||
|
||||
class DownloadDeployment extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'downloadDeployment';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/download')
|
||||
->desc('Download deployment')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'getDeploymentDownload')
|
||||
->label('sdk.description', '/docs/references/sites/get-deployment-download.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', '*/*')
|
||||
->label('sdk.methodType', 'location')
|
||||
->param('siteId', '', new UID(), 'Site ID.')
|
||||
->param('deploymentId', '', new UID(), 'Deployment ID.')
|
||||
->inject('response')
|
||||
->inject('request')
|
||||
->inject('dbForProject')
|
||||
->inject('deviceForSites')
|
||||
->inject('deviceForFunctions') //TODO: Remove this later
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForSites, Device $deviceForFunctions)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
if ($deployment->isEmpty()) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($deployment->getAttribute('resourceId') !== $site->getId()) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$path = $deployment->getAttribute('path', '');
|
||||
if (!$deviceForFunctions->exists($path)) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response
|
||||
->setContentType('application/gzip')
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
|
||||
->addHeader('X-Peak', \memory_get_peak_usage())
|
||||
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"');
|
||||
|
||||
$size = $deviceForFunctions->getFileSize($path);
|
||||
$rangeHeader = $request->getHeader('range');
|
||||
|
||||
if (!empty($rangeHeader)) {
|
||||
$start = $request->getRangeStart();
|
||||
$end = $request->getRangeEnd();
|
||||
$unit = $request->getRangeUnit();
|
||||
|
||||
if ($end === null) {
|
||||
$end = min(($start + MAX_OUTPUT_CHUNK_SIZE - 1), ($size - 1));
|
||||
}
|
||||
|
||||
if ($unit !== 'bytes' || $start >= $end || $end >= $size) {
|
||||
throw new Exception(Exception::STORAGE_INVALID_RANGE);
|
||||
}
|
||||
|
||||
$response
|
||||
->addHeader('Accept-Ranges', 'bytes')
|
||||
->addHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $size)
|
||||
->addHeader('Content-Length', $end - $start + 1)
|
||||
->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT);
|
||||
|
||||
$response->send($deviceForFunctions->read($path, $start, ($end - $start + 1)));
|
||||
}
|
||||
|
||||
if ($size > APP_STORAGE_READ_BUFFER) {
|
||||
for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) {
|
||||
$response->chunk(
|
||||
$deviceForFunctions->read(
|
||||
$path,
|
||||
($i * MAX_OUTPUT_CHUNK_SIZE),
|
||||
min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE))
|
||||
),
|
||||
(($i + 1) * MAX_OUTPUT_CHUNK_SIZE) >= $size
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$response->send($deviceForFunctions->read($path));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class GetDeployment extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'getDeployment';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId')
|
||||
->desc('Get deployment')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'getDeployment')
|
||||
->label('sdk.description', '/docs/references/sites/get-deployment.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_DEPLOYMENT)
|
||||
->param('siteId', '', new UID(), 'Site ID.')
|
||||
->param('deploymentId', '', new UID(), 'Deployment ID.')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForConsole')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, string $deploymentId, Response $response, Document $project, Database $dbForProject, Database $dbForConsole)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
|
||||
if ($deployment->getAttribute('resourceId') !== $site->getId()) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($deployment->isEmpty()) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''));
|
||||
$deployment->setAttribute('status', $build->getAttribute('status', 'waiting'));
|
||||
$deployment->setAttribute('buildLogs', $build->getAttribute('logs', ''));
|
||||
$deployment->setAttribute('buildTime', $build->getAttribute('duration', 0));
|
||||
$deployment->setAttribute('buildSize', $build->getAttribute('size', 0));
|
||||
$deployment->setAttribute('size', $deployment->getAttribute('size', 0));
|
||||
|
||||
$rule = Authorization::skip(fn () => $dbForConsole->findOne('rules', [
|
||||
Query::equal("projectInternalId", [$project->getInternalId()]),
|
||||
Query::equal("resourceType", ["deployment"]),
|
||||
Query::equal("resourceInternalId", [$deployment->getInternalId()])
|
||||
]));
|
||||
|
||||
if (!empty($rule)) {
|
||||
$deployment->setAttribute('domain', $rule->getAttribute('domain', ''));
|
||||
}
|
||||
|
||||
$response->dynamic($deployment, Response::MODEL_DEPLOYMENT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Deployments;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class ListDeployments extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'listDeployments';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/sites/:siteId/deployments')
|
||||
->desc('List deployments')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'listDeployments')
|
||||
->label('sdk.description', '/docs/references/sites/list-deployments.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_DEPLOYMENT_LIST)
|
||||
->param('siteId', '', new UID(), 'Site ID.')
|
||||
->param('queries', [], new Deployments(), '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. You may filter on the following attributes: ' . implode(', ', Deployments::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForConsole')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, array $queries, string $search, Response $response, Document $project, Database $dbForProject, Database $dbForConsole)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
// Set resource queries
|
||||
$queries[] = Query::equal('resourceInternalId', [$site->getInternalId()]);
|
||||
$queries[] = Query::equal('resourceType', ['sites']);
|
||||
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$deploymentId = $cursor->getValue();
|
||||
$cursorDocument = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Deployment '{$deploymentId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
$results = $dbForProject->find('deployments', $queries);
|
||||
$total = $dbForProject->count('deployments', $filterQueries, APP_LIMIT_COUNT);
|
||||
|
||||
foreach ($results as $result) {
|
||||
$build = $dbForProject->getDocument('builds', $result->getAttribute('buildId', ''));
|
||||
$result->setAttribute('status', $build->getAttribute('status', 'processing'));
|
||||
$result->setAttribute('buildLogs', $build->getAttribute('logs', ''));
|
||||
$result->setAttribute('buildTime', $build->getAttribute('duration', 0));
|
||||
$result->setAttribute('buildSize', $build->getAttribute('size', 0));
|
||||
$result->setAttribute('size', $result->getAttribute('size', 0));
|
||||
|
||||
$rule = Authorization::skip(fn () => $dbForConsole->findOne('rules', [
|
||||
Query::equal("projectInternalId", [$project->getInternalId()]),
|
||||
Query::equal("resourceType", ["deployment"]),
|
||||
Query::equal("resourceInternalId", [$result->getInternalId()])
|
||||
]));
|
||||
|
||||
if (!empty($rule)) {
|
||||
$result->setAttribute('domain', $rule->getAttribute('domain', ''));
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'deployments' => $results,
|
||||
'total' => $total,
|
||||
]), Response::MODEL_DEPLOYMENT_LIST);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
|
||||
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Storage\Device;
|
||||
|
||||
class RebuildDeployment extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'rebuildDeployment';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/build')
|
||||
->desc('Rebuild deployment')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.write')
|
||||
->label('event', 'sites.[siteId].deployments.[deploymentId].update')
|
||||
->label('audits.event', 'deployment.update')
|
||||
->label('audits.resource', 'site/{request.siteId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'createBuild')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('siteId', '', new UID(), 'Site ID.')
|
||||
->param('deploymentId', '', new UID(), 'Deployment ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForBuilds')
|
||||
->inject('deviceForSites')
|
||||
->inject('deviceForFunctions') //TODO: remove it later
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Build $queueForBuilds, Device $deviceForSites, Device $deviceForFunctions)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
|
||||
if ($deployment->isEmpty()) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$path = $deployment->getAttribute('path');
|
||||
if (empty($path) || !$deviceForFunctions->exists($path)) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$deploymentId = ID::unique();
|
||||
|
||||
$destination = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
|
||||
$deviceForFunctions->transfer($path, $destination, $deviceForFunctions);
|
||||
|
||||
$deployment->removeAttribute('$internalId');
|
||||
$deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([
|
||||
'$internalId' => '',
|
||||
'$id' => $deploymentId,
|
||||
'buildId' => '',
|
||||
'buildInternalId' => '',
|
||||
'path' => $destination,
|
||||
'buildCommand' => $site->getAttribute('buildCommand', ''),
|
||||
'installCommand' => $site->getAttribute('installCommand', ''),
|
||||
'outputDirectory' => $site->getAttribute('outputDirectory', ''),
|
||||
'search' => implode(' ', [$deploymentId]),
|
||||
]));
|
||||
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
->setResource($site)
|
||||
->setDeployment($deployment);
|
||||
|
||||
$queueForEvents
|
||||
->setParam('siteId', $site->getId())
|
||||
->setParam('deploymentId', $deployment->getId());
|
||||
|
||||
$response->noContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
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;
|
||||
|
||||
class UpdateDeployment extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateDeployment';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId')
|
||||
->desc('Update deployment')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.write')
|
||||
->label('event', 'sites.[siteId].deployments.[deploymentId].update')
|
||||
->label('audits.event', 'deployment.update')
|
||||
->label('audits.resource', 'site/{request.siteId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'updateDeployment')
|
||||
->label('sdk.description', '/docs/references/sites/update-site-deployment.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_SITE)
|
||||
->param('siteId', '', new UID(), 'Site ID.')
|
||||
->param('deploymentId', '', new UID(), 'Deployment ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('dbForConsole')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForConsole)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''));
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($deployment->isEmpty()) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($build->isEmpty()) {
|
||||
throw new Exception(Exception::BUILD_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($build->getAttribute('status') !== 'ready') {
|
||||
throw new Exception(Exception::BUILD_NOT_READY);
|
||||
}
|
||||
|
||||
$site = $dbForProject->updateDocument('sites', $site->getId(), new Document(array_merge($site->getArrayCopy(), [
|
||||
'deploymentInternalId' => $deployment->getInternalId(),
|
||||
'deploymentId' => $deployment->getId(),
|
||||
])));
|
||||
|
||||
$queueForEvents
|
||||
->setParam('siteId', $site->getId())
|
||||
->setParam('deploymentId', $deployment->getId());
|
||||
|
||||
$response->dynamic($site, Response::MODEL_SITE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
|
||||
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Sites\Validator\FrameworkSpecification;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model\Rule;
|
||||
use Utopia\App;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Swoole\Request;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\VCS\Adapter\Git\GitHub;
|
||||
|
||||
class CreateSite extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'createSite';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/sites')
|
||||
->desc('Create site')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.write')
|
||||
->label('event', 'sites.[siteId].create')
|
||||
->label('audits.event', 'site.create')
|
||||
->label('audits.resource', 'site/{response.$id}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'create')
|
||||
->label('sdk.description', '/docs/references/sites/create-site.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_SITE)
|
||||
->param('siteId', '', new CustomId(), 'Site ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
|
||||
->param('name', '', new Text(128), 'Site name. Max length: 128 chars.')
|
||||
->param('framework', '', new WhiteList(array_keys(Config::getParam('frameworks')), true), 'Sites framework.')
|
||||
->param('enabled', true, new Boolean(), 'Is site enabled? When set to \'disabled\', users cannot access the site but Server SDKs with and API key can still access the site. No data is lost when this is toggled.', true) // TODO: Add logging param later
|
||||
->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_COMPUTE_TIMEOUT', 900)), '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('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true)
|
||||
->param('subdomain', '', new CustomId(), 'Unique custom sub-domain. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true)
|
||||
->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.')
|
||||
->param('serveRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use when serving site.')
|
||||
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true)
|
||||
->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true)
|
||||
->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the site.', true)
|
||||
->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('templateRepository', '', new Text(128, 0), 'Repository name of the template.', true)
|
||||
->param('templateOwner', '', new Text(128, 0), 'The name of the owner of the template.', true)
|
||||
->param('templateRootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.', true)
|
||||
->param('templateVersion', '', new Text(128, 0), 'Version (tag) for the repo linked to the site template.', true)
|
||||
->param('specification', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new FrameworkSpecification(
|
||||
$plan,
|
||||
Config::getParam('framework-specifications', []),
|
||||
App::getEnv('_APP_COMPUTE_CPUS', APP_COMPUTE_CPUS_DEFAULT),
|
||||
App::getEnv('_APP_COMPUTE_MEMORY', APP_COMPUTE_MEMORY_DEFAULT)
|
||||
), 'Framework specification for the site and builds.', true, ['plan'])
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForBuilds')
|
||||
->inject('dbForConsole')
|
||||
->inject('gitHub')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, string $name, string $framework, bool $enabled, int $timeout, string $installCommand, string $buildCommand, string $outputDirectory, string $subdomain, string $buildRuntime, string $serveRuntime, string $installationId, ?string $fallbackFile, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github)
|
||||
{
|
||||
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
|
||||
$routeSubdomain = '';
|
||||
$domain = '';
|
||||
|
||||
if (!empty($sitesDomain)) {
|
||||
$routeSubdomain = $subdomain ?: ID::unique();
|
||||
$domain = "{$routeSubdomain}.{$sitesDomain}";
|
||||
|
||||
$subdomain = Authorization::skip(fn () => $dbForConsole->getDocument('rules', \md5($domain)));
|
||||
|
||||
if ($subdomain && !$subdomain->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Subdomain already exists. Please choose a different subdomain.');
|
||||
}
|
||||
}
|
||||
|
||||
$siteId = ($siteId == 'unique()') ? ID::unique() : $siteId;
|
||||
|
||||
$allowList = \array_filter(\explode(',', System::getEnv('_APP_SITES_FRAMEWORKS', '')));
|
||||
|
||||
if (!empty($allowList) && !\in_array($framework, $allowList)) {
|
||||
throw new Exception(Exception::SITE_FRAMEWORK_UNSUPPORTED, 'Framework "' . $framework . '" is not supported');
|
||||
}
|
||||
|
||||
// build from template
|
||||
$template = new Document([]);
|
||||
if (
|
||||
!empty($templateRepository)
|
||||
&& !empty($templateOwner)
|
||||
&& !empty($templateRootDirectory)
|
||||
&& !empty($templateVersion)
|
||||
) {
|
||||
$template->setAttribute('repositoryName', $templateRepository)
|
||||
->setAttribute('ownerName', $templateOwner)
|
||||
->setAttribute('rootDirectory', $templateRootDirectory)
|
||||
->setAttribute('version', $templateVersion);
|
||||
}
|
||||
|
||||
$installation = $dbForConsole->getDocument('installations', $installationId);
|
||||
|
||||
if (!empty($installationId) && $installation->isEmpty()) {
|
||||
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!empty($providerRepositoryId) && (empty($installationId) || empty($providerBranch))) {
|
||||
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([
|
||||
'$id' => $siteId,
|
||||
'enabled' => $enabled,
|
||||
'live' => true,
|
||||
'name' => $name,
|
||||
'framework' => $framework,
|
||||
'deploymentInternalId' => '',
|
||||
'deploymentId' => '',
|
||||
'timeout' => $timeout,
|
||||
'installCommand' => $installCommand,
|
||||
'buildCommand' => $buildCommand,
|
||||
'outputDirectory' => $outputDirectory,
|
||||
'search' => implode(' ', [$siteId, $name, $framework]),
|
||||
'fallbackFile' => $fallbackFile,
|
||||
'installationId' => $installation->getId(),
|
||||
'installationInternalId' => $installation->getInternalId(),
|
||||
'providerRepositoryId' => $providerRepositoryId,
|
||||
'repositoryId' => '',
|
||||
'repositoryInternalId' => '',
|
||||
'providerBranch' => $providerBranch,
|
||||
'providerRootDirectory' => $providerRootDirectory,
|
||||
'providerSilentMode' => $providerSilentMode,
|
||||
'specification' => $specification,
|
||||
'buildRuntime' => $buildRuntime,
|
||||
'serveRuntime' => $serveRuntime,
|
||||
]));
|
||||
|
||||
// Git connect logic
|
||||
if (!empty($providerRepositoryId)) {
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
|
||||
$repository = $dbForConsole->createDocument('repositories', 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')),
|
||||
],
|
||||
'installationId' => $installation->getId(),
|
||||
'installationInternalId' => $installation->getInternalId(),
|
||||
'projectId' => $project->getId(),
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'providerRepositoryId' => $providerRepositoryId,
|
||||
'resourceId' => $site->getId(),
|
||||
'resourceInternalId' => $site->getInternalId(),
|
||||
'resourceType' => 'site',
|
||||
'providerPullRequestIds' => []
|
||||
]));
|
||||
|
||||
$site->setAttribute('repositoryId', $repository->getId());
|
||||
$site->setAttribute('repositoryInternalId', $repository->getInternalId());
|
||||
}
|
||||
|
||||
$site = $dbForProject->updateDocument('sites', $site->getId(), $site);
|
||||
|
||||
if (!empty($providerRepositoryId)) {
|
||||
// Deploy VCS
|
||||
$this->redeployVcsSite($request, $site, $project, $installation, $dbForProject, $dbForConsole, $queueForBuilds, $template, $github);
|
||||
} elseif (!$template->isEmpty()) {
|
||||
// Deploy non-VCS from template
|
||||
$deploymentId = ID::unique();
|
||||
$deployment = $dbForProject->createDocument('deployments', new Document([
|
||||
'$id' => $deploymentId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceId' => $site->getId(),
|
||||
'resourceInternalId' => $site->getInternalId(),
|
||||
'resourceType' => 'sites',
|
||||
'installCommand' => $site->getAttribute('installCommand', ''),
|
||||
'buildCommand' => $site->getAttribute('buildCommand', ''),
|
||||
'outputDirectory' => $site->getAttribute('outputDirectory', ''),
|
||||
'type' => 'manual',
|
||||
'search' => implode(' ', [$deploymentId]),
|
||||
'activate' => true,
|
||||
]));
|
||||
|
||||
// Preview deployments url
|
||||
$projectId = $project->getId();
|
||||
|
||||
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
|
||||
$previewDomain = "{$deploymentId}-{$projectId}.{$sitesDomain}";
|
||||
|
||||
$rule = Authorization::skip(
|
||||
fn () => $dbForConsole->createDocument('rules', new Document([
|
||||
'$id' => \md5($previewDomain),
|
||||
'projectId' => $project->getId(),
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'domain' => $previewDomain,
|
||||
'resourceType' => 'deployment',
|
||||
'resourceId' => $deploymentId,
|
||||
'resourceInternalId' => $deployment->getInternalId(),
|
||||
'status' => 'verified',
|
||||
'certificateId' => '',
|
||||
]))
|
||||
);
|
||||
|
||||
$queueForBuilds
|
||||
->setType(BUILD_TYPE_DEPLOYMENT)
|
||||
->setResource($site)
|
||||
->setDeployment($deployment)
|
||||
->setTemplate($template);
|
||||
}
|
||||
|
||||
if (!empty($sitesDomain)) {
|
||||
$rule = Authorization::skip(
|
||||
fn () => $dbForConsole->createDocument('rules', new Document([
|
||||
'$id' => \md5($domain),
|
||||
'projectId' => $project->getId(),
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'domain' => $domain,
|
||||
'resourceType' => 'site',
|
||||
'resourceId' => $site->getId(),
|
||||
'resourceInternalId' => $site->getInternalId(),
|
||||
'status' => 'verified',
|
||||
'certificateId' => '',
|
||||
]))
|
||||
);
|
||||
|
||||
/** Trigger Webhook */
|
||||
$ruleModel = new Rule();
|
||||
$ruleCreate =
|
||||
$queueForEvents
|
||||
->setClass(Event::WEBHOOK_CLASS_NAME)
|
||||
->setQueue(Event::WEBHOOK_QUEUE_NAME);
|
||||
|
||||
$ruleCreate
|
||||
->setProject($project)
|
||||
->setEvent('rules.[ruleId].create')
|
||||
->setParam('ruleId', $rule->getId())
|
||||
->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules())))
|
||||
->trigger();
|
||||
|
||||
/** Trigger Sites */
|
||||
$ruleCreate
|
||||
->setClass(Event::SITES_CLASS_NAME)
|
||||
->setQueue(Event::SITES_QUEUE_NAME)
|
||||
->trigger();
|
||||
|
||||
/** Trigger realtime event */
|
||||
$allEvents = Event::generateEvents('rules.[ruleId].create', [
|
||||
'ruleId' => $rule->getId(),
|
||||
]);
|
||||
$target = Realtime::fromPayload(
|
||||
// Pass first, most verbose event pattern
|
||||
event: $allEvents[0],
|
||||
payload: $rule,
|
||||
project: $project
|
||||
);
|
||||
Realtime::send(
|
||||
projectId: 'console',
|
||||
payload: $rule->getArrayCopy(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles']
|
||||
);
|
||||
Realtime::send(
|
||||
projectId: $project->getId(),
|
||||
payload: $rule->getArrayCopy(),
|
||||
events: $allEvents,
|
||||
channels: $target['channels'],
|
||||
roles: $target['roles']
|
||||
);
|
||||
}
|
||||
|
||||
$queueForEvents->setParam('siteId', $site->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($site, Response::MODEL_SITE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
|
||||
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class DeleteSite extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'deleteSite';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
|
||||
->setHttpPath('/v1/sites/:siteId')
|
||||
->desc('Delete site')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.write')
|
||||
->label('event', 'sites.[siteId].delete')
|
||||
->label('audits.event', 'site.delete')
|
||||
->label('audits.resource', 'site/{request.siteId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'delete')
|
||||
->label('sdk.description', '/docs/references/sites/delete-site.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('siteId', '', new UID(), 'Site ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForDeletes')
|
||||
->inject('queueForEvents')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$dbForProject->deleteDocument('sites', $site->getId())) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove site from DB');
|
||||
}
|
||||
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($site);
|
||||
|
||||
$queueForEvents->setParam('siteId', $site->getId());
|
||||
|
||||
$response->noContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class GetSite extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'getSite';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/sites/:siteId')
|
||||
->desc('Get site')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'get')
|
||||
->label('sdk.description', '/docs/references/sites/get-site.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_SITE)
|
||||
->param('siteId', '', new UID(), 'Site ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, Response $response, Database $dbForProject)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($site, Response::MODEL_SITE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class GetSiteUsage extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'getSiteUsage';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/sites/:siteId/usage')
|
||||
->desc('Get site usage')
|
||||
->groups(['api', 'sites', 'usage'])
|
||||
->label('scope', 'sites.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'getSiteUsage')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USAGE_SITE)
|
||||
->param('siteId', '', new UID(), 'Site ID.')
|
||||
->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, string $range, Response $response, Database $dbForProject)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$periods = Config::getParam('usage', []);
|
||||
$stats = $usage = [];
|
||||
$days = $periods[$range];
|
||||
$metrics = [
|
||||
str_replace(['{resourceType}', '{resourceInternalId}'], ['sites', $site->getInternalId()], METRIC_SITE_ID_DEPLOYMENTS),
|
||||
str_replace(['{resourceType}', '{resourceInternalId}'], ['sites', $site->getInternalId()], METRIC_SITE_ID_DEPLOYMENTS_STORAGE),
|
||||
str_replace('{siteInternalId}', $site->getInternalId(), METRIC_SITE_ID_BUILDS),
|
||||
str_replace('{siteInternalId}', $site->getInternalId(), METRIC_SITE_ID_BUILDS_STORAGE),
|
||||
str_replace('{siteInternalId}', $site->getInternalId(), METRIC_SITE_ID_BUILDS_COMPUTE),
|
||||
str_replace('{siteInternalId}', $site->getInternalId(), METRIC_SITE_ID_BUILDS_MB_SECONDS)
|
||||
];
|
||||
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
Query::orderDesc('time'),
|
||||
]);
|
||||
$stats[$metric]['data'] = [];
|
||||
foreach ($results as $result) {
|
||||
$stats[$metric]['data'][$result->getAttribute('time')] = [
|
||||
'value' => $result->getAttribute('value'),
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
$usage[$metric]['total'] = $stats[$metric]['total'];
|
||||
$usage[$metric]['data'] = [];
|
||||
$leap = time() - ($days['limit'] * $days['factor']);
|
||||
while ($leap < time()) {
|
||||
$leap += $days['factor'];
|
||||
$formatDate = date($format, $leap);
|
||||
$usage[$metric]['data'][] = [
|
||||
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
|
||||
'date' => $formatDate,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'range' => $range,
|
||||
'deploymentsTotal' => $usage[$metrics[0]]['total'],
|
||||
'deploymentsStorageTotal' => $usage[$metrics[1]]['total'],
|
||||
'buildsTotal' => $usage[$metrics[2]]['total'],
|
||||
'buildsStorageTotal' => $usage[$metrics[3]]['total'],
|
||||
'buildsTimeTotal' => $usage[$metrics[4]]['total'],
|
||||
'deployments' => $usage[$metrics[0]]['data'],
|
||||
'deploymentsStorage' => $usage[$metrics[1]]['data'],
|
||||
'builds' => $usage[$metrics[2]]['data'],
|
||||
'buildsStorage' => $usage[$metrics[3]]['data'],
|
||||
'buildsTime' => $usage[$metrics[4]]['data'],
|
||||
'buildsMbSecondsTotal' => $usage[$metrics[7]]['total'],
|
||||
'buildsMbSeconds' => $usage[$metrics[7]]['data']
|
||||
// TODO: Add more metrics for requests, bandwidth, etc.
|
||||
]), Response::MODEL_USAGE_SITE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
|
||||
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class GetSitesUsage extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'getSitesUsage';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/sites/usage')
|
||||
->desc('Get sites usage')
|
||||
->groups(['api', 'sites', 'usage'])
|
||||
->label('scope', 'sites.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'getUsage')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_USAGE_SITES)
|
||||
->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $range, Response $response, Database $dbForProject)
|
||||
{
|
||||
$periods = Config::getParam('usage', []);
|
||||
$stats = $usage = [];
|
||||
$days = $periods[$range];
|
||||
$metrics = [
|
||||
METRIC_SITES,
|
||||
METRIC_DEPLOYMENTS,
|
||||
METRIC_DEPLOYMENTS_STORAGE,
|
||||
METRIC_BUILDS,
|
||||
METRIC_BUILDS_STORAGE,
|
||||
METRIC_BUILDS_COMPUTE,
|
||||
METRIC_BUILDS_MB_SECONDS,
|
||||
];
|
||||
|
||||
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
|
||||
foreach ($metrics as $metric) {
|
||||
$result = $dbForProject->findOne('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', ['inf'])
|
||||
]);
|
||||
|
||||
$stats[$metric]['total'] = $result['value'] ?? 0;
|
||||
$limit = $days['limit'];
|
||||
$period = $days['period'];
|
||||
$results = $dbForProject->find('stats', [
|
||||
Query::equal('metric', [$metric]),
|
||||
Query::equal('period', [$period]),
|
||||
Query::limit($limit),
|
||||
Query::orderDesc('time'),
|
||||
]);
|
||||
$stats[$metric]['data'] = [];
|
||||
foreach ($results as $result) {
|
||||
$stats[$metric]['data'][$result->getAttribute('time')] = [
|
||||
'value' => $result->getAttribute('value'),
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$format = match ($days['period']) {
|
||||
'1h' => 'Y-m-d\TH:00:00.000P',
|
||||
'1d' => 'Y-m-d\T00:00:00.000P',
|
||||
};
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
$usage[$metric]['total'] = $stats[$metric]['total'];
|
||||
$usage[$metric]['data'] = [];
|
||||
$leap = time() - ($days['limit'] * $days['factor']);
|
||||
while ($leap < time()) {
|
||||
$leap += $days['factor'];
|
||||
$formatDate = date($format, $leap);
|
||||
$usage[$metric]['data'][] = [
|
||||
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
|
||||
'date' => $formatDate,
|
||||
];
|
||||
}
|
||||
}
|
||||
$response->dynamic(new Document([
|
||||
'range' => $range,
|
||||
'sitesTotal' => $usage[$metrics[0]]['total'],
|
||||
'deploymentsTotal' => $usage[$metrics[1]]['total'],
|
||||
'deploymentsStorageTotal' => $usage[$metrics[2]]['total'],
|
||||
'buildsTotal' => $usage[$metrics[3]]['total'],
|
||||
'buildsStorageTotal' => $usage[$metrics[4]]['total'],
|
||||
'buildsTimeTotal' => $usage[$metrics[5]]['total'],
|
||||
'sites' => $usage[$metrics[0]]['data'],
|
||||
'deployments' => $usage[$metrics[1]]['data'],
|
||||
'deploymentsStorage' => $usage[$metrics[2]]['data'],
|
||||
'builds' => $usage[$metrics[3]]['data'],
|
||||
'buildsStorage' => $usage[$metrics[4]]['data'],
|
||||
'buildsTime' => $usage[$metrics[5]]['data'],
|
||||
'buildsMbSecondsTotal' => $usage[$metrics[8]]['total'],
|
||||
'buildsMbSeconds' => $usage[$metrics[8]]['data']
|
||||
]), Response::MODEL_USAGE_SITES);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class GetTemplate extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'getTemplate';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/sites/templates/:templateId')
|
||||
->desc('Get site template')
|
||||
->groups(['api'])
|
||||
->label('scope', 'public')
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'getTemplate')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.description', '/docs/references/sites/get-template.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TEMPLATE_SITE)
|
||||
->param('templateId', '', new Text(128), 'Template ID.')
|
||||
->inject('response')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $templateId, Response $response)
|
||||
{
|
||||
$templates = Config::getParam('site-templates', []);
|
||||
|
||||
$allowedTemplates = \array_filter($templates, function ($item) use ($templateId) {
|
||||
return $item['key'] === $templateId;
|
||||
});
|
||||
$template = array_shift($allowedTemplates);
|
||||
|
||||
if (empty($template)) {
|
||||
throw new Exception(Exception::SITE_TEMPLATE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic(new Document($template), Response::MODEL_TEMPLATE_SITE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
|
||||
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\System\System;
|
||||
|
||||
class ListFrameworks extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'listFrameworks';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/sites/frameworks')
|
||||
->desc('List frameworks')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'listFrameworks')
|
||||
->label('sdk.description', '/docs/references/sites/list-frameworks.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_FRAMEWORK_LIST)
|
||||
->inject('response')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(Response $response)
|
||||
{
|
||||
$frameworks = Config::getParam('frameworks');
|
||||
|
||||
$allowList = \array_filter(\explode(',', System::getEnv('_APP_SITES_FRAMEWORKS', '')));
|
||||
|
||||
$allowed = [];
|
||||
foreach ($frameworks as $id => $framework) {
|
||||
if (!empty($allowList) && !\in_array($id, $allowList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$allowed[] = $framework;
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'total' => count($allowed),
|
||||
'frameworks' => $allowed
|
||||
]), Response::MODEL_FRAMEWORK_LIST);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Sites;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class ListSites extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'listSites';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/sites')
|
||||
->desc('List sites')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'list')
|
||||
->label('sdk.description', '/docs/references/sites/list-sites.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_SITE_LIST)
|
||||
->param('queries', [], new Sites(), '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. You may filter on the following attributes: ' . implode(', ', Sites::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(array $queries, string $search, Response $response, Database $dbForProject)
|
||||
{
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$siteId = $cursor->getValue();
|
||||
$cursorDocument = $dbForProject->getDocument('sites', $siteId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Site '{$siteId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'sites' => $dbForProject->find('sites', $queries),
|
||||
'total' => $dbForProject->count('sites', $filterQueries, APP_LIMIT_COUNT),
|
||||
]), Response::MODEL_SITE_LIST);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
|
||||
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class ListTemplates extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'listTemplates';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/sites/templates')
|
||||
->desc('List templates')
|
||||
->groups(['api'])
|
||||
->label('scope', 'public')
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'listTemplates')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.description', '/docs/references/sites/list-templates.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TEMPLATE_SITE_LIST)
|
||||
->param('frameworks', [], new ArrayList(new WhiteList(array_keys(Config::getParam('frameworks')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of frameworks allowed for filtering site templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' frameworks are allowed.', true)
|
||||
->param('useCases', [], new ArrayList(new WhiteList(['dev-tools', 'starter', 'databases', 'ai', 'messaging', 'utilities']), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of use cases allowed for filtering site templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' use cases are allowed.', true)
|
||||
->param('limit', 25, new Range(1, 5000), 'Limit the number of templates returned in the response. Default limit is 25, and maximum limit is 5000.', true)
|
||||
->param('offset', 0, new Range(0, 5000), 'Offset the list of returned templates. Maximum offset is 5000.', true)
|
||||
->inject('response')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(array $frameworks, array $usecases, int $limit, int $offset, Response $response)
|
||||
{
|
||||
$templates = Config::getParam('site-templates', []);
|
||||
|
||||
if (!empty($frameworks)) {
|
||||
$templates = \array_filter($templates, function ($template) use ($frameworks) {
|
||||
return \count(\array_intersect($frameworks, \array_column($template['frameworks'], 'name'))) > 0;
|
||||
});
|
||||
}
|
||||
|
||||
if (!empty($usecases)) {
|
||||
$templates = \array_filter($templates, function ($template) use ($usecases) {
|
||||
return \count(\array_intersect($usecases, $template['useCases'])) > 0;
|
||||
});
|
||||
}
|
||||
|
||||
$responseTemplates = \array_slice($templates, $offset, $limit);
|
||||
$response->dynamic(new Document([
|
||||
'templates' => $responseTemplates,
|
||||
'total' => \count($responseTemplates),
|
||||
]), Response::MODEL_TEMPLATE_SITE_LIST);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
|
||||
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Sites\Validator\FrameworkSpecification;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Executor\Executor;
|
||||
use Utopia\App;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Swoole\Request;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\VCS\Adapter\Git\GitHub;
|
||||
|
||||
class UpdateSite extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateSite';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
|
||||
->setHttpPath('/v1/sites/:siteId')
|
||||
->desc('Update site')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.write')
|
||||
->label('event', 'sites.[siteId].update')
|
||||
->label('audits.event', 'sites.update')
|
||||
->label('audits.resource', 'site/{response.$id}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'update')
|
||||
->label('sdk.description', '/docs/references/sites/update-site.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_SITE)
|
||||
->param('siteId', '', new UID(), 'Site ID.')
|
||||
->param('name', '', new Text(128), 'Site name. Max length: 128 chars.')
|
||||
->param('framework', '', new WhiteList(array_keys(Config::getParam('frameworks')), true), 'Sites framework.')
|
||||
->param('enabled', true, new Boolean(), 'Is site enabled? When set to \'disabled\', users cannot access the site but Server SDKs with and API key can still access the site. No data is lost when this is toggled.', true) // TODO: Add logging param later
|
||||
->param('timeout', 15, new Range(1, (int) System::getEnv('_APP_COMPUTE_TIMEOUT', 900)), '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('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('serveRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use when serving site.', true)
|
||||
->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true)
|
||||
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true)
|
||||
->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the site.', true)
|
||||
->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', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new FrameworkSpecification(
|
||||
$plan,
|
||||
Config::getParam('framework-specifications', []),
|
||||
App::getEnv('_APP_COMPUTE_CPUS', APP_COMPUTE_CPUS_DEFAULT),
|
||||
App::getEnv('_APP_COMPUTE_MEMORY', APP_COMPUTE_MEMORY_DEFAULT)
|
||||
), 'Framework specification for the site and builds.', true, ['plan'])
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForBuilds')
|
||||
->inject('dbForConsole')
|
||||
->inject('gitHub')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, string $name, string $framework, bool $enabled, int $timeout, string $installCommand, string $buildCommand, string $outputDirectory, string $buildRuntime, string $serveRuntime, ?string $fallbackFile, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github)
|
||||
{
|
||||
// TODO: If only branch changes, re-deploy
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$installation = $dbForConsole->getDocument('installations', $installationId);
|
||||
|
||||
if (!empty($installationId) && $installation->isEmpty()) {
|
||||
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!empty($providerRepositoryId) && (empty($installationId) || empty($providerBranch))) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".');
|
||||
}
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (empty($framework)) {
|
||||
$framework = $site->getAttribute('framework');
|
||||
}
|
||||
|
||||
$enabled ??= $site->getAttribute('enabled', true);
|
||||
|
||||
$repositoryId = $site->getAttribute('repositoryId', '');
|
||||
$repositoryInternalId = $site->getAttribute('repositoryInternalId', '');
|
||||
|
||||
$isConnected = !empty($site->getAttribute('providerRepositoryId', ''));
|
||||
|
||||
// Git disconnect logic. Disconnecting only when providerRepositoryId is empty, allowing for continue updates without disconnecting git
|
||||
if ($isConnected && ($providerRepositoryId !== null && empty($providerRepositoryId))) {
|
||||
$repositories = $dbForConsole->find('repositories', [
|
||||
Query::equal('projectInternalId', [$project->getInternalId()]),
|
||||
Query::equal('resourceInternalId', [$site->getInternalId()]),
|
||||
Query::equal('resourceType', ['site']),
|
||||
Query::limit(100),
|
||||
]);
|
||||
|
||||
foreach ($repositories as $repository) {
|
||||
$dbForConsole->deleteDocument('repositories', $repository->getId());
|
||||
}
|
||||
|
||||
$providerRepositoryId = '';
|
||||
$installationId = '';
|
||||
$providerBranch = '';
|
||||
$providerRootDirectory = '';
|
||||
$providerSilentMode = true;
|
||||
$repositoryId = '';
|
||||
$repositoryInternalId = '';
|
||||
}
|
||||
|
||||
// Git connect logic
|
||||
if (!$isConnected && !empty($providerRepositoryId)) {
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
|
||||
$repository = $dbForConsole->createDocument('repositories', 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')),
|
||||
],
|
||||
'installationId' => $installation->getId(),
|
||||
'installationInternalId' => $installation->getInternalId(),
|
||||
'projectId' => $project->getId(),
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'providerRepositoryId' => $providerRepositoryId,
|
||||
'resourceId' => $site->getId(),
|
||||
'resourceInternalId' => $site->getInternalId(),
|
||||
'resourceType' => 'site',
|
||||
'providerPullRequestIds' => []
|
||||
]));
|
||||
|
||||
$repositoryId = $repository->getId();
|
||||
$repositoryInternalId = $repository->getInternalId();
|
||||
}
|
||||
|
||||
$live = true;
|
||||
|
||||
if (
|
||||
$site->getAttribute('name') !== $name ||
|
||||
$site->getAttribute('buildCommand') !== $buildCommand ||
|
||||
$site->getAttribute('installCommand') !== $installCommand ||
|
||||
$site->getAttribute('outputDirectory') !== $outputDirectory ||
|
||||
$site->getAttribute('providerRootDirectory') !== $providerRootDirectory ||
|
||||
$site->getAttribute('framework') !== $framework
|
||||
) {
|
||||
$live = false;
|
||||
}
|
||||
|
||||
$spec = Config::getParam('framework-specifications')[$specification] ?? [];
|
||||
|
||||
// Enforce Cold Start if spec limits change.
|
||||
if ($site->getAttribute('specification') !== $specification && !empty($site->getAttribute('deploymentId'))) {
|
||||
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$site = $dbForProject->updateDocument('sites', $site->getId(), new Document(array_merge($site->getArrayCopy(), [
|
||||
'name' => $name,
|
||||
'framework' => $framework,
|
||||
'enabled' => $enabled,
|
||||
'live' => $live,
|
||||
'timeout' => $timeout,
|
||||
'installCommand' => $installCommand,
|
||||
'buildCommand' => $buildCommand,
|
||||
'outputDirectory' => $outputDirectory,
|
||||
'installationId' => $installation->getId(),
|
||||
'installationInternalId' => $installation->getInternalId(),
|
||||
'providerRepositoryId' => $providerRepositoryId,
|
||||
'repositoryId' => $repositoryId,
|
||||
'repositoryInternalId' => $repositoryInternalId,
|
||||
'providerBranch' => $providerBranch,
|
||||
'providerRootDirectory' => $providerRootDirectory,
|
||||
'providerSilentMode' => $providerSilentMode,
|
||||
'specification' => $specification,
|
||||
'search' => implode(' ', [$siteId, $name, $framework]),
|
||||
'buildRuntime' => $buildRuntime,
|
||||
'serveRuntime' => $serveRuntime,
|
||||
'fallbackFile' => $fallbackFile,
|
||||
])));
|
||||
|
||||
// Redeploy logic
|
||||
if (!$isConnected && !empty($providerRepositoryId)) {
|
||||
$this->redeployVcsFunction($request, $site, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $github);
|
||||
}
|
||||
|
||||
$queueForEvents->setParam('siteId', $site->getId());
|
||||
|
||||
$response->dynamic($site, Response::MODEL_SITE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class CreateVariable extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'createVariable';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/sites/:siteId/variables')
|
||||
->desc('Create variable')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.write')
|
||||
->label('audits.event', 'variable.create')
|
||||
->label('audits.resource', 'site/{request.siteId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'createVariable')
|
||||
->label('sdk.description', '/docs/references/sites/create-variable.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_VARIABLE)
|
||||
->param('siteId', '', new UID(), 'Site unique ID.', false)
|
||||
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
|
||||
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
|
||||
->param('secret', false, new Boolean(), 'Is secret? Secret variables can only be updated or deleted, they cannot be read.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForConsole')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, string $key, string $value, bool $secret, Response $response, Database $dbForProject, Database $dbForConsole)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$variableId = ID::unique();
|
||||
|
||||
$variable = new Document([
|
||||
'$id' => $variableId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'resourceInternalId' => $site->getInternalId(),
|
||||
'resourceId' => $site->getId(),
|
||||
'resourceType' => 'site',
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'secret' => $secret,
|
||||
'search' => implode(' ', [$variableId, $site->getId(), $key, 'site']),
|
||||
]);
|
||||
|
||||
try {
|
||||
$variable = $dbForProject->createDocument('variables', $variable);
|
||||
} catch (DuplicateException $th) {
|
||||
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false));
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class DeleteVariable extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'deleteVariable';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
|
||||
->setHttpPath('/v1/sites/:siteId/variables/:variableId')
|
||||
->desc('Delete variable')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.write')
|
||||
->label('audits.event', 'variable.delete')
|
||||
->label('audits.resource', 'site/{request.siteId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'deleteVariable')
|
||||
->label('sdk.description', '/docs/references/sites/delete-variable.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('siteId', '', new UID(), 'Site unique ID.', false)
|
||||
->param('variableId', '', new UID(), 'Variable unique ID.', false)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, string $variableId, Response $response, Database $dbForProject)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $site->getInternalId() || $variable->getAttribute('resourceType') !== 'site') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($variable === false || $variable->isEmpty()) {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dbForProject->deleteDocument('variables', $variable->getId());
|
||||
|
||||
$dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false));
|
||||
|
||||
$response->noContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class GetVariable extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'getVariable';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/sites/:siteId/variables/:variableId')
|
||||
->desc('Get variable')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'getVariable')
|
||||
->label('sdk.description', '/docs/references/sites/get-variable.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_VARIABLE)
|
||||
->param('siteId', '', new UID(), 'Site unique ID.', false)
|
||||
->param('variableId', '', new UID(), 'Variable unique ID.', false)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, string $variableId, Response $response, Database $dbForProject)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if (
|
||||
$variable === false ||
|
||||
$variable->isEmpty() ||
|
||||
$variable->getAttribute('resourceInternalId') !== $site->getInternalId() ||
|
||||
$variable->getAttribute('resourceType') !== 'site'
|
||||
) {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($variable === false || $variable->isEmpty()) {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
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;
|
||||
|
||||
class ListVariables extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'listVariables';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/sites/:siteId/variables')
|
||||
->desc('List variables')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'listVariables')
|
||||
->label('sdk.description', '/docs/references/sites/list-variables.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_VARIABLE_LIST)
|
||||
->param('siteId', '', new UID(), 'Site unique ID.', false)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, Response $response, Database $dbForProject)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'variables' => $site->getAttribute('vars', []),
|
||||
'total' => \count($site->getAttribute('vars', [])),
|
||||
]), Response::MODEL_VARIABLE_LIST);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Compute\Base;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class UpdateVariable extends Base
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateVariable';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
|
||||
->setHttpPath('/v1/sites/:siteId/variables/:variableId')
|
||||
->desc('Update variable')
|
||||
->groups(['api', 'sites'])
|
||||
->label('scope', 'sites.write')
|
||||
->label('audits.event', 'variable.update')
|
||||
->label('audits.resource', 'site/{request.siteId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'sites')
|
||||
->label('sdk.method', 'updateVariable')
|
||||
->label('sdk.description', '/docs/references/sites/update-variable.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_VARIABLE)
|
||||
->param('siteId', '', new UID(), 'Site unique ID.', false)
|
||||
->param('variableId', '', new UID(), 'Variable unique ID.', false)
|
||||
->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
|
||||
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $siteId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject)
|
||||
{
|
||||
$site = $dbForProject->getDocument('sites', $siteId);
|
||||
|
||||
if ($site->isEmpty()) {
|
||||
throw new Exception(Exception::SITE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$variable = $dbForProject->getDocument('variables', $variableId);
|
||||
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $site->getInternalId() || $variable->getAttribute('resourceType') !== 'site') {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($variable === false || $variable->isEmpty()) {
|
||||
throw new Exception(Exception::VARIABLE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$variable
|
||||
->setAttribute('key', $key)
|
||||
->setAttribute('value', $value ?? $variable->getAttribute('value'))
|
||||
->setAttribute('search', implode(' ', [$variableId, $site->getId(), $key, 'site']));
|
||||
|
||||
try {
|
||||
$dbForProject->updateDocument('variables', $variable->getId(), $variable);
|
||||
} catch (DuplicateException $th) {
|
||||
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false));
|
||||
|
||||
$response->dynamic($variable, Response::MODEL_VARIABLE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites;
|
||||
|
||||
use Utopia\Platform;
|
||||
|
||||
class Module extends Platform\Module
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->addService('http', new Services\Http());
|
||||
$this->addService('workers', new Services\Workers());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Services;
|
||||
|
||||
use Appwrite\Platform\Modules\Sites\Http\Deployments\CancelDeployment;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Deployments\CreateDeployment;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Deployments\DeleteDeployment;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Deployments\DownloadBuild;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Deployments\DownloadDeployment;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Deployments\GetDeployment;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Deployments\ListDeployments;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Deployments\RebuildDeployment;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Deployments\UpdateDeployment;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Sites\CreateSite;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Sites\DeleteSite;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Sites\GetSite;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Sites\GetSitesUsage;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Sites\GetSiteUsage;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Sites\GetTemplate;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Sites\ListFrameworks;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Sites\ListSites;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Sites\ListTemplates;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Sites\UpdateSite;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Variables\CreateVariable;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Variables\DeleteVariable;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Variables\GetVariable;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Variables\ListVariables;
|
||||
use Appwrite\Platform\Modules\Sites\Http\Variables\UpdateVariable;
|
||||
use Utopia\Platform\Service;
|
||||
|
||||
class Http extends Service
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->type = Service::TYPE_HTTP;
|
||||
// Sites
|
||||
$this->addAction(CreateSite::getName(), new CreateSite());
|
||||
$this->addAction(GetSite::getName(), new GetSite());
|
||||
$this->addAction(ListSites::getName(), new ListSites());
|
||||
$this->addAction(UpdateSite::getName(), new UpdateSite());
|
||||
$this->addAction(DeleteSite::getName(), new DeleteSite());
|
||||
|
||||
// Frameworks
|
||||
$this->addAction(ListFrameworks::getName(), new ListFrameworks());
|
||||
|
||||
|
||||
// Deployments
|
||||
$this->addAction(CreateDeployment::getName(), new CreateDeployment());
|
||||
$this->addAction(GetDeployment::getName(), new GetDeployment());
|
||||
$this->addAction(ListDeployments::getName(), new ListDeployments());
|
||||
$this->addAction(UpdateDeployment::getName(), new UpdateDeployment());
|
||||
$this->addAction(DeleteDeployment::getName(), new DeleteDeployment());
|
||||
$this->addAction(DownloadDeployment::getName(), new DownloadDeployment());
|
||||
$this->addAction(DownloadBuild::getName(), new DownloadBuild());
|
||||
$this->addAction(RebuildDeployment::getName(), new RebuildDeployment());
|
||||
$this->addAction(CancelDeployment::getName(), new CancelDeployment());
|
||||
|
||||
// Variables
|
||||
$this->addAction(CreateVariable::getName(), new CreateVariable());
|
||||
$this->addAction(GetVariable::getName(), new GetVariable());
|
||||
$this->addAction(ListVariables::getName(), new ListVariables());
|
||||
$this->addAction(UpdateVariable::getName(), new UpdateVariable());
|
||||
$this->addAction(DeleteVariable::getName(), new DeleteVariable());
|
||||
|
||||
// Templates
|
||||
$this->addAction(ListTemplates::getName(), new ListTemplates());
|
||||
$this->addAction(GetTemplate::getName(), new GetTemplate());
|
||||
|
||||
// Usage
|
||||
$this->addAction(GetSiteUsage::getName(), new GetSiteUsage());
|
||||
$this->addAction(GetSitesUsage::getName(), new GetSitesUsage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Services;
|
||||
|
||||
use Appwrite\Platform\Modules\Sites\Workers\Insights;
|
||||
use Utopia\Platform\Service;
|
||||
|
||||
class Workers extends Service
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->type = Service::TYPE_WORKER;
|
||||
$this->addAction(Insights::getName(), new Insights());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Sites\Workers;
|
||||
|
||||
use Exception;
|
||||
use Executor\Executor;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Fetch\Client;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\System\System;
|
||||
|
||||
class Insights extends Action
|
||||
{
|
||||
protected Client $client;
|
||||
protected Executor $executor;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'insights';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new Client();
|
||||
$this->client->addHeader('Authorization', 'Bearer 123');
|
||||
$this
|
||||
->desc('Builds worker')
|
||||
->inject('message')
|
||||
->inject('dbForConsole')
|
||||
->inject('dbForProject')
|
||||
->inject('deviceForFunctions')
|
||||
->inject('log')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Message $message
|
||||
* @param Database $dbForConsole
|
||||
* @param Database $dbForProject
|
||||
* @param Device $deviceForFunctions
|
||||
* @param Log $log
|
||||
* @return void
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
public function action(Message $message, Database $dbForConsole, Database $dbForProject, Device $deviceForFunctions, Log $log): void
|
||||
{
|
||||
$payload = $message->getPayload() ?? [];
|
||||
$this->executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
|
||||
|
||||
if (empty($payload)) {
|
||||
throw new \Exception('Missing payload');
|
||||
}
|
||||
|
||||
//todo: handle preview deployments
|
||||
$resource = new Document($payload['resource'] ?? []);
|
||||
$deployment = new Document($payload['deployment'] ?? []);
|
||||
$rule = $dbForConsole->findOne('rules', [
|
||||
Query::equal('resourceType', ['site']),
|
||||
Query::equal('resourceInternalId', [$resource->getInternalId()]),
|
||||
]);
|
||||
|
||||
if (!$rule) {
|
||||
throw new \Exception('No rule found');
|
||||
}
|
||||
|
||||
$url = "http://{$rule->getAttribute('domain')}";
|
||||
$screenshot = $this->getScreenshot($url);
|
||||
// todo: figure out where to store the screenshot
|
||||
$path = $deviceForFunctions->getRoot() . DIRECTORY_SEPARATOR . "screenshots" .DIRECTORY_SEPARATOR. "{$resource->getId()}.{$deployment->getId()}.png";
|
||||
$success = $deviceForFunctions->write($path, $screenshot, "image/png");
|
||||
if ($success) {
|
||||
$deployment->setAttribute('screenshot', $path);
|
||||
$dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
}
|
||||
$audit = $this->getAudit($url);
|
||||
if ($audit) {
|
||||
$deployment->setAttribute('auditJson', $audit);
|
||||
$dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getScreenshot(string $url): mixed|false
|
||||
{
|
||||
$response = $this->client->fetch('http://appwrite-browser/screenshot', query: [
|
||||
'url' => $url
|
||||
]);
|
||||
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response->getBody();
|
||||
}
|
||||
|
||||
protected function getAudit(string $url): string|false
|
||||
{
|
||||
$response = $this->client->fetch('http://appwrite-browser/lighthouse', query: [
|
||||
'url' => $url
|
||||
]);
|
||||
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response->text();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Appwrite\Platform\Services;
|
||||
|
||||
use Appwrite\Platform\Workers\Audits;
|
||||
use Appwrite\Platform\Workers\Builds;
|
||||
use Appwrite\Platform\Workers\Certificates;
|
||||
use Appwrite\Platform\Workers\Databases;
|
||||
use Appwrite\Platform\Workers\Deletes;
|
||||
@@ -23,7 +22,6 @@ class Workers extends Service
|
||||
$this->type = Service::TYPE_WORKER;
|
||||
$this
|
||||
->addAction(Audits::getName(), new Audits())
|
||||
->addAction(Builds::getName(), new Builds())
|
||||
->addAction(Certificates::getName(), new Certificates())
|
||||
->addAction(Databases::getName(), new Databases())
|
||||
->addAction(Deletes::getName(), new Deletes())
|
||||
|
||||
@@ -96,10 +96,10 @@ class Doctor extends Action
|
||||
Console::log('🟢 HTTPS force option is enabled');
|
||||
}
|
||||
|
||||
if ('enabled' !== System::getEnv('_APP_OPTIONS_FUNCTIONS_FORCE_HTTPS', 'disabled')) {
|
||||
Console::log('🔴 HTTPS force option is disabled for function domains');
|
||||
if ('enabled' !== System::getEnv('_APP_OPTIONS_COMPUTE_FORCE_HTTPS', 'disabled')) {
|
||||
Console::log('🔴 HTTPS force option is disabled for function/site domains');
|
||||
} else {
|
||||
Console::log('🟢 HTTPS force option is enabled for function domains');
|
||||
Console::log('🟢 HTTPS force option is enabled for function/site domains');
|
||||
}
|
||||
|
||||
$providerConfig = System::getEnv('_APP_LOGGING_CONFIG', '');
|
||||
|
||||
@@ -25,6 +25,9 @@ use Appwrite\Spec\Swagger2;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class SDKs extends Action
|
||||
{
|
||||
@@ -37,23 +40,35 @@ class SDKs extends Action
|
||||
{
|
||||
$this
|
||||
->desc('Generate Appwrite SDKs')
|
||||
->callback(fn () => $this->action());
|
||||
->param('platform', null, new Nullable(new Text(256)), 'Selected Platform', optional: true)
|
||||
->param('sdk', null, new Nullable(new Text(256)), 'Selected SDK', optional:true)
|
||||
->param('version', null, new Nullable(new Text(256)), 'Selected SDK', optional:true)
|
||||
->param('git', null, new Nullable(new WhiteList(['yes', 'no'])), 'Should we use git push?', optional: true)
|
||||
->param('production', null, new Nullable(new WhiteList(['yes', 'no'])), 'Should we push to production?', optional:true)
|
||||
->param('message', null, new Nullable(new Text(256)), 'Commit Message', optional:true)
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(): void
|
||||
public function action(?string $selectedPlatform, ?string $selectedSDK, ?string $version, ?string $git, ?string $production, ?string $message)
|
||||
{
|
||||
$platforms = Config::getParam('platforms');
|
||||
$selectedPlatform = Console::confirm('Choose Platform ("' . APP_PLATFORM_CLIENT . '", "' . APP_PLATFORM_SERVER . '", "' . APP_PLATFORM_CONSOLE . '" or "*" for all):');
|
||||
$selectedSDK = \strtolower(Console::confirm('Choose SDK ("*" for all):'));
|
||||
$version = Console::confirm('Choose an Appwrite version');
|
||||
$git = (Console::confirm('Should we use git push? (yes/no)') == 'yes');
|
||||
$production = ($git) ? (Console::confirm('Type "Appwrite" to push code to production git repos') == 'Appwrite') : false;
|
||||
$message = ($git) ? Console::confirm('Please enter your commit message:') : '';
|
||||
$selectedPlatform ??= Console::confirm('Choose Platform ("' . APP_PLATFORM_CLIENT . '", "' . APP_PLATFORM_SERVER . '", "' . APP_PLATFORM_CONSOLE . '" or "*" for all):');
|
||||
$selectedSDK ??= \strtolower(Console::confirm('Choose SDK ("*" for all):'));
|
||||
$version ??= Console::confirm('Choose an Appwrite version');
|
||||
|
||||
$git ??= Console::confirm('Should we use git push? (yes/no)');
|
||||
$git = $git === 'yes';
|
||||
|
||||
if ($git) {
|
||||
$production ??= Console::confirm('Type "Appwrite" to push code to production git repos');
|
||||
$production = $production === 'Appwrite';
|
||||
$message ??= Console::confirm('Please enter your commit message:');
|
||||
}
|
||||
|
||||
if (!in_array($version, ['0.6.x', '0.7.x', '0.8.x', '0.9.x', '0.10.x', '0.11.x', '0.12.x', '0.13.x', '0.14.x', '0.15.x', '1.0.x', '1.1.x', '1.2.x', '1.3.x', '1.4.x', '1.5.x', '1.6.x', 'latest'])) {
|
||||
throw new \Exception('Unknown version given');
|
||||
}
|
||||
|
||||
$platforms = Config::getParam('platforms');
|
||||
foreach ($platforms as $key => $platform) {
|
||||
if ($selectedPlatform !== $key && $selectedPlatform !== '*') {
|
||||
continue;
|
||||
|
||||
@@ -92,6 +92,7 @@ class Databases extends Action
|
||||
* @throws Authorization
|
||||
* @throws Conflict
|
||||
* @throws \Exception
|
||||
* @throws \Throwable
|
||||
*/
|
||||
private function createAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForConsole, Database $dbForProject): void
|
||||
{
|
||||
@@ -134,7 +135,6 @@ class Databases extends Action
|
||||
$options = $attribute->getAttribute('options', []);
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
|
||||
try {
|
||||
switch ($type) {
|
||||
case Database::VAR_RELATIONSHIP:
|
||||
@@ -170,7 +170,6 @@ class Databases extends Action
|
||||
|
||||
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'available'));
|
||||
} catch (\Throwable $e) {
|
||||
// TODO: Send non DatabaseExceptions to Sentry
|
||||
Console::error($e->getMessage());
|
||||
|
||||
if ($e instanceof DatabaseException) {
|
||||
@@ -193,15 +192,17 @@ class Databases extends Action
|
||||
$relatedAttribute->setAttribute('status', 'failed')
|
||||
);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
} finally {
|
||||
$this->trigger($database, $collection, $attribute, $project, $projectId, $events);
|
||||
}
|
||||
|
||||
if ($type === Database::VAR_RELATIONSHIP && $options['twoWay']) {
|
||||
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId());
|
||||
}
|
||||
if ($type === Database::VAR_RELATIONSHIP && $options['twoWay']) {
|
||||
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId());
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId);
|
||||
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,6 +216,7 @@ class Databases extends Action
|
||||
* @throws Authorization
|
||||
* @throws Conflict
|
||||
* @throws \Exception
|
||||
* @throws \Throwable
|
||||
**/
|
||||
private function deleteAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForConsole, Database $dbForProject): void
|
||||
{
|
||||
@@ -248,113 +250,114 @@ class Databases extends Action
|
||||
// - stuck: attribute was available but cannot be removed
|
||||
|
||||
try {
|
||||
if ($status !== 'failed') {
|
||||
if ($type === Database::VAR_RELATIONSHIP) {
|
||||
if ($options['twoWay']) {
|
||||
$relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']);
|
||||
if ($relatedCollection->isEmpty()) {
|
||||
throw new DatabaseException('Collection not found');
|
||||
try {
|
||||
if ($status !== 'failed') {
|
||||
if ($type === Database::VAR_RELATIONSHIP) {
|
||||
if ($options['twoWay']) {
|
||||
$relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']);
|
||||
if ($relatedCollection->isEmpty()) {
|
||||
throw new DatabaseException('Collection not found');
|
||||
}
|
||||
$relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']);
|
||||
}
|
||||
$relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']);
|
||||
}
|
||||
|
||||
if (!$dbForProject->deleteRelationship('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
|
||||
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'stuck'));
|
||||
throw new DatabaseException('Failed to delete Relationship');
|
||||
if (!$dbForProject->deleteRelationship('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
|
||||
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'stuck'));
|
||||
throw new DatabaseException('Failed to delete Relationship');
|
||||
}
|
||||
} elseif (!$dbForProject->deleteAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
|
||||
throw new DatabaseException('Failed to delete Attribute');
|
||||
}
|
||||
} elseif (!$dbForProject->deleteAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
|
||||
throw new DatabaseException('Failed to delete Attribute');
|
||||
}
|
||||
}
|
||||
|
||||
$dbForProject->deleteDocument('attributes', $attribute->getId());
|
||||
$dbForProject->deleteDocument('attributes', $attribute->getId());
|
||||
|
||||
if (!$relatedAttribute->isEmpty()) {
|
||||
$dbForProject->deleteDocument('attributes', $relatedAttribute->getId());
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// TODO: Send non DatabaseExceptions to Sentry
|
||||
Console::error($e->getMessage());
|
||||
|
||||
if ($e instanceof DatabaseException) {
|
||||
$attribute->setAttribute('error', $e->getMessage());
|
||||
if (!$relatedAttribute->isEmpty()) {
|
||||
$relatedAttribute->setAttribute('error', $e->getMessage());
|
||||
$dbForProject->deleteDocument('attributes', $relatedAttribute->getId());
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Console::error($e->getMessage());
|
||||
|
||||
if ($e instanceof DatabaseException) {
|
||||
$attribute->setAttribute('error', $e->getMessage());
|
||||
if (!$relatedAttribute->isEmpty()) {
|
||||
$relatedAttribute->setAttribute('error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
$dbForProject->updateDocument(
|
||||
'attributes',
|
||||
$attribute->getId(),
|
||||
$attribute->setAttribute('status', 'stuck')
|
||||
);
|
||||
if (!$relatedAttribute->isEmpty()) {
|
||||
$dbForProject->updateDocument(
|
||||
'attributes',
|
||||
$relatedAttribute->getId(),
|
||||
$relatedAttribute->setAttribute('status', 'stuck')
|
||||
$attribute->getId(),
|
||||
$attribute->setAttribute('status', 'stuck')
|
||||
);
|
||||
if (!$relatedAttribute->isEmpty()) {
|
||||
$dbForProject->updateDocument(
|
||||
'attributes',
|
||||
$relatedAttribute->getId(),
|
||||
$relatedAttribute->setAttribute('status', 'stuck')
|
||||
);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
} finally {
|
||||
$this->trigger($database, $collection, $attribute, $project, $projectId, $events);
|
||||
}
|
||||
} finally {
|
||||
$this->trigger($database, $collection, $attribute, $project, $projectId, $events);
|
||||
}
|
||||
|
||||
// The underlying database removes/rebuilds indexes when attribute is removed
|
||||
// Update indexes table with changes
|
||||
/** @var Document[] $indexes */
|
||||
$indexes = $collection->getAttribute('indexes', []);
|
||||
// The underlying database removes/rebuilds indexes when attribute is removed
|
||||
// Update indexes table with changes
|
||||
/** @var Document[] $indexes */
|
||||
$indexes = $collection->getAttribute('indexes', []);
|
||||
|
||||
foreach ($indexes as $index) {
|
||||
/** @var string[] $attributes */
|
||||
$attributes = $index->getAttribute('attributes');
|
||||
$lengths = $index->getAttribute('lengths');
|
||||
$orders = $index->getAttribute('orders');
|
||||
foreach ($indexes as $index) {
|
||||
/** @var string[] $attributes */
|
||||
$attributes = $index->getAttribute('attributes');
|
||||
$lengths = $index->getAttribute('lengths');
|
||||
$orders = $index->getAttribute('orders');
|
||||
|
||||
$found = \array_search($key, $attributes);
|
||||
$found = \array_search($key, $attributes);
|
||||
|
||||
if ($found !== false) {
|
||||
// If found, remove entry from attributes, lengths, and orders
|
||||
// array_values wraps array_diff to reindex array keys
|
||||
// when found attribute is removed from array
|
||||
$attributes = \array_values(\array_diff($attributes, [$attributes[$found]]));
|
||||
$lengths = \array_values(\array_diff($lengths, isset($lengths[$found]) ? [$lengths[$found]] : []));
|
||||
$orders = \array_values(\array_diff($orders, isset($orders[$found]) ? [$orders[$found]] : []));
|
||||
if ($found !== false) {
|
||||
// If found, remove entry from attributes, lengths, and orders
|
||||
// array_values wraps array_diff to reindex array keys
|
||||
// when found attribute is removed from array
|
||||
$attributes = \array_values(\array_diff($attributes, [$attributes[$found]]));
|
||||
$lengths = \array_values(\array_diff($lengths, isset($lengths[$found]) ? [$lengths[$found]] : []));
|
||||
$orders = \array_values(\array_diff($orders, isset($orders[$found]) ? [$orders[$found]] : []));
|
||||
|
||||
if (empty($attributes)) {
|
||||
$dbForProject->deleteDocument('indexes', $index->getId());
|
||||
} else {
|
||||
$index
|
||||
->setAttribute('attributes', $attributes, Document::SET_TYPE_ASSIGN)
|
||||
->setAttribute('lengths', $lengths, Document::SET_TYPE_ASSIGN)
|
||||
->setAttribute('orders', $orders, Document::SET_TYPE_ASSIGN);
|
||||
|
||||
// Check if an index exists with the same attributes and orders
|
||||
$exists = false;
|
||||
foreach ($indexes as $existing) {
|
||||
if (
|
||||
$existing->getAttribute('key') !== $index->getAttribute('key') // Ignore itself
|
||||
&& $existing->getAttribute('attributes') === $index->getAttribute('attributes')
|
||||
&& $existing->getAttribute('orders') === $index->getAttribute('orders')
|
||||
) {
|
||||
$exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($exists) { // Delete the duplicate if created, else update in db
|
||||
$this->deleteIndex($database, $collection, $index, $project, $dbForConsole, $dbForProject);
|
||||
if (empty($attributes)) {
|
||||
$dbForProject->deleteDocument('indexes', $index->getId());
|
||||
} else {
|
||||
$dbForProject->updateDocument('indexes', $index->getId(), $index);
|
||||
$index
|
||||
->setAttribute('attributes', $attributes, Document::SET_TYPE_ASSIGN)
|
||||
->setAttribute('lengths', $lengths, Document::SET_TYPE_ASSIGN)
|
||||
->setAttribute('orders', $orders, Document::SET_TYPE_ASSIGN);
|
||||
|
||||
// Check if an index exists with the same attributes and orders
|
||||
$exists = false;
|
||||
foreach ($indexes as $existing) {
|
||||
if (
|
||||
$existing->getAttribute('key') !== $index->getAttribute('key') // Ignore itself
|
||||
&& $existing->getAttribute('attributes') === $index->getAttribute('attributes')
|
||||
&& $existing->getAttribute('orders') === $index->getAttribute('orders')
|
||||
) {
|
||||
$exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($exists) { // Delete the duplicate if created, else update in db
|
||||
$this->deleteIndex($database, $collection, $index, $project, $dbForConsole, $dbForProject);
|
||||
} else {
|
||||
$dbForProject->updateDocument('indexes', $index->getId(), $index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId);
|
||||
|
||||
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId);
|
||||
$dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId());
|
||||
|
||||
if (!$relatedCollection->isEmpty() && !$relatedAttribute->isEmpty()) {
|
||||
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId());
|
||||
$dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId());
|
||||
if (!$relatedCollection->isEmpty() && !$relatedAttribute->isEmpty()) {
|
||||
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,6 +373,7 @@ class Databases extends Action
|
||||
* @throws Conflict
|
||||
* @throws Structure
|
||||
* @throws DatabaseException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
private function createIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForConsole, Database $dbForProject): void
|
||||
{
|
||||
@@ -401,9 +405,7 @@ class Databases extends Action
|
||||
}
|
||||
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'available'));
|
||||
} catch (\Throwable $e) {
|
||||
// TODO: Send non DatabaseExceptions to Sentry
|
||||
Console::error($e->getMessage());
|
||||
|
||||
if ($e instanceof DatabaseException) {
|
||||
$index->setAttribute('error', $e->getMessage());
|
||||
}
|
||||
@@ -412,11 +414,12 @@ class Databases extends Action
|
||||
$index->getId(),
|
||||
$index->setAttribute('status', 'failed')
|
||||
);
|
||||
|
||||
throw $e;
|
||||
} finally {
|
||||
$this->trigger($database, $collection, $index, $project, $projectId, $events);
|
||||
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId);
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -431,6 +434,7 @@ class Databases extends Action
|
||||
* @throws Conflict
|
||||
* @throws Structure
|
||||
* @throws DatabaseException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
private function deleteIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForConsole, Database $dbForProject): void
|
||||
{
|
||||
@@ -459,7 +463,6 @@ class Databases extends Action
|
||||
$dbForProject->deleteDocument('indexes', $index->getId());
|
||||
$index->setAttribute('status', 'deleted');
|
||||
} catch (\Throwable $e) {
|
||||
// TODO: Send non DatabaseExceptions to Sentry
|
||||
Console::error($e->getMessage());
|
||||
|
||||
if ($e instanceof DatabaseException) {
|
||||
@@ -470,11 +473,13 @@ class Databases extends Action
|
||||
$index->getId(),
|
||||
$index->setAttribute('status', 'stuck')
|
||||
);
|
||||
|
||||
throw $e;
|
||||
|
||||
} finally {
|
||||
$this->trigger($database, $collection, $index, $project, $projectId, $events);
|
||||
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collection->getId());
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collection->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,6 +47,7 @@ class Deletes extends Action
|
||||
->inject('dbForConsole')
|
||||
->inject('getProjectDB')
|
||||
->inject('deviceForFiles')
|
||||
->inject('deviceForSites')
|
||||
->inject('deviceForFunctions')
|
||||
->inject('deviceForBuilds')
|
||||
->inject('deviceForCache')
|
||||
@@ -54,14 +55,14 @@ class Deletes extends Action
|
||||
->inject('executionRetention')
|
||||
->inject('auditRetention')
|
||||
->inject('log')
|
||||
->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log) => $this->action($message, $dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $abuseRetention, $executionRetention, $auditRetention, $log));
|
||||
->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log) => $this->action($message, $dbForConsole, $getProjectDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $abuseRetention, $executionRetention, $auditRetention, $log));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log): void
|
||||
public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log): void
|
||||
{
|
||||
$payload = $message->getPayload() ?? [];
|
||||
|
||||
@@ -84,7 +85,7 @@ class Deletes extends Action
|
||||
case DELETE_TYPE_DOCUMENT:
|
||||
switch ($document->getCollection()) {
|
||||
case DELETE_TYPE_PROJECTS:
|
||||
$this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $document);
|
||||
$this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $document);
|
||||
break;
|
||||
case DELETE_TYPE_FUNCTIONS:
|
||||
$this->deleteFunction($dbForConsole, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $project);
|
||||
@@ -451,11 +452,12 @@ class Deletes extends Action
|
||||
|
||||
foreach ($projects as $project) {
|
||||
$deviceForFiles = getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId());
|
||||
$deviceForSites = getDevice(APP_STORAGE_SITES . '/app-' . $project->getId());
|
||||
$deviceForFunctions = getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId());
|
||||
$deviceForBuilds = getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId());
|
||||
$deviceForCache = getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId());
|
||||
|
||||
$this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $project);
|
||||
$this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $project);
|
||||
$dbForConsole->deleteDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
@@ -473,7 +475,7 @@ class Deletes extends Action
|
||||
* @throws Authorization
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
private function deleteProject(Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, Document $document): void
|
||||
private function deleteProject(Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, Document $document): void
|
||||
{
|
||||
$projectInternalId = $document->getInternalId();
|
||||
$projectId = $document->getId();
|
||||
@@ -503,7 +505,7 @@ class Deletes extends Action
|
||||
try {
|
||||
$dbForProject->deleteCollection($collection->getId());
|
||||
} catch (Throwable $e) {
|
||||
Console::error('Error deleting '.$collection->getId().' '.$e->getMessage());
|
||||
Console::error('Error deleting ' . $collection->getId() . ' ' . $e->getMessage());
|
||||
|
||||
/**
|
||||
* Ignore junction tables;
|
||||
@@ -579,6 +581,7 @@ class Deletes extends Action
|
||||
|
||||
// Delete all storage directories
|
||||
$deviceForFiles->delete($deviceForFiles->getRoot(), true);
|
||||
$deviceForSites->delete($deviceForSites->getRoot(), true);
|
||||
$deviceForFunctions->delete($deviceForFunctions->getRoot(), true);
|
||||
$deviceForBuilds->delete($deviceForBuilds->getRoot(), true);
|
||||
$deviceForCache->delete($deviceForCache->getRoot(), true);
|
||||
|
||||
@@ -326,7 +326,7 @@ class Functions extends Action
|
||||
$user ??= new Document();
|
||||
$functionId = $function->getId();
|
||||
$deploymentId = $function->getAttribute('deployment', '');
|
||||
$spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_FUNCTION_SPECIFICATION_DEFAULT)];
|
||||
$spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
|
||||
|
||||
$log->addTag('deploymentId', $deploymentId);
|
||||
|
||||
@@ -479,8 +479,8 @@ class Functions extends Action
|
||||
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
|
||||
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
|
||||
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
|
||||
'APPWRITE_FUNCTION_CPUS' => ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT),
|
||||
'APPWRITE_FUNCTION_MEMORY' => ($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT),
|
||||
'APPWRITE_COMPUTE_CPUS' => ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT),
|
||||
'APPWRITE_COMPUTE_MEMORY' => ($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT),
|
||||
'APPWRITE_VERSION' => APP_VERSION_STABLE,
|
||||
'APPWRITE_REGION' => $project->getAttribute('region'),
|
||||
'APPWRITE_DEPLOYMENT_TYPE' => $deployment->getAttribute('type', ''),
|
||||
@@ -518,8 +518,8 @@ class Functions extends Action
|
||||
method: $method,
|
||||
headers: $headers,
|
||||
runtimeEntrypoint: $command,
|
||||
cpus: $spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT,
|
||||
memory: $spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT,
|
||||
cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
|
||||
memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
|
||||
logging: $function->getAttribute('logging', true),
|
||||
);
|
||||
|
||||
@@ -558,8 +558,8 @@ class Functions extends Action
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
|
||||
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000))// per project
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000))
|
||||
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
|
||||
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
|
||||
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
|
||||
->trigger()
|
||||
;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ use Utopia\Database\Exception\Conflict;
|
||||
use Utopia\Database\Exception\Restricted;
|
||||
use Utopia\Database\Exception\Structure;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Migration\Destination;
|
||||
use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite;
|
||||
use Utopia\Migration\Exception as MigrationException;
|
||||
@@ -37,6 +36,8 @@ class Migrations extends Action
|
||||
|
||||
protected Document $project;
|
||||
|
||||
protected $logError;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'migrations';
|
||||
@@ -52,14 +53,14 @@ class Migrations extends Action
|
||||
->inject('message')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForConsole')
|
||||
->inject('log')
|
||||
->callback(fn (Message $message, Database $dbForProject, Database $dbForConsole, Log $log) => $this->action($message, $dbForProject, $dbForConsole, $log));
|
||||
->inject('logError')
|
||||
->callback(fn (Message $message, Database $dbForProject, Database $dbForConsole, callable $logError) => $this->action($message, $dbForProject, $dbForConsole, $logError));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function action(Message $message, Database $dbForProject, Database $dbForConsole, Log $log): void
|
||||
public function action(Message $message, Database $dbForProject, Database $dbForConsole, callable $logError): void
|
||||
{
|
||||
$payload = $message->getPayload() ?? [];
|
||||
|
||||
@@ -78,6 +79,7 @@ class Migrations extends Action
|
||||
$this->dbForProject = $dbForProject;
|
||||
$this->dbForConsole = $dbForConsole;
|
||||
$this->project = $project;
|
||||
$this->logError = $logError;
|
||||
|
||||
/**
|
||||
* Handle Event execution.
|
||||
@@ -86,10 +88,7 @@ class Migrations extends Action
|
||||
return;
|
||||
}
|
||||
|
||||
$log->addTag('migrationId', $migration->getId());
|
||||
$log->addTag('projectId', $project->getId());
|
||||
|
||||
$this->processMigration($migration, $log);
|
||||
$this->processMigration($migration);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,7 +258,7 @@ class Migrations extends Action
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function processMigration(Document $migration, Log $log): void
|
||||
protected function processMigration(Document $migration): void
|
||||
{
|
||||
$project = $this->project;
|
||||
$projectDocument = $this->dbForConsole->getDocument('projects', $project->getId());
|
||||
@@ -268,8 +267,6 @@ class Migrations extends Action
|
||||
$transfer = $source = $destination = null;
|
||||
|
||||
try {
|
||||
$migration = $this->dbForProject->getDocument('migrations', $migration->getId());
|
||||
|
||||
if (
|
||||
$migration->getAttribute('source') === SourceAppwrite::getName() &&
|
||||
empty($migration->getAttribute('credentials', []))
|
||||
@@ -287,8 +284,6 @@ class Migrations extends Action
|
||||
$migration->setAttribute('status', 'processing');
|
||||
$this->updateMigrationDocument($migration, $projectDocument);
|
||||
|
||||
$log->addTag('type', $migration->getAttribute('source'));
|
||||
|
||||
$source = $this->processSource($migration);
|
||||
$destination = $this->processDestination($migration, $tempAPIKey->getAttribute('secret'));
|
||||
|
||||
@@ -326,7 +321,6 @@ class Migrations extends Action
|
||||
|
||||
$errorMessages = [];
|
||||
foreach ($sourceErrors as $error) {
|
||||
/** @var $sourceErrors $error */
|
||||
$message = "Error occurred while fetching '{$error->getResourceName()}:{$error->getResourceId()}' from source with message: '{$error->getMessage()}'";
|
||||
if ($error->getPrevious()) {
|
||||
$message .= " Message: ".$error->getPrevious()->getMessage() . " File: ".$error->getPrevious()->getFile() . " Line: ".$error->getPrevious()->getLine();
|
||||
@@ -346,7 +340,6 @@ class Migrations extends Action
|
||||
}
|
||||
|
||||
$migration->setAttribute('errors', $errorMessages);
|
||||
$log->addExtra('migrationErrors', json_encode($errorMessages));
|
||||
$this->updateMigrationDocument($migration, $projectDocument);
|
||||
|
||||
return;
|
||||
@@ -361,7 +354,6 @@ class Migrations extends Action
|
||||
if (! $migration->isEmpty()) {
|
||||
$migration->setAttribute('status', 'failed');
|
||||
$migration->setAttribute('stage', 'finished');
|
||||
$migration->setAttribute('errors', [$th->getMessage()]);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -381,7 +373,6 @@ class Migrations extends Action
|
||||
}
|
||||
|
||||
$migration->setAttribute('errors', $errorMessages);
|
||||
$log->addTag('migrationErrors', json_encode($errorMessages));
|
||||
}
|
||||
} finally {
|
||||
if (! $tempAPIKey->isEmpty()) {
|
||||
@@ -396,7 +387,23 @@ class Migrations extends Action
|
||||
$destination->error();
|
||||
$source->error();
|
||||
|
||||
throw new Exception('Migration failed');
|
||||
foreach ($source->getErrors() as $error) {
|
||||
call_user_func($this->logError, $error, 'appwrite-worker', 'appwrite-queue-' . self::getName(), [
|
||||
'migrationId' => $migration->getId() ?? '',
|
||||
'source' => $migration->getAttribute('source') ?? '',
|
||||
'resourceName' => $error->getResourceName(),
|
||||
'resourceGroup' => $error->getResourceGroup()
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($destination->getErrors() as $error) {
|
||||
call_user_func($this->logError, $error, 'appwrite-worker', 'appwrite-queue-' . self::getName(), [
|
||||
'migrationId' => $migration->getId() ?? '',
|
||||
'source' => $migration->getAttribute('source') ?? '',
|
||||
'resourceName' => $error->getResourceName(),
|
||||
'resourceGroup' => $error->getResourceGroup()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($migration->getAttribute('status', '') === 'completed') {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Sites\Validator;
|
||||
|
||||
use Utopia\Validator;
|
||||
|
||||
class FrameworkSpecification extends Validator
|
||||
{
|
||||
private array $plan;
|
||||
|
||||
private array $specifications;
|
||||
|
||||
private float $maxCpus;
|
||||
|
||||
private int $maxMemory;
|
||||
|
||||
public function __construct(array $plan, array $specifications, float $maxCpus, int $maxMemory)
|
||||
{
|
||||
$this->plan = $plan;
|
||||
$this->specifications = $specifications;
|
||||
$this->maxCpus = $maxCpus;
|
||||
$this->maxMemory = $maxMemory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Allowed Specifications.
|
||||
*
|
||||
* Get allowed specifications taking into account the limits set by the environment variables and the plan.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAllowedSpecifications(): array
|
||||
{
|
||||
$allowedSpecifications = [];
|
||||
|
||||
foreach ($this->specifications as $size => $values) {
|
||||
if ($values['cpus'] <= $this->maxCpus && $values['memory'] <= $this->maxMemory) {
|
||||
if (!empty($this->plan) && array_key_exists('frameworkSpecifications', $this->plan)) {
|
||||
if (!\in_array($size, $this->plan['frameworkSpecifications'])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$allowedSpecifications[] = $size;
|
||||
}
|
||||
}
|
||||
|
||||
return $allowedSpecifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Description.
|
||||
*
|
||||
* Returns validator description.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Specification must be one of: ' . implode(', ', $this->getAllowedSpecifications());
|
||||
}
|
||||
|
||||
/**
|
||||
* Is valid.
|
||||
*
|
||||
* Returns true if valid or false if not.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
if (empty($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\in_array($value, $this->getAllowedSpecifications())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is array.
|
||||
*
|
||||
* Function will return true if object is array.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isArray(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type.
|
||||
*
|
||||
* Returns validator type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return self::TYPE_STRING;
|
||||
}
|
||||
}
|
||||
@@ -198,6 +198,17 @@ abstract class Format
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'sites':
|
||||
switch ($method) {
|
||||
case 'getUsage':
|
||||
case 'getSiteUsage':
|
||||
switch ($param) {
|
||||
case 'range':
|
||||
return 'SiteUsageRange';
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'messaging':
|
||||
switch ($method) {
|
||||
case 'getUsage':
|
||||
@@ -386,6 +397,14 @@ abstract class Format
|
||||
return ['Twenty Four Hours', 'Thirty Days', 'Ninety Days'];
|
||||
}
|
||||
break;
|
||||
case 'sites':
|
||||
switch ($method) {
|
||||
case 'getUsage':
|
||||
case 'getSiteUsage':
|
||||
// Range Enum Keys
|
||||
return ['Twenty Four Hours', 'Thirty Days', 'Ninety Days'];
|
||||
}
|
||||
break;
|
||||
case 'users':
|
||||
switch ($method) {
|
||||
case 'getUsage':
|
||||
|
||||
@@ -323,6 +323,7 @@ class OpenAPI3 extends Format
|
||||
case 'Utopia\Validator\JSON':
|
||||
case 'Utopia\Validator\Mock':
|
||||
case 'Utopia\Validator\Assoc':
|
||||
case 'Appwrite\Functions\Validator\Payload':
|
||||
$param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
|
||||
$node['schema']['type'] = 'object';
|
||||
$node['schema']['x-example'] = '{}';
|
||||
|
||||
@@ -349,6 +349,7 @@ class Swagger2 extends Format
|
||||
case 'Utopia\Validator\JSON':
|
||||
case 'Utopia\Validator\Mock':
|
||||
case 'Utopia\Validator\Assoc':
|
||||
case 'Appwrite\Functions\Validator\Payload':
|
||||
$node['type'] = 'object';
|
||||
$node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
|
||||
$node['x-example'] = '{}';
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database\Validator\Queries;
|
||||
|
||||
class Sites extends Base
|
||||
{
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
'name',
|
||||
'enabled',
|
||||
'framework',
|
||||
'deploymentId',
|
||||
'buildCommand',
|
||||
'installCommand',
|
||||
'outputDirectory',
|
||||
'installationId'
|
||||
];
|
||||
|
||||
/**
|
||||
* Expression constructor
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('sites', self::ALLOWED_ATTRIBUTES);
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ use Appwrite\Utopia\Response\Model\Error;
|
||||
use Appwrite\Utopia\Response\Model\ErrorDev;
|
||||
use Appwrite\Utopia\Response\Model\Execution;
|
||||
use Appwrite\Utopia\Response\Model\File;
|
||||
use Appwrite\Utopia\Response\Model\Framework;
|
||||
use Appwrite\Utopia\Response\Model\Func;
|
||||
use Appwrite\Utopia\Response\Model\Headers;
|
||||
use Appwrite\Utopia\Response\Model\HealthAntivirus;
|
||||
@@ -84,13 +85,16 @@ use Appwrite\Utopia\Response\Model\ProviderRepository;
|
||||
use Appwrite\Utopia\Response\Model\Rule;
|
||||
use Appwrite\Utopia\Response\Model\Runtime;
|
||||
use Appwrite\Utopia\Response\Model\Session;
|
||||
use Appwrite\Utopia\Response\Model\Site;
|
||||
use Appwrite\Utopia\Response\Model\Specification;
|
||||
use Appwrite\Utopia\Response\Model\Subscriber;
|
||||
use Appwrite\Utopia\Response\Model\Target;
|
||||
use Appwrite\Utopia\Response\Model\Team;
|
||||
use Appwrite\Utopia\Response\Model\TemplateEmail;
|
||||
use Appwrite\Utopia\Response\Model\TemplateFramework;
|
||||
use Appwrite\Utopia\Response\Model\TemplateFunction;
|
||||
use Appwrite\Utopia\Response\Model\TemplateRuntime;
|
||||
use Appwrite\Utopia\Response\Model\TemplateSite;
|
||||
use Appwrite\Utopia\Response\Model\TemplateSMS;
|
||||
use Appwrite\Utopia\Response\Model\TemplateVariable;
|
||||
use Appwrite\Utopia\Response\Model\Token;
|
||||
@@ -102,6 +106,8 @@ use Appwrite\Utopia\Response\Model\UsageDatabases;
|
||||
use Appwrite\Utopia\Response\Model\UsageFunction;
|
||||
use Appwrite\Utopia\Response\Model\UsageFunctions;
|
||||
use Appwrite\Utopia\Response\Model\UsageProject;
|
||||
use Appwrite\Utopia\Response\Model\UsageSite;
|
||||
use Appwrite\Utopia\Response\Model\UsageSites;
|
||||
use Appwrite\Utopia\Response\Model\UsageStorage;
|
||||
use Appwrite\Utopia\Response\Model\UsageUsers;
|
||||
use Appwrite\Utopia\Response\Model\User;
|
||||
@@ -140,6 +146,8 @@ class Response extends SwooleResponse
|
||||
public const MODEL_USAGE_STORAGE = 'usageStorage';
|
||||
public const MODEL_USAGE_FUNCTIONS = 'usageFunctions';
|
||||
public const MODEL_USAGE_FUNCTION = 'usageFunction';
|
||||
public const MODEL_USAGE_SITES = 'usageSites';
|
||||
public const MODEL_USAGE_SITE = 'usageSite';
|
||||
public const MODEL_USAGE_PROJECT = 'usageProject';
|
||||
|
||||
// Database
|
||||
@@ -244,6 +252,15 @@ class Response extends SwooleResponse
|
||||
public const MODEL_VCS_CONTENT = 'vcsContent';
|
||||
public const MODEL_VCS_CONTENT_LIST = 'vcsContentList';
|
||||
|
||||
// Sites
|
||||
public const MODEL_SITE = 'site';
|
||||
public const MODEL_SITE_LIST = 'siteList';
|
||||
public const MODEL_FRAMEWORK = 'framework';
|
||||
public const MODEL_FRAMEWORK_LIST = 'frameworkList';
|
||||
public const MODEL_TEMPLATE_SITE = 'templateSite';
|
||||
public const MODEL_TEMPLATE_SITE_LIST = 'templateSiteList';
|
||||
public const MODEL_TEMPLATE_FRAMEWORK = 'templateFramework';
|
||||
|
||||
// Functions
|
||||
public const MODEL_FUNCTION = 'function';
|
||||
public const MODEL_FUNCTION_LIST = 'functionList';
|
||||
@@ -351,11 +368,14 @@ class Response extends SwooleResponse
|
||||
->setModel(new BaseList('Buckets List', self::MODEL_BUCKET_LIST, 'buckets', self::MODEL_BUCKET))
|
||||
->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM))
|
||||
->setModel(new BaseList('Memberships List', self::MODEL_MEMBERSHIP_LIST, 'memberships', self::MODEL_MEMBERSHIP))
|
||||
->setModel(new BaseList('Sites List', self::MODEL_SITE_LIST, 'sites', self::MODEL_SITE))
|
||||
->setModel(new BaseList('Site Templates List', self::MODEL_TEMPLATE_SITE_LIST, 'templates', self::MODEL_TEMPLATE_SITE))
|
||||
->setModel(new BaseList('Functions List', self::MODEL_FUNCTION_LIST, 'functions', self::MODEL_FUNCTION))
|
||||
->setModel(new BaseList('Function Templates List', self::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', self::MODEL_TEMPLATE_FUNCTION))
|
||||
->setModel(new BaseList('Installations List', self::MODEL_INSTALLATION_LIST, 'installations', self::MODEL_INSTALLATION))
|
||||
->setModel(new BaseList('Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_LIST, 'providerRepositories', self::MODEL_PROVIDER_REPOSITORY))
|
||||
->setModel(new BaseList('Branches List', self::MODEL_BRANCH_LIST, 'branches', self::MODEL_BRANCH))
|
||||
->setModel(new BaseList('Frameworks List', self::MODEL_FRAMEWORK_LIST, 'frameworks', self::MODEL_FRAMEWORK))
|
||||
->setModel(new BaseList('Runtimes List', self::MODEL_RUNTIME_LIST, 'runtimes', self::MODEL_RUNTIME))
|
||||
->setModel(new BaseList('Deployments List', self::MODEL_DEPLOYMENT_LIST, 'deployments', self::MODEL_DEPLOYMENT))
|
||||
->setModel(new BaseList('Executions List', self::MODEL_EXECUTION_LIST, 'executions', self::MODEL_EXECUTION))
|
||||
@@ -422,6 +442,9 @@ class Response extends SwooleResponse
|
||||
->setModel(new Bucket())
|
||||
->setModel(new Team())
|
||||
->setModel(new Membership())
|
||||
->setModel(new Site())
|
||||
->setModel(new TemplateSite())
|
||||
->setModel(new TemplateFramework())
|
||||
->setModel(new Func())
|
||||
->setModel(new TemplateFunction())
|
||||
->setModel(new TemplateRuntime())
|
||||
@@ -432,6 +455,7 @@ class Response extends SwooleResponse
|
||||
->setModel(new VcsContent())
|
||||
->setModel(new Branch())
|
||||
->setModel(new Runtime())
|
||||
->setModel(new Framework())
|
||||
->setModel(new Deployment())
|
||||
->setModel(new Execution())
|
||||
->setModel(new Build())
|
||||
@@ -463,6 +487,8 @@ class Response extends SwooleResponse
|
||||
->setModel(new UsageBuckets())
|
||||
->setModel(new UsageFunctions())
|
||||
->setModel(new UsageFunction())
|
||||
->setModel(new UsageSites())
|
||||
->setModel(new UsageSite())
|
||||
->setModel(new UsageProject())
|
||||
->setModel(new Headers())
|
||||
->setModel(new Specification())
|
||||
|
||||
@@ -22,7 +22,7 @@ class ConsoleVariables extends Model
|
||||
'default' => '',
|
||||
'example' => '30000000',
|
||||
])
|
||||
->addRule('_APP_FUNCTIONS_SIZE_LIMIT', [
|
||||
->addRule('_APP_COMPUTE_SIZE_LIMIT', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Maximum file size allowed for deployment in bytes.',
|
||||
'default' => '',
|
||||
@@ -51,7 +51,22 @@ class ConsoleVariables extends Model
|
||||
'description' => 'Defines if AI assistant is enabled.',
|
||||
'default' => false,
|
||||
'example' => true,
|
||||
]);
|
||||
])
|
||||
->addRule('_APP_DOMAIN_SITES', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'A domain to use for site URLs.',
|
||||
'default' => '',
|
||||
'example' => 'sites.localhost',
|
||||
])
|
||||
->addRule(
|
||||
'_APP_OPTIONS_FORCE_HTTPS',
|
||||
[
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Defines if HTTPS is enforced for all requests.',
|
||||
'default' => '',
|
||||
'example' => 'enabled',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -94,6 +94,12 @@ class Deployment extends Model
|
||||
'default' => 0,
|
||||
'example' => 128,
|
||||
])
|
||||
->addRule('domain', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Preview domain.',
|
||||
'default' => '',
|
||||
'example' => 'deploy1-project1.appwrite.site',
|
||||
])
|
||||
->addRule('providerRepositoryName', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'The name of the vcs provider repository',
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class Framework extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('key', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Parent framework key.',
|
||||
'default' => '',
|
||||
'example' => 'sveltekit',
|
||||
])
|
||||
->addRule('name', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Framework Name.',
|
||||
'default' => '',
|
||||
'example' => 'SvelteKit'
|
||||
])
|
||||
->addRule('logo', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Name of the logo image.',
|
||||
'default' => '',
|
||||
'example' => 'sveltekit.png',
|
||||
])
|
||||
->addRule('serveRuntimes', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'List of supported runtime versions.',
|
||||
'default' => '',
|
||||
'example' => 'static-1',
|
||||
'array' => true,
|
||||
])
|
||||
->addRule('buildRuntimes', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'List of supported runtime versions.',
|
||||
'default' => '',
|
||||
'example' => 'node-21.0',
|
||||
'array' => true,
|
||||
])
|
||||
->addRule('defaultServeRuntime', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Default runtime version.',
|
||||
'default' => '',
|
||||
'example' => 'static-1',
|
||||
])
|
||||
->addRule('defaultBuildRuntime', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Default runtime version.',
|
||||
'default' => '',
|
||||
'example' => 'node-22',
|
||||
])
|
||||
->addRule('defaultInstallCommand', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Default command to download dependencies.',
|
||||
'default' => '',
|
||||
'example' => 'npm install',
|
||||
])
|
||||
->addRule('defaultBuildCommand', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Default command to build site into output directory.',
|
||||
'default' => '',
|
||||
'example' => 'npm run build',
|
||||
])
|
||||
->addRule('defaultOutputDirectory', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Default output directory of build.',
|
||||
'default' => '',
|
||||
'example' => './dist',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Framework';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_FRAMEWORK;
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class Func extends Model
|
||||
])
|
||||
->addRule('runtime', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Function execution runtime.',
|
||||
'description' => 'Function execution and build runtime.',
|
||||
'default' => '',
|
||||
'example' => 'python-3.8',
|
||||
])
|
||||
@@ -155,8 +155,8 @@ class Func extends Model
|
||||
->addRule('specification', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Machine specification for builds and executions.',
|
||||
'default' => APP_FUNCTION_SPECIFICATION_DEFAULT,
|
||||
'example' => APP_FUNCTION_SPECIFICATION_DEFAULT,
|
||||
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
|
||||
'example' => APP_COMPUTE_SPECIFICATION_DEFAULT,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class Site extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('$id', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Site ID.',
|
||||
'default' => '',
|
||||
'example' => '5e5ea5c16897e',
|
||||
])
|
||||
->addRule('$createdAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Site creation date in ISO 8601 format.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('$updatedAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Site update date in ISO 8601 format.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('name', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Site name.',
|
||||
'default' => '',
|
||||
'example' => 'My Site',
|
||||
])
|
||||
->addRule('enabled', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Site enabled.',
|
||||
'default' => true,
|
||||
'example' => false,
|
||||
])
|
||||
->addRule('live', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Is the site deployed with the latest configuration? This is set to false if you\'ve changed an environment variables, entrypoint, commands, or other settings that needs redeploy to be applied. When the value is false, redeploy the site to update it with the latest configuration.',
|
||||
'default' => true,
|
||||
'example' => false,
|
||||
])
|
||||
->addRule('framework', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Site framework.',
|
||||
'default' => '',
|
||||
'example' => 'react',
|
||||
])
|
||||
->addRule('deploymentId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Site\'s active deployment ID.',
|
||||
'default' => '',
|
||||
'example' => '5e5ea5c16897e',
|
||||
])
|
||||
->addRule('vars', [
|
||||
'type' => Response::MODEL_VARIABLE,
|
||||
'description' => 'Site variables.',
|
||||
'default' => [],
|
||||
'example' => [],
|
||||
'array' => true
|
||||
])
|
||||
->addRule('timeout', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Site request timeout in seconds.',
|
||||
'default' => 15,
|
||||
'example' => 300,
|
||||
])
|
||||
->addRule('installCommand', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'The install command used to install the site dependencies.',
|
||||
'default' => '',
|
||||
'example' => 'npm install',
|
||||
])
|
||||
->addRule('buildCommand', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'The build command used to build the site.',
|
||||
'default' => '',
|
||||
'example' => 'npm run build',
|
||||
])
|
||||
->addRule('outputDirectory', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'The directory where the site build output is located.',
|
||||
'default' => '',
|
||||
'example' => 'build',
|
||||
])
|
||||
->addRule('installationId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Site VCS (Version Control System) installation id.',
|
||||
'default' => '',
|
||||
'example' => '6m40at4ejk5h2u9s1hboo',
|
||||
])
|
||||
->addRule('providerRepositoryId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'VCS (Version Control System) Repository ID',
|
||||
'default' => '',
|
||||
'example' => 'appwrite',
|
||||
])
|
||||
->addRule('providerBranch', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'VCS (Version Control System) branch name',
|
||||
'default' => '',
|
||||
'example' => 'main',
|
||||
])
|
||||
->addRule('providerRootDirectory', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Path to site in VCS (Version Control System) repository',
|
||||
'default' => '',
|
||||
'example' => 'sites/helloWorld',
|
||||
])
|
||||
->addRule('providerSilentMode', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Is VCS (Version Control System) connection is in silent mode? When in silence mode, no comments will be posted on the repository pull or merge requests',
|
||||
'default' => false,
|
||||
'example' => false,
|
||||
])
|
||||
->addRule('specification', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Machine specification for builds and executions.',
|
||||
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
|
||||
'example' => APP_COMPUTE_SPECIFICATION_DEFAULT,
|
||||
])
|
||||
->addRule('buildRuntime', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Site build runtime.',
|
||||
'default' => '',
|
||||
'example' => 'node-22',
|
||||
])
|
||||
->addRule('serveRuntime', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Site serve runtime.',
|
||||
'default' => '',
|
||||
'example' => 'static-1',
|
||||
])
|
||||
->addRule('fallbackFile', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Name of fallback file to use instead of 404 page. If null, Appwrite 404 page will be displayed.',
|
||||
'default' => null,
|
||||
'example' => 'index.html',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Site';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_SITE;
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ class Specification extends Model
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Size slug.',
|
||||
'default' => '',
|
||||
'example' => APP_FUNCTION_SPECIFICATION_DEFAULT
|
||||
'example' => APP_COMPUTE_SPECIFICATION_DEFAULT
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class TemplateFramework extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('key', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Parent framework key.',
|
||||
'default' => '',
|
||||
'example' => 'sveltekit',
|
||||
])
|
||||
->addRule('name', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Framework Name.',
|
||||
'default' => '',
|
||||
'example' => 'SvelteKit'
|
||||
])
|
||||
->addRule('installCommand', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'The install command used to install the dependencies.',
|
||||
'default' => '',
|
||||
'example' => 'npm install',
|
||||
])
|
||||
->addRule('buildCommand', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'The build command used to build the deployment.',
|
||||
'default' => '',
|
||||
'example' => 'npm run build',
|
||||
])
|
||||
->addRule('outputDirectory', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'The output directory to store the build output.',
|
||||
'default' => '',
|
||||
'example' => './build',
|
||||
])
|
||||
->addRule('providerRootDirectory', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Path to site in VCS (Version Control System) repository',
|
||||
'default' => '',
|
||||
'example' => './svelte-kit/starter',
|
||||
])
|
||||
->addRule('serveRuntime', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Runtime used during serve of template deployment.',
|
||||
'default' => '',
|
||||
'example' => 'static-1',
|
||||
])
|
||||
->addRule('buildRuntime', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Runtime used during build step of template.',
|
||||
'default' => '',
|
||||
'example' => 'node-22',
|
||||
])
|
||||
->addRule('fallbackFile', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Fallback file for SPA. Only relevant for static serve runtime.',
|
||||
'default' => null,
|
||||
'example' => 'index.html',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Template Framework';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_TEMPLATE_FRAMEWORK;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user