Compare commits

...
Author SHA1 Message Date
Khushboo Verma 44adb1dc96 Merge branch 'feat-sites' into feat-sites-insights 2024-11-28 19:51:56 +05:30
Khushboo VermaandGitHub df4c36d71b Merge pull request #9030 from appwrite/chore-sentence-case-templates
Change case to sentence case for site template titles
2024-11-26 20:03:48 +05:30
793eb53790 Update app/config/site-templates.php
Co-authored-by: Matej Bačo <matejbacocom@gmail.com>
2024-11-26 19:59:50 +05:30
Khushboo Verma 6b568e8b7f Change case to sentence case 2024-11-26 19:55:13 +05:30
Matej Bačo fec74ee9e4 Fix naming mistake 2024-11-25 18:39:24 +01:00
Matej Bačo 10ef570485 Linter fix 2024-11-25 18:22:49 +01:00
Matej Bačo d719fd9be4 Update enum specs for frameworks 2024-11-25 18:19:18 +01:00
Matej Bačo 135023b3ed Fix start issue 2024-11-25 18:15:12 +01:00
Matej Bačo 2bd2092263 Dart support 2024-11-25 17:48:15 +01:00
Matej Bačo eee7d0422c Fix paths 2024-11-22 15:45:28 +01:00
Matej Bačo 0d314c00a6 Add more defaults to frameworks 2024-11-22 14:55:52 +01:00
Matej Bačo a5fc2e0ef7 Add missing frameworks 2024-11-22 14:15:37 +01:00
Matej Bačo e168ab9ffc Update default frameworks 2024-11-22 14:08:07 +01:00
Matej Bačo 644c18fc69 Update specs 2024-11-22 12:20:12 +01:00
Matej Bačo 57717f4690 Separate starter templates, add demo URLs 2024-11-22 12:19:59 +01:00
Matej Bačo c46a9bdb45 Add all starters 2024-11-22 11:58:54 +01:00
Matej Bačo d431b53434 Switch to official starter template for sites 2024-11-22 11:52:33 +01:00
Matej Bačo 18d9ebaab3 Add template fallback file 2024-11-21 21:43:06 +01:00
Matej Bačo 40e343d17d Fix response model missing fallbackFile 2024-11-20 15:02:26 +01:00
Matej Bačo f6122157c7 Ensure latest specs after merges 2024-11-20 14:37:47 +01:00
Matej BačoandGitHub cbaad2dfb9 Merge pull request #8995 from appwrite/feat-add-fallbackFile-attribute
Add fallbackFile for SPA
2024-11-20 14:33:28 +01:00
Matej BačoandGitHub c509ec56bd Merge branch 'feat-sites' into feat-add-fallbackFile-attribute 2024-11-20 14:33:19 +01:00
Matej BačoandGitHub a25b786fca Merge pull request #9010 from appwrite/chore-add-site-domains-var
Add _APP_DOMAIN_SITES to console variables
2024-11-20 14:33:02 +01:00
Matej Bačo 06224aa34e formatting fix 2024-11-20 14:19:15 +01:00
Matej Bačo 4bcbd49d6d Formatting fix 2024-11-20 14:18:43 +01:00
Matej BačoandGitHub 09e2ded206 Merge branch 'feat-sites' into feat-add-fallbackFile-attribute 2024-11-20 14:14:28 +01:00
Matej BačoandGitHub 4a8c063d51 Merge branch 'feat-sites' into chore-add-site-domains-var 2024-11-20 14:13:39 +01:00
Matej Bačo 2e9e6d38e2 Fix site URLs 2024-11-20 13:48:42 +01:00
Matej Bačo 53917d6d90 Update specs 2024-11-20 12:25:28 +01:00
Matej Bačo d20714eed6 Merge branch 'feat-sites' into chore-add-site-domains-var 2024-11-20 12:21:13 +01:00
Matej Bačo 2f328bf4cf Fix double rule IDs 2024-11-20 12:20:29 +01:00
Matej Bačo e3c4cdb040 Update template-runtimes.php 2024-11-20 12:10:09 +01:00
Matej Bačo c8cf645414 Merge branch '1.6.x' into feat-sites 2024-11-20 12:07:13 +01:00
Torsten Dittmann 0b11e0cc92 fix: gitignore merge conflict leftover 2024-11-20 11:52:27 +01:00
Torsten DittmannandGitHub a5d43ddf71 Merge pull request #9011 from appwrite/sync-1.6.x
sync: 1.6.x
2024-11-20 11:48:37 +01:00
Khushboo Verma aa47d9f07a Add _APP_OPTIONS_FORCE_HTTPS to console vars 2024-11-20 16:12:35 +05:30
Torsten Dittmann 15a5f8690d sync: 1.6.x with main 2024-11-20 11:33:38 +01:00
Torsten Dittmann 9901c781b7 Merge branch 'main' of https://github.com/appwrite/appwrite into sync-1.6.x 2024-11-20 11:31:52 +01:00
Khushboo Verma 338559967a Update var count in tests 2024-11-20 14:52:03 +05:30
Khushboo Verma a736a42e26 Update test 2024-11-20 14:46:04 +05:30
Khushboo Verma 00c6549610 Add _APP_DOMAIN_SITES to console variables 2024-11-20 14:44:22 +05:30
Matej BačoandGitHub 510d871379 Merge pull request #9001 from appwrite/chore-move-preview-url-to-create-deployment
Create preview url rule when deployment is created
2024-11-20 09:52:03 +01:00
Eldad A. FuxandGitHub ee3d34310a Merge pull request #9005 from appwrite/base-0.9.5
chore: update base to 0.9.5
2024-11-19 17:15:43 +01:00
Fabian Gruber 877a978451 chore: update base to 0.9.5 2024-11-19 16:42:16 +01:00
Khushboo Verma 43e2d7454a Create preview url rule when deployment is created 2024-11-18 20:27:07 +05:30
Jake BarnbyandGitHub d49b0540fd Merge pull request #8978 from appwrite/unset-index-length
Unset index length
2024-11-18 17:05:00 +13:00
Khushboo Verma c31634e64d Fix typo 2024-11-18 00:39:17 +05:30
Khushboo Verma 3a58e5927b Merge branch 'feat-sites' into feat-sites-insights 2024-11-18 00:31:28 +05:30
Khushboo Verma cb359e40ba Add fallbackFile for SPA 2024-11-18 00:23:45 +05:30
Torsten DittmannandGitHub 8e02115828 ci: console sdk previews (#8990) 2024-11-15 13:27:46 +01:00
Steven NguyenandGitHub 3a748fbd35 Merge pull request #8972 from appwrite/fix-remove-duplicate-dart-216-runtime-template
Remove duplicate dart-2.16 runtime template
2024-11-14 12:33:33 -08:00
fogelito 7e7a4658cd revert lock 2024-11-14 14:20:28 +02:00
fogelito abf97e391e formatting 2024-11-14 14:19:33 +02:00
fogelito d8799e5a9c Remove comment 2024-11-14 14:13:28 +02:00
fogelito 217e471a41 Merge branch '1.6.x' of https://github.com/appwrite/appwrite into unset-index-length 2024-11-14 14:12:03 +02:00
fogelito 72e491ac69 Remove unsetting index length for strings 2024-11-14 14:11:36 +02:00
Jake BarnbyandGitHub bdb0b188ef Merge pull request #8971 from appwrite/fix-alter-attributes
Fix Patch attribute
2024-11-14 20:55:59 +13:00
Jake BarnbyandGitHub 191f884772 Merge pull request #8959 from appwrite/fix-send-migration-errors-individually
Send migration errors individually
2024-11-14 19:57:27 +13:00
Steven NguyenandGitHub 45f85d6b4d fix: remove duplicate dart-2.16 runtime template 2024-11-13 11:30:48 -08:00
fogelito 8f845aef53 Merge branch '1.6.x' of https://github.com/appwrite/appwrite into fix-alter-attributes
# Conflicts:
#	src/Appwrite/Platform/Workers/Databases.php
2024-11-13 18:34:57 +02:00
fogelito 68e96e9851 comment 2024-11-13 18:32:20 +02:00
Eldad A. FuxandGitHub c6cf27bd6f Merge pull request #8960 from appwrite/PLA-1854
feat: add telemetry
2024-11-13 14:08:45 +01:00
fogelito 71f2c6c131 Remove purgeCachedCollection 2024-11-13 14:55:56 +02:00
Fabian Gruber 1db71a46ea chore: use telemetry 0.1.0 2024-11-13 13:52:41 +01:00
Fabian Gruber 8124f6998e feat: add telemetry 2024-11-13 13:52:41 +01:00
Torsten DittmannandGitHub 7f06b0ec76 Merge pull request #8969 from appwrite/fix-memberships-privacy-mfa
fix: memberships privacy mfa
2024-11-13 12:36:57 +01:00
loks0n ca533f40c9 fix: memberships-privacy mfa only 2024-11-13 10:45:50 +01:00
Khushboo Verma 2ad0428727 Merge 1.6.x into feat-sites 2024-11-13 13:18:21 +05:30
Bradley Schofield 54db5655a1 Run Linter 2024-11-13 13:42:49 +09:00
Bradley Schofield 91cc0e627a Address Comments 2024-11-13 10:29:43 +09:00
Bradley Schofield f1af8b330e Merge remote-tracking branch 'origin/1.6.x' into fix-send-migration-errors-individually 2024-11-13 09:13:15 +09:00
Bradley Schofield 29e7d1f7d0 Address Comments 2024-11-13 09:13:00 +09:00
Steven NguyenandGitHub 97bcccba85 Merge pull request #8963 from appwrite/remove-inaccurate-doc
Remove inaccurate info about leaving the URL parameter empty
2024-11-12 15:43:56 -08:00
Ebenezer DonandGitHub 7f3a342940 Remove inaccurate info about leaving the URL parameter empty 2024-11-12 16:44:20 +00:00
Torsten DittmannandGitHub cca5606b70 Merge pull request #8943 from appwrite/feat-realtime-ping-pong
feat: realtime ping pong
2024-11-12 14:09:58 +01:00
Bradley Schofield 1337ea77b1 Fix Optionals 2024-11-12 16:18:09 +09:00
Bradley Schofield a460a41fe5 Run Linter 2024-11-12 11:33:13 +09:00
Bradley Schofield b62640489d Fire logs for each individual migration error 2024-11-12 11:29:19 +09:00
Jake BarnbyandGitHub f9e671419b Merge pull request #8944 from appwrite/database-worker-errors
Database worker errors
2024-11-12 13:23:24 +13:00
fogelito fcf018580c Merge branch '1.6.x' of https://github.com/appwrite/appwrite into database-worker-errors 2024-11-11 18:53:36 +02:00
fogelito 8cca88f2b8 Remove Sentry comment 2024-11-11 18:52:15 +02:00
fogelito bae6e465fa updateAttributes 2024-11-11 18:46:47 +02:00
Jake BarnbyandGitHub 3846903b9d Merge pull request #8956 from appwrite/fix-migration-id-null
Remove double fetch from migrations worker
2024-11-11 22:13:57 +13:00
Bradley Schofield d9e2ecff54 Remove get document from worker 2024-11-11 16:45:17 +09:00
fogelito 06e3afcf88 Merge branch '1.6.x' of https://github.com/appwrite/appwrite into database-worker-errors 2024-11-10 16:42:34 +02:00
fogelito 0ef28eba17 Merge remote-tracking branch 'origin/database-worker-errors' into database-worker-errors 2024-11-10 16:38:04 +02:00
fogelito e5730506eb Remove var dumps 2024-11-10 16:37:44 +02:00
Christy JacobandGitHub 59c233b1e5 Merge pull request #8951 from appwrite/update-rules-fetching
feat: use getDocument instead of find() for rules
2024-11-09 17:00:28 +01:00
Christy Jacob 9334002214 Merge branch '1.6.x' of github.com:appwrite/appwrite into update-rules-fetching 2024-11-09 16:35:03 +01:00
Christy Jacob ba91d90c40 chore: linter 2024-11-09 16:31:33 +01:00
Christy Jacob 6c7f484383 chore: update more find methods 2024-11-09 16:18:22 +01:00
Christy Jacob e8ddda079d chore: update more find methods 2024-11-09 16:13:44 +01:00
Christy Jacob e04551ab6e feat: adjust more proxy queries 2024-11-09 11:43:52 +01:00
Christy Jacob e4cd914d57 feat: downgrade db library 2024-11-09 11:34:56 +01:00
Christy JacobandGitHub 275bccb9b1 Merge pull request #8948 from appwrite/fix-setpaused
fix: setpaused
2024-11-09 10:33:16 +01:00
Christy Jacob 6ae9952fa6 chore: linter 2024-11-08 22:18:05 +01:00
Christy Jacob 5f29af2c27 feat: use getDocument instead of find() for rules 2024-11-08 22:08:13 +01:00
Christy JacobandGitHub 3dff15f7cd Merge pull request #8949 from appwrite/revert-8946-chore-update-database
Revert "Update database"
2024-11-08 22:02:20 +01:00
Christy JacobandGitHub b1a1ad618a Revert "Update database" 2024-11-09 01:01:14 +04:00
loks0n af448ac183 feat: setpaused 2024-11-08 21:55:53 +01:00
Christy JacobandGitHub cf55bf95ce Merge pull request #8947 from appwrite/feat-configurable-compression-minimum-size
feat: configurable compression minimum size
2024-11-08 20:20:55 +01:00
loks0n 532aac6a62 chore: dockerfiles 2024-11-08 20:00:05 +01:00
loks0n 428998b6b6 chore: fix 2024-11-08 19:50:20 +01:00
loks0n d0cf0f6d1d feat: min size env 2024-11-08 19:48:32 +01:00
Christy JacobandGitHub ee2d0c0b25 Merge pull request #8938 from appwrite/feat-use-utopia-compression
feat: use utopia compression
2024-11-08 16:52:53 +01:00
loks0n d37c0500fc chore: bump framework, remove vender mount 2024-11-08 16:11:30 +01:00
loks0n cd817dbc0f chore: bump framework 2024-11-08 16:04:33 +01:00
loks0n 4b9457acc5 chore: bump compression 2024-11-08 16:03:32 +01:00
loks0n 56c43560f1 chore: composer update 2024-11-08 13:41:29 +01:00
loks0n fb42cbcade Merge branch '1.6.x' of https://github.com/appwrite/appwrite into feat-use-utopia-compression 2024-11-08 13:39:13 +01:00
loks0n c8c4dd28df chore: composer update 2024-11-08 13:28:01 +01:00
loks0n e43887c53b feat: remove verbose 2024-11-08 13:06:09 +01:00
loks0n ee4b54857f fix: tests 2024-11-08 13:01:18 +01:00
Jake Barnby 3b82719b73 Merge remote-tracking branch 'origin/1.6.x' into database-worker-errors 2024-11-08 23:33:31 +13:00
Jake BarnbyandGitHub c0bd1664be Merge pull request #8946 from appwrite/chore-update-database
Update database
2024-11-08 23:32:45 +13:00
Jake Barnby 78c4bb395c Fix test 2024-11-08 21:59:42 +13:00
Jake Barnby 14532acb37 Update limit errors 2024-11-08 21:43:25 +13:00
Jake Barnby a17e722dc7 Update database 2024-11-08 20:19:40 +13:00
Jake BarnbyandGitHub b42dd86451 Merge pull request #8891 from appwrite/fix-xdebug-extension-enabled
Fix XDebug Extension not being removed
2024-11-08 18:12:36 +13:00
fogelito 06274764c6 Remove extra lines 2024-11-08 07:12:23 +02:00
Bradley Schofield d33056d13b Address Comments 2024-11-08 13:48:41 +09:00
loks0n 83cbddb926 fix: test 2024-11-07 20:31:12 +01:00
Christy JacobandGitHub d3bb5d963b Merge pull request #8913 from appwrite/feat-list-memberships-as-client
feat: list memberships as client
2024-11-07 20:28:46 +01:00
loks0n 21e946e9b6 fix: tests 2024-11-07 19:12:41 +01:00
loks0n 33a441421e chore: enabled privacy by default 2024-11-07 18:23:13 +01:00
loks0n 71eba6a879 fix: tests 2024-11-07 18:21:41 +01:00
loks0n e212481262 chore: bump composer 2024-11-07 16:50:33 +01:00
fogelito fb40970eba line 2024-11-07 17:35:03 +02:00
fogelito cd0d1d94db Add to finally 2024-11-07 17:27:56 +02:00
fogelito 19667f78a9 Add try catch on delete Attribute 2024-11-07 17:23:46 +02:00
loks0n 31062aa3ee fix: specs 2024-11-07 15:54:49 +01:00
loks0n 25adb39634 chore: update model 2024-11-07 15:37:34 +01:00
loks0n c19d6a6dda chore: description 2024-11-07 15:33:44 +01:00
fogelito a6779caa5e Trigger sentry 2024-11-07 14:28:39 +02:00
Torsten Dittmann cddd16b753 Merge branch '1.6.x' of https://github.com/appwrite/appwrite into feat-realtime-ping-pong 2024-11-07 13:10:23 +01:00
Torsten Dittmann f91f382baf style: remove unnecessary comment 2024-11-07 12:08:11 +01:00
Torsten Dittmann 4ee9e9730f feat: realtime ping pong 2024-11-07 12:05:37 +01:00
loks0n 5b4d51f6dd chore: composer update 2024-11-07 11:30:59 +01:00
loks0n e7e1a5b11f fix: tests 2024-11-07 11:13:20 +01:00
loks0n d4de44429f fix: tests 2024-11-07 10:33:40 +01:00
loks0n b7397617e6 fix: tests 2024-11-07 10:19:22 +01:00
loks0n afbdca3fc3 chore: specs 2024-11-06 21:21:38 +01:00
loks0n 99565cae99 feat: improve copy 2024-11-06 21:21:21 +01:00
loks0n e4c6e97ded feat: memberships privacy 2024-11-06 21:15:31 +01:00
loks0n 35c2978b74 fix: test warning 2024-11-06 19:01:18 +01:00
loks0n ce36392a37 chore: bump framework 2024-11-06 18:56:40 +01:00
loks0n a6ae565093 fix: tests 2024-11-06 17:54:02 +01:00
loks0n 617004c914 Merge branch 'feat-list-memberships-as-client' of https://github.com/appwrite/appwrite into feat-list-memberships-as-client 2024-11-06 16:52:41 +01:00
loks0n ff711d5ff9 chore: update specs 2024-11-06 16:52:37 +01:00
Luke B. SilverandGitHub bb174b19b4 Merge branch '1.6.x' into feat-list-memberships-as-client 2024-11-06 16:49:08 +01:00
loks0n cb70a51adb fix: tests 2024-11-06 16:35:31 +01:00
loks0n 7fd5f8c934 docs 2024-11-06 16:00:27 +01:00
loks0n 12b60e01cf fix: tests 2024-11-06 15:51:15 +01:00
Christy JacobandGitHub 0a8f62fd09 Merge pull request #8905 from appwrite/redis-cluster
feat: PubSub adapter support
2024-11-06 12:38:01 +01:00
Fabian Gruber b410b17ba6 feat: redis-cluster support 2024-11-06 12:27:03 +01:00
Jake BarnbyandGitHub d7c64b6557 Merge pull request #8929 from appwrite/validate-string-permissions
Validate string permissions
2024-11-06 22:12:05 +13:00
fogelito 231c181dc8 Merge branch '1.6.x' of https://github.com/appwrite/appwrite into validate-string-permissions 2024-11-06 09:41:44 +02:00
fogelito 46af6dc3f0 Fix permissions 2024-11-06 09:36:27 +02:00
Jake BarnbyandGitHub a24fb66909 Merge pull request #8927 from appwrite/chore-update-database
Update database for transaction counter fixes with retries
2024-11-06 16:47:31 +13:00
Jake Barnby 59674fa908 Update database 2024-11-06 16:32:31 +13:00
Jake Barnby 90f63a30f6 Revert "Fix trivy scans"
This reverts commit 0f6aa3d5b1.
2024-11-06 16:24:55 +13:00
Jake Barnby 0f6aa3d5b1 Fix trivy scans 2024-11-06 15:45:26 +13:00
Jake Barnby c6b297dc82 Update database for transaction counter fixes with retries 2024-11-06 15:05:04 +13:00
loks0n d27a7189c7 fix: stash 2024-11-05 19:46:22 +01:00
Christy JacobandGitHub 51274ba709 Merge pull request #8718 from appwrite/fix-oauth-trigger-create-user-event
fix: trigger create user event
2024-11-05 19:39:13 +01:00
loks0n 0e254618f6 fix: tests 2024-11-05 18:37:48 +01:00
loks0n 6bd641d70e fix: default 2024-11-05 18:11:06 +01:00
loks0n bd94025402 fix: typo 2024-11-05 18:07:08 +01:00
loks0n 6fd54d646a fix: tests 2024-11-05 18:05:33 +01:00
loks0n 9d1f366161 fix: console 2024-11-05 17:59:14 +01:00
loks0n 71879236b8 fix: disable test 2024-11-05 17:42:01 +01:00
loks0n 8ecc2058a1 fix: tests 2024-11-05 17:19:56 +01:00
Luke B. SilverandGitHub 65fa578c3a Merge branch '1.6.x' into feat-list-memberships-as-client 2024-11-05 17:07:00 +01:00
loks0n 69e8fdf077 chore: refactor 2024-11-05 17:05:21 +01:00
loks0n 229fe9ecd5 fix: tests 2024-11-05 15:50:32 +01:00
loks0n ab7e454292 fix: event name 2024-11-05 15:17:41 +01:00
loks0n 2dea9b1f51 chore: clone 2024-11-05 14:35:35 +01:00
Luke B. SilverandGitHub 5c9d81f659 Merge branch '1.6.x' into fix-oauth-trigger-create-user-event 2024-11-05 14:24:02 +01:00
Jake BarnbyandGitHub 477d6b52be Merge pull request #8923 from appwrite/chore-update-database
Update database
2024-11-05 23:38:03 +13:00
Jake Barnby 540e997fdb Update database 2024-11-05 23:15:08 +13:00
loks0n e9a0f47711 fix: event test 2024-11-05 09:55:56 +01:00
loks0n 20d8a702a1 chore: refactor 2024-11-04 15:20:43 +00:00
Luke B. SilverandGitHub 1ff21034a9 Merge branch '1.6.x' into fix-oauth-trigger-create-user-event 2024-11-04 15:07:31 +00:00
loks0n d2deca7f1f feat: refactor events 2024-11-04 15:05:54 +00:00
Jake BarnbyandGitHub 5d4ae8075a Merge pull request #8917 from appwrite/chore-update-database
Update database
2024-11-04 22:43:42 +13:00
Jake Barnby 4489d5f77f Fix validator 2024-11-04 22:13:38 +13:00
Jake Barnby a00671fe29 Lint 2024-11-04 21:53:00 +13:00
Jake Barnby 6bba134650 Set min/max for datetime validator 2024-11-04 21:50:22 +13:00
Jake Barnby de2ce16f10 Fix non-variable passed by reference 2024-11-04 21:49:20 +13:00
Jake Barnby b6287e11a3 Add storage catches 2024-11-04 21:40:02 +13:00
Jake Barnby c59cd97eb8 Not found exception 2024-11-04 20:57:08 +13:00
Jake Barnby ca5d9ce96a Update database 2024-11-04 15:09:16 +13:00
Jake BarnbyandGitHub f9813b86ac Merge pull request #8890 from appwrite/fix-update-size-validator
Fix validator usage for updating string size
2024-11-04 14:59:09 +13:00
loks0n ff95532ccb chore: refactor 2024-11-01 16:29:34 +00:00
loks0n e87ed0e13a chore: refactor use from over clonse 2024-11-01 15:24:29 +00:00
loks0n 4c14375d89 fix: tests 2024-11-01 13:31:33 +00:00
loks0n 37d60e1039 fix: var name 2024-11-01 13:28:13 +00:00
loks0n f5e2f4acfa fix: endpoint name 2024-11-01 12:53:54 +00:00
loks0n c9b2d6eb90 feat: teamHideSensitiveFields 2024-11-01 11:38:56 +00:00
Luke B. SilverandGitHub ab3b7d3028 Merge branch '1.6.x' into fix-oauth-trigger-create-user-event 2024-10-31 10:39:42 +00:00
Jake BarnbyandGitHub 5be76494c2 Merge pull request #8680 from appwrite/chore-update-databases-findone
Update Database lib
2024-10-31 22:07:31 +13:00
Bradley Schofield a30dd72b0f Remove unnecessary repo in composer.json 2024-10-31 18:06:07 +09:00
Bradley Schofield 2f64620cb5 Use actual package 2024-10-31 17:27:13 +09:00
Bradley Schofield 40b8af0671 Further cleanup code now findOne returns empty doc 2024-10-31 17:13:23 +09:00
Bradley Schofield e15ac03c59 Merge branch '1.6.x' into chore-update-databases-findone
# Conflicts:
#	composer.json
#	composer.lock
2024-10-31 16:57:45 +09:00
loks0n 51ae3a6475 fix: tests 2024-10-30 18:31:14 +00:00
loks0n 2736f6009a feat: dirty solution 2024-10-30 18:18:37 +00:00
loks0n 401996e7c2 Merge branch 'fix-oauth-trigger-create-user-event' of https://github.com/appwrite/appwrite into fix-oauth-trigger-create-user-event 2024-10-30 18:01:24 +00:00
loks0n 26c53bcf9c feat: clone queue 2024-10-30 18:01:12 +00:00
Luke B. SilverandGitHub 1594dc8925 Merge branch '1.6.x' into fix-oauth-trigger-create-user-event 2024-10-30 11:45:38 +00:00
Luke B. SilverandGitHub e89603c479 Merge branch '1.6.x' into fix-oauth-trigger-create-user-event 2024-10-29 12:18:21 +00:00
loks0n 0b35d51547 Merge branch '1.6.x' of https://github.com/appwrite/appwrite into fix-oauth-trigger-create-user-event 2024-10-29 12:16:47 +00:00
loks0n 9d4fd41701 fix: webhook test 2024-10-29 11:56:41 +00:00
loks0n 13d1376e28 fix: type 2024-10-29 11:20:52 +00:00
loks0n 3020c0beea fix: missing dep 2024-10-29 11:01:29 +00:00
loks0n d16251d261 fix: remove old create user events 2024-10-29 10:58:57 +00:00
loks0n 5afa8c6158 feat: usage db listener 2024-10-29 10:51:40 +00:00
Bradley Schofield 943a3a7fdd Remove correct version of XDebug 2024-10-29 17:20:15 +09:00
Jake Barnby eacb55cdcc Fix validator usage for updating string size 2024-10-29 19:04:37 +13:00
Luke B. SilverandGitHub 61627337fa Merge branch 'main' into fix-oauth-trigger-create-user-event 2024-10-28 18:40:21 +00:00
Matej BačoandGitHub 6874b4f6fb Merge pull request #8885 from appwrite/feat-add-sites-comments
Feat add sites comments
2024-10-28 15:47:16 +01:00
Matej BačoandGitHub eeba650f6c Merge branch 'feat-sites' into feat-add-sites-comments 2024-10-28 15:43:46 +01:00
Matej Bačo 54b07e6b99 Add protocol 2024-10-28 15:24:34 +01:00
Khushboo Verma dc8b9478ee Fix builds 2024-10-28 15:20:36 +01:00
Torsten DittmannandGitHub d97b02add9 Merge pull request #8884 from appwrite/fix-sites-get-template
Fix: sites get template missing key
2024-10-28 15:07:10 +01:00
Matej Bačo 8427ade0fa Fix sites get template missing key 2024-10-28 15:04:48 +01:00
Khushboo Verma 44d6a0dea1 Merge branch 'feat-add-sites-comments' of github.com:appwrite/appwrite into feat-add-sites-comments 2024-10-28 15:02:00 +01:00
Khushboo Verma f710ce266b Merge branch 'feat-sites' into feat-add-sites-comments 2024-10-28 14:54:08 +01:00
Matej Bačo 45dc9a39d9 Get preview URL 2024-10-28 14:53:06 +01:00
Matej Bačo d703a118c4 Merge branch 'feat-sites' into feat-add-sites-comments 2024-10-28 14:52:07 +01:00
Matej Bačo c20e580a9e Fix missing params in VCS comment flow 2024-10-28 14:45:18 +01:00
Matej Bačo 9b4de5d7bf Fix comments 2024-10-28 14:39:12 +01:00
Matej BačoandGitHub 7c9f396b40 Merge pull request #8883 from appwrite/feat-sites-logs-styled
Feat: Styled&timestamped logs
2024-10-28 14:35:30 +01:00
Matej Bačo 31b9f2b4f9 Fix comments 2024-10-28 14:19:44 +01:00
Khushboo Verma 6ff15f9b53 WIP: GitHub Comments for Sites 2024-10-28 14:00:23 +01:00
Matej BačoandGitHub 1ebd6c252c Merge branch 'feat-sites' into feat-sites-logs-styled 2024-10-28 12:27:06 +01:00
Matej BačoandGitHub 8424351d49 Merge pull request #8882 from appwrite/fix-sites-logs-realtime
Fix: sites deployment logs realtime
2024-10-28 12:26:54 +01:00
Matej Bačo 0c4e597819 Implemented styled and timestamped logs, fix cosistency 2024-10-28 12:22:34 +01:00
Matej BačoandGitHub d1917d1a2b Merge branch 'feat-sites' into fix-sites-logs-realtime 2024-10-28 12:08:27 +01:00
Matej Bačo bb3503f0c4 Fix sites deployment logs realtime 2024-10-28 12:00:06 +01:00
Torsten DittmannandGitHub 7c22776254 Merge pull request #8880 from appwrite/fix-deployment-domains
Fix: deployment domain incorrect
2024-10-28 11:59:21 +01:00
Matej BačoandGitHub 2294586371 Merge branch 'feat-sites' into fix-deployment-domains 2024-10-28 11:21:08 +01:00
Torsten DittmannandGitHub b7f519f86e Merge pull request #8879 from appwrite/fix-check-domain-availability
Throw exception for error
2024-10-28 11:19:04 +01:00
Matej Bačo febd5294f4 Ensure function executor is ready 2024-10-28 11:18:15 +01:00
Matej Bačo 690a23fe89 Fix deployments showing wrong domains 2024-10-28 11:17:44 +01:00
Khushboo Verma 39c1a88dae Throw exception for error 2024-10-28 10:55:58 +01:00
Khushboo VermaandGitHub d0808e0c1a Merge pull request #8878 from appwrite/fix-sites-notice
Fix: unwanted notice
2024-10-27 22:11:50 +01:00
Matej Bačo 34fd22e4e9 Fix unwanted object instead of array 2024-10-27 21:49:44 +01:00
Matej Bačo d4fb0d37c2 Fix notice 2024-10-27 21:44:59 +01:00
Matej BačoandGitHub fd02ca00f0 Merge pull request #8877 from appwrite/feat-preview-deployments
Feat: Implement preview deployments
2024-10-27 21:25:27 +01:00
Matej Bačo 8a53d02d0c Remove leftover 2024-10-27 21:25:12 +01:00
Matej Bačo 58ee53876b Implement preview deployments 2024-10-27 21:10:53 +01:00
Matej BačoandGitHub 853b730def Merge pull request #8875 from appwrite/chore-add-build-runtime
Add build and serve runtime to updateSite
2024-10-27 19:17:13 +01:00
Khushboo Verma be5fdbfd4f Add build and serve runtime to updateSite 2024-10-27 18:54:20 +01:00
Khushboo VermaandGitHub e6d123b5e8 Merge pull request #8873 from appwrite/chore-rename-common-vars-to-compute
Use compute vars for functions and sites
2024-10-27 18:35:27 +01:00
Khushboo Verma d8a844a2f9 Fix functions tests 2024-10-27 18:14:47 +01:00
Khushboo Verma dd87cdbebf Fix remaining vars 2024-10-27 18:04:40 +01:00
Khushboo Verma 4c1881324e Use compute vars for functions and sites 2024-10-27 17:22:45 +01:00
Matej BačoandGitHub bd27a36d16 Merge pull request #8872 from appwrite/fix-site-leftovers
Fix: leftover structure
2024-10-27 16:58:12 +01:00
Matej Bačo 2f999ba3db Fix crashing containers 2024-10-27 16:34:49 +01:00
Matej Bačo d8ec390cd6 Merge branch 'feat-sites' into fix-site-leftovers 2024-10-27 16:22:53 +01:00
Matej Bačo 7a0e7bfb95 Add runtimes to templates 2024-10-27 16:15:49 +01:00
Khushboo VermaandGitHub da7ae6b953 Merge pull request #8863 from appwrite/feat-sites-endpoints
Add timeout and add sites scopes
2024-10-27 16:15:00 +01:00
Khushboo Verma 2dead545f0 Resolve PR comments 2024-10-27 15:51:17 +01:00
Matej Bačo 9c566ec17c Merge branch 'feat-sites' into fix-site-leftovers 2024-10-27 15:49:53 +01:00
Matej Bačo 385fe303cc Fix leftover fields and attributes 2024-10-27 15:48:39 +01:00
Khushboo Verma 81e3709d25 Remove scopes from updateSite endpoint 2024-10-27 15:48:06 +01:00
Khushboo Verma 7f17efcdd8 Resolve merge conflicts 2024-10-27 15:43:52 +01:00
Khushboo VermaandGitHub a566ac1078 Merge pull request #8865 from appwrite/feat-check-domain-availability
Add custom domain param in create site endpoint
2024-10-27 15:38:14 +01:00
Khushboo Verma 44e76b4eaa Add a todo 2024-10-27 15:35:18 +01:00
Khushboo Verma e7e1cfa12e Update desc 2024-10-27 15:32:29 +01:00
Khushboo Verma 77a70cad71 Add endpoint to check subdomain availability 2024-10-27 15:31:00 +01:00
Khushboo Verma 0c2db53ee8 Address PR Comments 2024-10-27 14:11:53 +01:00
Khushboo Verma 83807ed247 Fix projects tests 2024-10-27 12:56:56 +01:00
Torsten Dittmann 8e8d71df35 Merge branch 'feat-sites-insights' of https://github.com/appwrite/appwrite into feat-sites-insights 2024-10-27 11:52:43 +01:00
Torsten Dittmann d03d21b0d4 chore: add todos 2024-10-27 11:52:38 +01:00
Khushboo Verma 302a135e5a Fix health tests 2024-10-27 11:52:22 +01:00
Torsten DittmannandGitHub b79daa2ee9 Merge branch 'feat-sites' into feat-sites-insights 2024-10-27 11:49:11 +01:00
Torsten Dittmann 94a5f5fff9 feat: insights 2024-10-27 11:46:47 +01:00
Khushboo Verma 6b2160f388 Fix vcs deployments 2024-10-27 11:33:56 +01:00
Khushboo Verma 86a68c50bc Rename subdomain 2024-10-27 11:32:43 +01:00
Khushboo Verma e2e7d6578c Resolve merge conflicts 2024-10-27 11:30:17 +01:00
Matej BačoandGitHub a171e17666 Merge pull request #8860 from appwrite/feat-sites-static
Feat: Static sites
2024-10-26 21:26:48 +02:00
Matej Bačo d8610e0f2a Upgrade executor to fix tests 2024-10-26 18:57:37 +00:00
Matej Bačo 93222bf3f9 Fix response format; formatting 2024-10-26 20:47:43 +02:00
Matej Bačo ea6765c68a Support static sites 2024-10-26 18:43:30 +02:00
Khushboo Verma 6ac81dea19 Check if subdomain exists before creating site 2024-10-26 18:34:58 +02:00
Khushboo Verma e4dd9bc9f9 Use custom domain in create site endpoint 2024-10-26 17:07:15 +02:00
Khushboo Verma 03d4d4c8fa Add build env vars for sites 2024-10-26 12:29:42 +02:00
Khushboo Verma fcc51d3352 Add sites build timeout env var 2024-10-26 11:14:55 +02:00
Khushboo Verma 08630cd686 Add timeout and add sites scopes 2024-10-26 11:01:14 +02:00
Matej Bačo 9295a4e7ce Update depenrencies 2024-10-25 19:25:51 +02:00
Matej BačoandGitHub 826a1a02a2 Merge pull request #8844 from appwrite/feat-site-endpointsa
Add new endpoints in HTTP structure
2024-10-25 18:46:11 +02:00
Khushboo Verma 4ed08cefa7 Fix getTemplate endpoint 2024-10-25 18:04:32 +02:00
Khushboo Verma 1a33a0c4d1 Fix range and frameworks 2024-10-25 16:23:29 +02:00
Khushboo Verma 4d3d710211 Add missing group to templates API 2024-10-25 16:05:45 +02:00
Khushboo Verma ac93e70dd2 Update listframeworks endpoint 2024-10-25 15:56:28 +02:00
Khushboo Verma dd3ffbb391 Add getTemplate endpoint for sites 2024-10-25 14:55:09 +02:00
Khushboo Verma 9d1d101586 Update VCS endpoin to work for both functions and sites 2024-10-25 14:41:14 +02:00
Khushboo VermaandGitHub 32da7b8fe2 Merge pull request #8852 from appwrite/fix-sites-commands-and-hardcoded
fix: commands and todos
2024-10-25 11:59:02 +02:00
Torsten Dittmann 41484113a0 chore: add comments 2024-10-25 10:55:31 +02:00
Torsten Dittmann 5d6a8be66d fix: function command 2024-10-24 19:43:12 +02:00
Torsten Dittmann aba3a31663 fix: deployment/deploymentId 2024-10-24 19:28:20 +02:00
Torsten Dittmann 8950f71b0a revert: comments 2024-10-24 19:24:24 +02:00
Torsten Dittmann 54d3668693 fix: todos for sites 2024-10-24 19:20:00 +02:00
Khushboo Verma b16169f867 Add secret to templateVariable model 2024-10-24 18:09:43 +02:00
Khushboo Verma bbd589f6a8 Add download build endpoint to sites 2024-10-24 18:02:26 +02:00
Khushboo Verma 765d8241f2 Add secret param to site variables 2024-10-24 16:32:53 +02:00
Khushboo Verma 8e041ddeb6 Merge branch 'feat-sites' into feat-site-endpointsa 2024-10-24 15:24:37 +02:00
Khushboo Verma a4a7fe0c48 Merge branch '1.7.x' into feat-sites 2024-10-24 15:24:25 +02:00
Khushboo Verma a247fc3b48 Merge branch 'feat-site-endpointsa' of github.com:appwrite/appwrite into feat-site-endpointsa 2024-10-24 15:22:16 +02:00
Khushboo Verma d876085f6d Add usage endpoints 2024-10-24 15:21:40 +02:00
Torsten DittmannandGitHub bba9c3d57a Merge pull request #8831 from appwrite/feat-add-secret-environment-vars
Add secret attribute to variables
2024-10-24 12:53:10 +02:00
Khushboo VermaandGitHub 944e971124 Merge pull request #8849 from appwrite/feat-site-static-runtime
feat: static runtime
2024-10-24 12:38:57 +02:00
Torsten Dittmann e512ab1816 Merge branch 'feat-site-static-runtime' of https://github.com/appwrite/appwrite into feat-site-static-runtime 2024-10-24 12:19:19 +02:00
Torsten Dittmann 83b5aecbfd chore: add todo comment 2024-10-24 12:19:10 +02:00
Khushboo Verma 2f9d00fc07 Update the description for secret var 2024-10-24 12:08:33 +02:00
Khushboo VermaandGitHub 23b57692ec Merge branch 'feat-site-endpointsa' into feat-site-static-runtime 2024-10-24 12:06:02 +02:00
Khushboo Verma 8bda7f1e1e Add listSiteTemplates endpoint and response models 2024-10-24 11:47:09 +02:00
Khushboo Verma 12bddc3b1f Change secret type to bool 2024-10-24 10:59:47 +02:00
Khushboo Verma 8f5fa84996 Add variable endpoints for sites 2024-10-24 10:49:43 +02:00
Khushboo VermaandGitHub 3af34f078c Merge branch 'feat-site-endpointsa' into feat-site-static-runtime 2024-10-23 18:37:45 +02:00
Torsten Dittmann 57e596fea2 feat: static runtime for site 2024-10-23 18:36:26 +02:00
Khushboo Verma 9793cf4ece Add all deployment endpoints for sites 2024-10-23 17:07:28 +02:00
Khushboo Verma a4bdc23aef Add more endpoints and models for sites 2024-10-23 12:19:07 +02:00
Khushboo Verma 4240089d41 Add create deployment endpoint 2024-10-22 19:45:49 +02:00
Khushboo VermaandGitHub 09ebfac307 Merge pull request #8843 from appwrite/fix-builds-worker
fix: builds worker new structure
2024-10-22 18:57:37 +02:00
Torsten Dittmann 89b6291488 fix: khusboo review comments 2024-10-22 18:56:34 +02:00
Torsten Dittmann 1434b6bbff fix: builds worker new structure 2024-10-22 18:03:40 +02:00
Torsten DittmannandGitHub de164ff936 Merge pull request #8842 from appwrite/fix-workers
Fix workers
2024-10-22 17:37:01 +02:00
Torsten Dittmann a8e2fb1ee5 Merge branch 'feat-sites' of https://github.com/appwrite/appwrite into fix-workers 2024-10-22 17:35:54 +02:00
Torsten Dittmann 7da8b44c63 chore: run formatter 2024-10-22 17:35:24 +02:00
Torsten Dittmann 8f0c1a4224 fix: gitignore 2024-10-22 17:34:22 +02:00
Torsten Dittmann c57cb3d2d9 fix: workers 2024-10-22 17:33:33 +02:00
Torsten DittmannandGitHub 5172189e36 Merge pull request #8839 from appwrite/feat-add-sites-endpoints
Move functions endpoints to modules
2024-10-22 17:31:43 +02:00
Khushboo Verma 6c6723e181 Refactor callback 2024-10-22 17:10:50 +02:00
Khushboo Verma 5b0a05b4b3 Create site endpoint working 2024-10-22 17:01:38 +02:00
Khushboo Verma 87cbd8fbf3 Move create function, create deployment to modules 2024-10-22 13:00:10 +02:00
Torsten DittmannandGitHub 9060250806 Merge pull request #8832 from appwrite/feat-add-sites-collection
Add new collection
2024-10-21 19:08:34 +02:00
Khushboo Verma bc63c3b383 Add sites collection 2024-10-21 19:01:37 +02:00
Khushboo Verma a503803725 Add secret attribute to variables 2024-10-21 16:33:57 +02:00
Bradley Schofield 1a903efc06 Use repository temporarily to check tests 2024-10-08 11:34:27 +09:00
Bradley Schofield 4c5b202f8d Merge branch '1.6.x' into chore-update-databases-findone
# Conflicts:
#	composer.json
#	composer.lock
2024-10-07 11:42:43 +09:00
Bradley Schofield 91b78a70ae Remove redundancy 2024-10-07 11:40:01 +09:00
Bradley Schofield 1e7c51cf04 Update lockfile 2024-09-27 12:50:32 +09:00
Luke B. SilverandGitHub 36368f3d80 Merge branch 'main' into fix-oauth-trigger-create-user-event 2024-09-25 11:26:58 +01:00
loks0n 3ba7d7c7de fix: add payload 2024-09-25 10:48:39 +01:00
loks0n ab5ef97767 fix: oauth trigger create user event 2024-09-25 10:39:11 +01:00
Bradley Schofield e88cfed03a Normalize algorithm being used 2024-09-18 16:30:02 +09:00
Bradley Schofield be3e3790c9 Update Database lib, update instances where findOne is used to ensure we check document is empty. 2024-09-18 14:47:36 +09:00
159 changed files with 24851 additions and 4786 deletions
+13 -9
View File
@@ -2,6 +2,7 @@ _APP_ENV=development
_APP_EDITION=self-hosted
_APP_LOCALE=en
_APP_WORKER_PER_CORE=6
_APP_COMPRESSION_MIN_SIZE_BYTES=1024
_APP_CONSOLE_WHITELIST_ROOT=disabled
_APP_CONSOLE_WHITELIST_EMAILS=
_APP_CONSOLE_SESSION_ALERTS=enabled
@@ -17,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
@@ -66,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
+33
View File
@@ -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
+1 -1
View File
@@ -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
+1
View File
@@ -16,3 +16,4 @@ dev/yasd_init.php
.phpunit.result.cache
Makefile
appwrite.json
/.zed/
+5 -4
View File
@@ -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 && \
@@ -92,10 +93,10 @@ RUN chmod +x /usr/local/bin/doctor && \
RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/
# Enable Extensions
RUN if [ "$DEBUG" == "true" ]; then cp /usr/src/code/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini; fi
RUN if [ "$DEBUG" == "true" ]; then mkdir -p /tmp/xdebug; fi
RUN if [ "$DEBUG" = "true" ]; then cp /usr/src/code/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini; fi
RUN if [ "$DEBUG" = "true" ]; then mkdir -p /tmp/xdebug; fi
RUN if [ "$DEBUG" = "false" ]; then rm -rf /usr/src/code/dev; fi
RUN if [ "$DEBUG" = "false" ]; then rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20220829/xdebug.so; fi
RUN if [ "$DEBUG" = "false" ]; then rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20230831/xdebug.so; fi
EXPOSE 80
+439 -4
View File
@@ -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,
+17
View File
@@ -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,
+28
View File
@@ -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,
+125
View File
@@ -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',
]
];
+69 -100
View File
@@ -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'
+2
View File
@@ -58,6 +58,8 @@ $admins = [
'health.read',
'functions.read',
'functions.write',
'sites.read',
'sites.write',
'execution.read',
'execution.write',
'rules.read',
+6
View File
@@ -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',
],
+206
View File
@@ -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' => [],
],
];
+41 -35
View File
@@ -4945,7 +4945,7 @@
},
"x-appwrite": {
"method": "listExecutions",
"weight": 305,
"weight": 306,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5033,7 +5033,7 @@
},
"x-appwrite": {
"method": "createExecution",
"weight": 304,
"weight": 305,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5150,7 +5150,7 @@
},
"x-appwrite": {
"method": "getExecution",
"weight": 306,
"weight": 307,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5226,7 +5226,7 @@
},
"x-appwrite": {
"method": "query",
"weight": 330,
"weight": 331,
"cookies": false,
"type": "graphql",
"deprecated": false,
@@ -5280,7 +5280,7 @@
},
"x-appwrite": {
"method": "mutation",
"weight": 329,
"weight": 330,
"cookies": false,
"type": "graphql",
"deprecated": false,
@@ -5766,7 +5766,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 381,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5851,7 +5851,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 385,
"weight": 386,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5928,7 +5928,7 @@
},
"x-appwrite": {
"method": "listFiles",
"weight": 207,
"weight": 208,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6016,7 +6016,7 @@
},
"x-appwrite": {
"method": "createFile",
"weight": 206,
"weight": 207,
"cookies": false,
"type": "upload",
"deprecated": false,
@@ -6116,7 +6116,7 @@
},
"x-appwrite": {
"method": "getFile",
"weight": 208,
"weight": 209,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6190,7 +6190,7 @@
},
"x-appwrite": {
"method": "updateFile",
"weight": 213,
"weight": 214,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6281,7 +6281,7 @@
},
"x-appwrite": {
"method": "deleteFile",
"weight": 214,
"weight": 215,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6350,7 +6350,7 @@
},
"x-appwrite": {
"method": "getFileDownload",
"weight": 210,
"weight": 211,
"cookies": false,
"type": "location",
"deprecated": false,
@@ -6419,7 +6419,7 @@
},
"x-appwrite": {
"method": "getFilePreview",
"weight": 209,
"weight": 210,
"cookies": false,
"type": "location",
"deprecated": false,
@@ -6637,7 +6637,7 @@
},
"x-appwrite": {
"method": "getFileView",
"weight": 211,
"weight": 212,
"cookies": false,
"type": "location",
"deprecated": false,
@@ -6713,7 +6713,7 @@
},
"x-appwrite": {
"method": "list",
"weight": 218,
"weight": 219,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6791,7 +6791,7 @@
},
"x-appwrite": {
"method": "create",
"weight": 217,
"weight": 218,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6878,7 +6878,7 @@
},
"x-appwrite": {
"method": "get",
"weight": 219,
"weight": 220,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6942,7 +6942,7 @@
},
"x-appwrite": {
"method": "updateName",
"weight": 221,
"weight": 222,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7018,7 +7018,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 223,
"weight": 224,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7069,7 +7069,7 @@
"tags": [
"teams"
],
"description": "Use this endpoint to list a team's members using the team's ID. All team members have read access to this endpoint.",
"description": "Use this endpoint to list a team's members using the team's ID. All team members have read access to this endpoint. Hide sensitive attributes from the response by toggling membership privacy in the Console.",
"responses": {
"200": {
"description": "Memberships List",
@@ -7084,7 +7084,7 @@
},
"x-appwrite": {
"method": "listMemberships",
"weight": 225,
"weight": 226,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7172,7 +7172,7 @@
},
"x-appwrite": {
"method": "createMembership",
"weight": 224,
"weight": 225,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7270,7 +7270,7 @@
"tags": [
"teams"
],
"description": "Get a team member by the membership unique id. All team members have read access for this resource.",
"description": "Get a team member by the membership unique id. All team members have read access for this resource. Hide sensitive attributes from the response by toggling membership privacy in the Console.",
"responses": {
"200": {
"description": "Membership",
@@ -7285,7 +7285,7 @@
},
"x-appwrite": {
"method": "getMembership",
"weight": 226,
"weight": 227,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7359,7 +7359,7 @@
},
"x-appwrite": {
"method": "updateMembership",
"weight": 227,
"weight": 228,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7448,7 +7448,7 @@
},
"x-appwrite": {
"method": "deleteMembership",
"weight": 229,
"weight": 230,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7524,7 +7524,7 @@
},
"x-appwrite": {
"method": "updateMembershipStatus",
"weight": 228,
"weight": 229,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7624,7 +7624,7 @@
},
"x-appwrite": {
"method": "getPrefs",
"weight": 220,
"weight": 221,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7687,7 +7687,7 @@
},
"x-appwrite": {
"method": "updatePrefs",
"weight": 222,
"weight": 223,
"cookies": false,
"type": "",
"deprecated": false,
@@ -9266,12 +9266,12 @@
},
"userName": {
"type": "string",
"description": "User name.",
"description": "User name. Hide this attribute by toggling membership privacy in the Console.",
"x-example": "John Doe"
},
"userEmail": {
"type": "string",
"description": "User email address.",
"description": "User email address. Hide this attribute by toggling membership privacy in the Console.",
"x-example": "john@appwrite.io"
},
"teamId": {
@@ -9301,7 +9301,7 @@
},
"mfa": {
"type": "boolean",
"description": "Multi factor authentication status, true if the user has MFA enabled or false otherwise.",
"description": "Multi factor authentication status, true if the user has MFA enabled or false otherwise. Hide this attribute by toggling membership privacy in the Console.",
"x-example": false
},
"roles": {
@@ -9826,7 +9826,7 @@
"name": {
"type": "string",
"description": "Target Name.",
"x-example": "Aegon apple token"
"x-example": "Apple iPhone 12"
},
"userId": {
"type": "string",
@@ -9848,6 +9848,11 @@
"type": "string",
"description": "The target identifier.",
"x-example": "token"
},
"expired": {
"type": "boolean",
"description": "Is the target expired.",
"x-example": false
}
},
"required": [
@@ -9857,7 +9862,8 @@
"name",
"userId",
"providerType",
"identifier"
"identifier",
"expired"
]
}
},
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+126 -120
View File
@@ -43,7 +43,7 @@
},
"x-appwrite": {
"method": "get",
"weight": 8,
"weight": 9,
"cookies": false,
"type": "",
"deprecated": false,
@@ -94,7 +94,7 @@
},
"x-appwrite": {
"method": "create",
"weight": 7,
"weight": 8,
"cookies": false,
"type": "",
"deprecated": false,
@@ -166,7 +166,7 @@
"tags": [
"account"
],
"description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\r\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\r\n",
"description": "Update currently logged in user account email address. After changing user address, the user confirmation status will get reset. A new confirmation email is not sent automatically however you can use the send confirmation email endpoint again to send the confirmation email. For security measures, user password is required to complete this request.\nThis endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password.\n",
"responses": {
"200": {
"description": "User",
@@ -181,7 +181,7 @@
},
"x-appwrite": {
"method": "updateEmail",
"weight": 33,
"weight": 34,
"cookies": false,
"type": "",
"deprecated": false,
@@ -259,7 +259,7 @@
},
"x-appwrite": {
"method": "listIdentities",
"weight": 56,
"weight": 57,
"cookies": false,
"type": "",
"deprecated": false,
@@ -320,7 +320,7 @@
},
"x-appwrite": {
"method": "deleteIdentity",
"weight": 57,
"weight": 58,
"cookies": false,
"type": "",
"deprecated": false,
@@ -385,7 +385,7 @@
},
"x-appwrite": {
"method": "createJWT",
"weight": 28,
"weight": 29,
"cookies": false,
"type": "",
"deprecated": false,
@@ -436,7 +436,7 @@
},
"x-appwrite": {
"method": "listLogs",
"weight": 30,
"weight": 31,
"cookies": false,
"type": "",
"deprecated": false,
@@ -504,7 +504,7 @@
},
"x-appwrite": {
"method": "updateMFA",
"weight": 43,
"weight": 44,
"cookies": false,
"type": "",
"deprecated": false,
@@ -576,7 +576,7 @@
},
"x-appwrite": {
"method": "createMfaAuthenticator",
"weight": 45,
"weight": 46,
"cookies": false,
"type": "",
"deprecated": false,
@@ -644,7 +644,7 @@
},
"x-appwrite": {
"method": "updateMfaAuthenticator",
"weight": 46,
"weight": 47,
"cookies": false,
"type": "",
"deprecated": false,
@@ -724,7 +724,7 @@
},
"x-appwrite": {
"method": "deleteMfaAuthenticator",
"weight": 50,
"weight": 51,
"cookies": false,
"type": "",
"deprecated": false,
@@ -794,7 +794,7 @@
},
"x-appwrite": {
"method": "createMfaChallenge",
"weight": 51,
"weight": 52,
"cookies": false,
"type": "",
"deprecated": false,
@@ -870,7 +870,7 @@
},
"x-appwrite": {
"method": "updateMfaChallenge",
"weight": 52,
"weight": 53,
"cookies": false,
"type": "",
"deprecated": false,
@@ -948,7 +948,7 @@
},
"x-appwrite": {
"method": "listMfaFactors",
"weight": 44,
"weight": 45,
"cookies": false,
"type": "",
"deprecated": false,
@@ -1001,7 +1001,7 @@
},
"x-appwrite": {
"method": "getMfaRecoveryCodes",
"weight": 49,
"weight": 50,
"cookies": false,
"type": "",
"deprecated": false,
@@ -1052,7 +1052,7 @@
},
"x-appwrite": {
"method": "createMfaRecoveryCodes",
"weight": 47,
"weight": 48,
"cookies": false,
"type": "",
"deprecated": false,
@@ -1103,7 +1103,7 @@
},
"x-appwrite": {
"method": "updateMfaRecoveryCodes",
"weight": 48,
"weight": 49,
"cookies": false,
"type": "",
"deprecated": false,
@@ -1156,7 +1156,7 @@
},
"x-appwrite": {
"method": "updateName",
"weight": 31,
"weight": 32,
"cookies": false,
"type": "",
"deprecated": false,
@@ -1228,7 +1228,7 @@
},
"x-appwrite": {
"method": "updatePassword",
"weight": 32,
"weight": 33,
"cookies": false,
"type": "",
"deprecated": false,
@@ -1305,7 +1305,7 @@
},
"x-appwrite": {
"method": "updatePhone",
"weight": 34,
"weight": 35,
"cookies": false,
"type": "",
"deprecated": false,
@@ -1383,7 +1383,7 @@
},
"x-appwrite": {
"method": "getPrefs",
"weight": 29,
"weight": 30,
"cookies": false,
"type": "",
"deprecated": false,
@@ -1434,7 +1434,7 @@
},
"x-appwrite": {
"method": "updatePrefs",
"weight": 35,
"weight": 36,
"cookies": false,
"type": "",
"deprecated": false,
@@ -1506,7 +1506,7 @@
},
"x-appwrite": {
"method": "createRecovery",
"weight": 37,
"weight": 38,
"cookies": false,
"type": "",
"deprecated": false,
@@ -1570,7 +1570,7 @@
"tags": [
"account"
],
"description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.",
"description": "Use this endpoint to complete the user account password reset. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST \/account\/recovery](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createRecovery) endpoint.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.",
"responses": {
"200": {
"description": "Token",
@@ -1585,7 +1585,7 @@
},
"x-appwrite": {
"method": "updateRecovery",
"weight": 38,
"weight": 39,
"cookies": false,
"type": "",
"deprecated": false,
@@ -1669,7 +1669,7 @@
},
"x-appwrite": {
"method": "listSessions",
"weight": 10,
"weight": 11,
"cookies": false,
"type": "",
"deprecated": false,
@@ -1713,7 +1713,7 @@
},
"x-appwrite": {
"method": "deleteSessions",
"weight": 11,
"weight": 12,
"cookies": false,
"type": "",
"deprecated": false,
@@ -1766,7 +1766,7 @@
},
"x-appwrite": {
"method": "createAnonymousSession",
"weight": 16,
"weight": 17,
"cookies": false,
"type": "",
"deprecated": false,
@@ -1802,7 +1802,7 @@
"tags": [
"account"
],
"description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\r\n\r\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).",
"description": "Allow the user to login into their account by providing a valid email and password combination. This route will create a new session for the user.\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).",
"responses": {
"201": {
"description": "Session",
@@ -1817,7 +1817,7 @@
},
"x-appwrite": {
"method": "createEmailPasswordSession",
"weight": 15,
"weight": 16,
"cookies": false,
"type": "",
"deprecated": false,
@@ -1893,7 +1893,7 @@
},
"x-appwrite": {
"method": "updateMagicURLSession",
"weight": 25,
"weight": 26,
"cookies": false,
"type": "",
"deprecated": true,
@@ -1954,7 +1954,7 @@
"tags": [
"account"
],
"description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\r\n\r\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\r\n\r\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).\r\n",
"description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.\n\nIf there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.\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": {
"301": {
"description": "File"
@@ -1962,7 +1962,7 @@
},
"x-appwrite": {
"method": "createOAuth2Session",
"weight": 18,
"weight": 19,
"cookies": false,
"type": "webAuth",
"deprecated": false,
@@ -2105,7 +2105,7 @@
},
"x-appwrite": {
"method": "updatePhoneSession",
"weight": 26,
"weight": 27,
"cookies": false,
"type": "",
"deprecated": true,
@@ -2181,7 +2181,7 @@
},
"x-appwrite": {
"method": "createSession",
"weight": 17,
"weight": 18,
"cookies": false,
"type": "",
"deprecated": false,
@@ -2257,7 +2257,7 @@
},
"x-appwrite": {
"method": "getSession",
"weight": 12,
"weight": 13,
"cookies": false,
"type": "",
"deprecated": false,
@@ -2320,7 +2320,7 @@
},
"x-appwrite": {
"method": "updateSession",
"weight": 14,
"weight": 15,
"cookies": false,
"type": "",
"deprecated": false,
@@ -2376,7 +2376,7 @@
},
"x-appwrite": {
"method": "deleteSession",
"weight": 13,
"weight": 14,
"cookies": false,
"type": "",
"deprecated": false,
@@ -2441,7 +2441,7 @@
},
"x-appwrite": {
"method": "updateStatus",
"weight": 36,
"weight": 37,
"cookies": false,
"type": "",
"deprecated": false,
@@ -2494,7 +2494,7 @@
},
"x-appwrite": {
"method": "createPushTarget",
"weight": 53,
"weight": 54,
"cookies": false,
"type": "",
"deprecated": false,
@@ -2575,7 +2575,7 @@
},
"x-appwrite": {
"method": "updatePushTarget",
"weight": 54,
"weight": 55,
"cookies": false,
"type": "",
"deprecated": false,
@@ -2655,7 +2655,7 @@
},
"x-appwrite": {
"method": "deletePushTarget",
"weight": 55,
"weight": 56,
"cookies": false,
"type": "",
"deprecated": false,
@@ -2703,7 +2703,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and 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 secret sent to the user's email is valid for 15 minutes.\r\n\r\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).",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and 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 secret sent to the user's email is valid for 15 minutes.\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).",
"responses": {
"201": {
"description": "Token",
@@ -2718,7 +2718,7 @@
},
"x-appwrite": {
"method": "createEmailToken",
"weight": 24,
"weight": 25,
"cookies": false,
"type": "",
"deprecated": false,
@@ -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.\r\n\r\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).\r\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",
@@ -2799,7 +2799,7 @@
},
"x-appwrite": {
"method": "createMagicURLToken",
"weight": 23,
"weight": 24,
"cookies": false,
"type": "",
"deprecated": false,
@@ -2873,7 +2873,7 @@
"tags": [
"account"
],
"description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \r\n\r\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\r\n\r\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).",
"description": "Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. \n\nIf authentication succeeds, `userId` and `secret` of a token will be appended to the success URL as query parameters. These can be used to create a new session using the [Create session](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint.\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).",
"responses": {
"301": {
"description": "File"
@@ -2881,7 +2881,7 @@
},
"x-appwrite": {
"method": "createOAuth2Token",
"weight": 22,
"weight": 23,
"cookies": false,
"type": "webAuth",
"deprecated": false,
@@ -3009,7 +3009,7 @@
"tags": [
"account"
],
"description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and 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 secret sent to the user's phone is valid for 15 minutes.\r\n\r\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).",
"description": "Sends the user an SMS with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and 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 secret sent to the user's phone is valid for 15 minutes.\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).",
"responses": {
"201": {
"description": "Token",
@@ -3024,7 +3024,7 @@
},
"x-appwrite": {
"method": "createPhoneToken",
"weight": 27,
"weight": 28,
"cookies": false,
"type": "",
"deprecated": false,
@@ -3088,7 +3088,7 @@
"tags": [
"account"
],
"description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\r\n\r\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\r\n",
"description": "Use this endpoint to send a verification message to your user email address to confirm they are the valid owners of that address. Both the **userId** and **secret** arguments will be passed as query parameters to the URL you have provided to be attached to the verification email. The provided URL should redirect the user back to your app and allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#updateVerification). The verification link sent to the user's email address is valid for 7 days.\n\nPlease note that in order to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.\n",
"responses": {
"201": {
"description": "Token",
@@ -3103,7 +3103,7 @@
},
"x-appwrite": {
"method": "createVerification",
"weight": 39,
"weight": 40,
"cookies": false,
"type": "",
"deprecated": false,
@@ -3173,7 +3173,7 @@
},
"x-appwrite": {
"method": "updateVerification",
"weight": 40,
"weight": 41,
"cookies": false,
"type": "",
"deprecated": false,
@@ -3251,7 +3251,7 @@
},
"x-appwrite": {
"method": "createPhoneVerification",
"weight": 41,
"weight": 42,
"cookies": false,
"type": "",
"deprecated": false,
@@ -3305,7 +3305,7 @@
},
"x-appwrite": {
"method": "updatePhoneVerification",
"weight": 42,
"weight": 43,
"cookies": false,
"type": "",
"deprecated": false,
@@ -3368,7 +3368,7 @@
"tags": [
"avatars"
],
"description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.",
"description": "You can use this endpoint to show different browser icons to your users. The code argument receives the browser code as it appears in your user [GET \/account\/sessions](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#getSessions) endpoint. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.",
"responses": {
"200": {
"description": "Image"
@@ -3376,7 +3376,7 @@
},
"x-appwrite": {
"method": "getBrowser",
"weight": 59,
"weight": 60,
"cookies": false,
"type": "location",
"deprecated": false,
@@ -3496,7 +3496,7 @@
"tags": [
"avatars"
],
"description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n",
"description": "The credit card endpoint will return you the icon of the credit card provider you need. Use width, height and quality arguments to change the output settings.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n",
"responses": {
"200": {
"description": "Image"
@@ -3504,7 +3504,7 @@
},
"x-appwrite": {
"method": "getCreditCard",
"weight": 58,
"weight": 59,
"cookies": false,
"type": "location",
"deprecated": false,
@@ -3628,7 +3628,7 @@
"tags": [
"avatars"
],
"description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\r\n\r\nThis endpoint does not follow HTTP redirects.",
"description": "Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.\n\nThis endpoint does not follow HTTP redirects.",
"responses": {
"200": {
"description": "Image"
@@ -3636,7 +3636,7 @@
},
"x-appwrite": {
"method": "getFavicon",
"weight": 62,
"weight": 63,
"cookies": false,
"type": "location",
"deprecated": false,
@@ -3688,7 +3688,7 @@
"tags": [
"avatars"
],
"description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n",
"description": "You can use this endpoint to show different country flags icons to your users. The code argument receives the 2 letter country code. Use width, height and quality arguments to change the output settings. Country codes follow the [ISO 3166-1](https:\/\/en.wikipedia.org\/wiki\/ISO_3166-1) standard.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n",
"responses": {
"200": {
"description": "Image"
@@ -3696,7 +3696,7 @@
},
"x-appwrite": {
"method": "getFlag",
"weight": 60,
"weight": 61,
"cookies": false,
"type": "location",
"deprecated": false,
@@ -4178,7 +4178,7 @@
"tags": [
"avatars"
],
"description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\r\n\r\nThis endpoint does not follow HTTP redirects.",
"description": "Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.\n\nThis endpoint does not follow HTTP redirects.",
"responses": {
"200": {
"description": "Image"
@@ -4186,7 +4186,7 @@
},
"x-appwrite": {
"method": "getImage",
"weight": 61,
"weight": 62,
"cookies": false,
"type": "location",
"deprecated": false,
@@ -4262,7 +4262,7 @@
"tags": [
"avatars"
],
"description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\r\n\r\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\r\n\r\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\r\n",
"description": "Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned.\n\nYou can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials.\n\nWhen one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 100x100px.\n",
"responses": {
"200": {
"description": "Image"
@@ -4270,7 +4270,7 @@
},
"x-appwrite": {
"method": "getInitials",
"weight": 64,
"weight": 65,
"cookies": false,
"type": "location",
"deprecated": false,
@@ -4356,7 +4356,7 @@
"tags": [
"avatars"
],
"description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\r\n",
"description": "Converts a given plain text to a QR code image. You can use the query parameters to change the size and style of the resulting image.\n",
"responses": {
"200": {
"description": "Image"
@@ -4364,7 +4364,7 @@
},
"x-appwrite": {
"method": "getQR",
"weight": 63,
"weight": 64,
"cookies": false,
"type": "location",
"deprecated": false,
@@ -4465,7 +4465,7 @@
},
"x-appwrite": {
"method": "listDocuments",
"weight": 108,
"weight": 109,
"cookies": false,
"type": "",
"deprecated": false,
@@ -4552,7 +4552,7 @@
},
"x-appwrite": {
"method": "createDocument",
"weight": 107,
"weight": 108,
"cookies": false,
"type": "",
"deprecated": false,
@@ -4661,7 +4661,7 @@
},
"x-appwrite": {
"method": "getDocument",
"weight": 109,
"weight": 110,
"cookies": false,
"type": "",
"deprecated": false,
@@ -4758,7 +4758,7 @@
},
"x-appwrite": {
"method": "updateDocument",
"weight": 111,
"weight": 112,
"cookies": false,
"type": "",
"deprecated": false,
@@ -4859,7 +4859,7 @@
},
"x-appwrite": {
"method": "deleteDocument",
"weight": 112,
"weight": 113,
"cookies": false,
"type": "",
"deprecated": false,
@@ -4945,7 +4945,7 @@
},
"x-appwrite": {
"method": "listExecutions",
"weight": 304,
"weight": 303,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5033,7 +5033,7 @@
},
"x-appwrite": {
"method": "createExecution",
"weight": 303,
"weight": 302,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5084,7 +5084,7 @@
"body": {
"type": "string",
"description": "HTTP body of execution. Default value is empty string.",
"x-example": null
"x-example": "<BODY>"
},
"async": {
"type": "boolean",
@@ -5150,7 +5150,7 @@
},
"x-appwrite": {
"method": "getExecution",
"weight": 305,
"weight": 304,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5319,7 +5319,7 @@
"tags": [
"locale"
],
"description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\r\n\r\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))",
"description": "Get the current user location based on IP. Returns an object with user country code, country name, continent name, continent code, ip address and suggested currency. You can use the locale header to get the data in a supported language.\n\n([IP Geolocation by DB-IP](https:\/\/db-ip.com))",
"responses": {
"200": {
"description": "Locale",
@@ -5334,7 +5334,7 @@
},
"x-appwrite": {
"method": "get",
"weight": 116,
"weight": 117,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5388,7 +5388,7 @@
},
"x-appwrite": {
"method": "listCodes",
"weight": 117,
"weight": 118,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5442,7 +5442,7 @@
},
"x-appwrite": {
"method": "listContinents",
"weight": 121,
"weight": 122,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5496,7 +5496,7 @@
},
"x-appwrite": {
"method": "listCountries",
"weight": 118,
"weight": 119,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5550,7 +5550,7 @@
},
"x-appwrite": {
"method": "listCountriesEU",
"weight": 119,
"weight": 120,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5604,7 +5604,7 @@
},
"x-appwrite": {
"method": "listCountriesPhones",
"weight": 120,
"weight": 121,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5658,7 +5658,7 @@
},
"x-appwrite": {
"method": "listCurrencies",
"weight": 122,
"weight": 123,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5712,7 +5712,7 @@
},
"x-appwrite": {
"method": "listLanguages",
"weight": 123,
"weight": 124,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5928,7 +5928,7 @@
},
"x-appwrite": {
"method": "listFiles",
"weight": 206,
"weight": 208,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6001,7 +6001,7 @@
"tags": [
"storage"
],
"description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\r\n\r\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\r\n\r\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\r\n\r\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\r\n",
"description": "Create a new file. Before using this route, you should create a new bucket resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/storage#storageCreateBucket) API or directly from your Appwrite console.\n\nLarger files should be uploaded using multiple requests with the [content-range](https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Content-Range) header to send a partial request with a maximum supported chunk of `5MB`. The `content-range` header values should always be in bytes.\n\nWhen the first request is sent, the server will return the **File** object, and the subsequent part request must include the file's **id** in `x-appwrite-id` header to allow the server to know that the partial upload is for the existing file and not for a new one.\n\nIf you're creating a new file using one of the Appwrite SDKs, all the chunking logic will be managed by the SDK internally.\n",
"responses": {
"201": {
"description": "File",
@@ -6016,7 +6016,7 @@
},
"x-appwrite": {
"method": "createFile",
"weight": 205,
"weight": 207,
"cookies": false,
"type": "upload",
"deprecated": false,
@@ -6116,7 +6116,7 @@
},
"x-appwrite": {
"method": "getFile",
"weight": 207,
"weight": 209,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6190,7 +6190,7 @@
},
"x-appwrite": {
"method": "updateFile",
"weight": 212,
"weight": 214,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6281,7 +6281,7 @@
},
"x-appwrite": {
"method": "deleteFile",
"weight": 213,
"weight": 215,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6350,7 +6350,7 @@
},
"x-appwrite": {
"method": "getFileDownload",
"weight": 209,
"weight": 211,
"cookies": false,
"type": "location",
"deprecated": false,
@@ -6419,7 +6419,7 @@
},
"x-appwrite": {
"method": "getFilePreview",
"weight": 208,
"weight": 210,
"cookies": false,
"type": "location",
"deprecated": false,
@@ -6637,7 +6637,7 @@
},
"x-appwrite": {
"method": "getFileView",
"weight": 210,
"weight": 212,
"cookies": false,
"type": "location",
"deprecated": false,
@@ -6713,7 +6713,7 @@
},
"x-appwrite": {
"method": "list",
"weight": 217,
"weight": 219,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6791,7 +6791,7 @@
},
"x-appwrite": {
"method": "create",
"weight": 216,
"weight": 218,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6878,7 +6878,7 @@
},
"x-appwrite": {
"method": "get",
"weight": 218,
"weight": 220,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6942,7 +6942,7 @@
},
"x-appwrite": {
"method": "updateName",
"weight": 220,
"weight": 222,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7018,7 +7018,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 222,
"weight": 224,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7069,7 +7069,7 @@
"tags": [
"teams"
],
"description": "Use this endpoint to list a team's members using the team's ID. All team members have read access to this endpoint.",
"description": "Use this endpoint to list a team's members using the team's ID. All team members have read access to this endpoint. Hide sensitive attributes from the response by toggling membership privacy in the Console.",
"responses": {
"200": {
"description": "Memberships List",
@@ -7084,7 +7084,7 @@
},
"x-appwrite": {
"method": "listMemberships",
"weight": 224,
"weight": 226,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7157,7 +7157,7 @@
"tags": [
"teams"
],
"description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\r\n\r\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\r\n\r\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \r\n\r\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\r\n",
"description": "Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team.\n\nYou only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters.\n\nUse the `url` parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/teams#updateMembershipStatus) endpoint to allow the user to accept the invitation to the team. \n\nPlease note that to avoid a [Redirect Attack](https:\/\/github.com\/OWASP\/CheatSheetSeries\/blob\/master\/cheatsheets\/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console.\n",
"responses": {
"201": {
"description": "Membership",
@@ -7172,7 +7172,7 @@
},
"x-appwrite": {
"method": "createMembership",
"weight": 223,
"weight": 225,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7270,7 +7270,7 @@
"tags": [
"teams"
],
"description": "Get a team member by the membership unique id. All team members have read access for this resource.",
"description": "Get a team member by the membership unique id. All team members have read access for this resource. Hide sensitive attributes from the response by toggling membership privacy in the Console.",
"responses": {
"200": {
"description": "Membership",
@@ -7285,7 +7285,7 @@
},
"x-appwrite": {
"method": "getMembership",
"weight": 225,
"weight": 227,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7344,7 +7344,7 @@
"tags": [
"teams"
],
"description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\r\n",
"description": "Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](https:\/\/appwrite.io\/docs\/permissions).\n",
"responses": {
"200": {
"description": "Membership",
@@ -7359,7 +7359,7 @@
},
"x-appwrite": {
"method": "updateMembership",
"weight": 226,
"weight": 228,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7448,7 +7448,7 @@
},
"x-appwrite": {
"method": "deleteMembership",
"weight": 228,
"weight": 230,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7509,7 +7509,7 @@
"tags": [
"teams"
],
"description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\r\n\r\nIf the request is successful, a session for the user is automatically created.\r\n",
"description": "Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.\n\nIf the request is successful, a session for the user is automatically created.\n",
"responses": {
"200": {
"description": "Membership",
@@ -7524,7 +7524,7 @@
},
"x-appwrite": {
"method": "updateMembershipStatus",
"weight": 227,
"weight": 229,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7624,7 +7624,7 @@
},
"x-appwrite": {
"method": "getPrefs",
"weight": 219,
"weight": 221,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7687,7 +7687,7 @@
},
"x-appwrite": {
"method": "updatePrefs",
"weight": 221,
"weight": 223,
"cookies": false,
"type": "",
"deprecated": false,
@@ -9266,12 +9266,12 @@
},
"userName": {
"type": "string",
"description": "User name.",
"description": "User name. Hide this attribute by toggling membership privacy in the Console.",
"x-example": "John Doe"
},
"userEmail": {
"type": "string",
"description": "User email address.",
"description": "User email address. Hide this attribute by toggling membership privacy in the Console.",
"x-example": "john@appwrite.io"
},
"teamId": {
@@ -9301,7 +9301,7 @@
},
"mfa": {
"type": "boolean",
"description": "Multi factor authentication status, true if the user has MFA enabled or false otherwise.",
"description": "Multi factor authentication status, true if the user has MFA enabled or false otherwise. Hide this attribute by toggling membership privacy in the Console.",
"x-example": false
},
"roles": {
@@ -9826,7 +9826,7 @@
"name": {
"type": "string",
"description": "Target Name.",
"x-example": "Aegon apple token"
"x-example": "Apple iPhone 12"
},
"userId": {
"type": "string",
@@ -9848,6 +9848,11 @@
"type": "string",
"description": "The target identifier.",
"x-example": "token"
},
"expired": {
"type": "boolean",
"description": "Is the target expired.",
"x-example": false
}
},
"required": [
@@ -9857,7 +9862,8 @@
"name",
"userId",
"providerType",
"identifier"
"identifier",
"expired"
]
}
},
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+41 -35
View File
@@ -5101,7 +5101,7 @@
},
"x-appwrite": {
"method": "listExecutions",
"weight": 305,
"weight": 306,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5186,7 +5186,7 @@
},
"x-appwrite": {
"method": "createExecution",
"weight": 304,
"weight": 305,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5307,7 +5307,7 @@
},
"x-appwrite": {
"method": "getExecution",
"weight": 306,
"weight": 307,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5381,7 +5381,7 @@
},
"x-appwrite": {
"method": "query",
"weight": 330,
"weight": 331,
"cookies": false,
"type": "graphql",
"deprecated": false,
@@ -5457,7 +5457,7 @@
},
"x-appwrite": {
"method": "mutation",
"weight": 329,
"weight": 330,
"cookies": false,
"type": "graphql",
"deprecated": false,
@@ -5981,7 +5981,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 381,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6070,7 +6070,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 385,
"weight": 386,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6145,7 +6145,7 @@
},
"x-appwrite": {
"method": "listFiles",
"weight": 207,
"weight": 208,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6230,7 +6230,7 @@
},
"x-appwrite": {
"method": "createFile",
"weight": 206,
"weight": 207,
"cookies": false,
"type": "upload",
"deprecated": false,
@@ -6324,7 +6324,7 @@
},
"x-appwrite": {
"method": "getFile",
"weight": 208,
"weight": 209,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6396,7 +6396,7 @@
},
"x-appwrite": {
"method": "updateFile",
"weight": 213,
"weight": 214,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6487,7 +6487,7 @@
},
"x-appwrite": {
"method": "deleteFile",
"weight": 214,
"weight": 215,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6561,7 +6561,7 @@
},
"x-appwrite": {
"method": "getFileDownload",
"weight": 210,
"weight": 211,
"cookies": false,
"type": "location",
"deprecated": false,
@@ -6635,7 +6635,7 @@
},
"x-appwrite": {
"method": "getFilePreview",
"weight": 209,
"weight": 210,
"cookies": false,
"type": "location",
"deprecated": false,
@@ -6836,7 +6836,7 @@
},
"x-appwrite": {
"method": "getFileView",
"weight": 211,
"weight": 212,
"cookies": false,
"type": "location",
"deprecated": false,
@@ -6910,7 +6910,7 @@
},
"x-appwrite": {
"method": "list",
"weight": 218,
"weight": 219,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6987,7 +6987,7 @@
},
"x-appwrite": {
"method": "create",
"weight": 217,
"weight": 218,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7081,7 +7081,7 @@
},
"x-appwrite": {
"method": "get",
"weight": 219,
"weight": 220,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7145,7 +7145,7 @@
},
"x-appwrite": {
"method": "updateName",
"weight": 221,
"weight": 222,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7222,7 +7222,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 223,
"weight": 224,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7277,7 +7277,7 @@
"tags": [
"teams"
],
"description": "Use this endpoint to list a team's members using the team's ID. All team members have read access to this endpoint.",
"description": "Use this endpoint to list a team's members using the team's ID. All team members have read access to this endpoint. Hide sensitive attributes from the response by toggling membership privacy in the Console.",
"responses": {
"200": {
"description": "Memberships List",
@@ -7288,7 +7288,7 @@
},
"x-appwrite": {
"method": "listMemberships",
"weight": 225,
"weight": 226,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7373,7 +7373,7 @@
},
"x-appwrite": {
"method": "createMembership",
"weight": 224,
"weight": 225,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7479,7 +7479,7 @@
"tags": [
"teams"
],
"description": "Get a team member by the membership unique id. All team members have read access for this resource.",
"description": "Get a team member by the membership unique id. All team members have read access for this resource. Hide sensitive attributes from the response by toggling membership privacy in the Console.",
"responses": {
"200": {
"description": "Membership",
@@ -7490,7 +7490,7 @@
},
"x-appwrite": {
"method": "getMembership",
"weight": 226,
"weight": 227,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7562,7 +7562,7 @@
},
"x-appwrite": {
"method": "updateMembership",
"weight": 227,
"weight": 228,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7650,7 +7650,7 @@
},
"x-appwrite": {
"method": "deleteMembership",
"weight": 229,
"weight": 230,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7724,7 +7724,7 @@
},
"x-appwrite": {
"method": "updateMembershipStatus",
"weight": 228,
"weight": 229,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7822,7 +7822,7 @@
},
"x-appwrite": {
"method": "getPrefs",
"weight": 220,
"weight": 221,
"cookies": false,
"type": "",
"deprecated": false,
@@ -7885,7 +7885,7 @@
},
"x-appwrite": {
"method": "updatePrefs",
"weight": 222,
"weight": 223,
"cookies": false,
"type": "",
"deprecated": false,
@@ -9445,12 +9445,12 @@
},
"userName": {
"type": "string",
"description": "User name.",
"description": "User name. Hide this attribute by toggling membership privacy in the Console.",
"x-example": "John Doe"
},
"userEmail": {
"type": "string",
"description": "User email address.",
"description": "User email address. Hide this attribute by toggling membership privacy in the Console.",
"x-example": "john@appwrite.io"
},
"teamId": {
@@ -9480,7 +9480,7 @@
},
"mfa": {
"type": "boolean",
"description": "Multi factor authentication status, true if the user has MFA enabled or false otherwise.",
"description": "Multi factor authentication status, true if the user has MFA enabled or false otherwise. Hide this attribute by toggling membership privacy in the Console.",
"x-example": false
},
"roles": {
@@ -10008,7 +10008,7 @@
"name": {
"type": "string",
"description": "Target Name.",
"x-example": "Aegon apple token"
"x-example": "Apple iPhone 12"
},
"userId": {
"type": "string",
@@ -10030,6 +10030,11 @@
"type": "string",
"description": "The target identifier.",
"x-example": "token"
},
"expired": {
"type": "boolean",
"description": "Is the target expired.",
"x-example": false
}
},
"required": [
@@ -10039,7 +10044,8 @@
"name",
"userId",
"providerType",
"identifier"
"identifier",
"expired"
]
}
},
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+40
View File
@@ -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']
],
];
+21 -25
View File
@@ -274,7 +274,6 @@ $createSession = function (string $userId, string $secret, Request $request, Res
App::post('/v1/account')
->desc('Create account')
->groups(['api', 'account', 'auth'])
->label('event', 'users.[userId].create')
->label('scope', 'sessions.write')
->label('auth.type', 'emailPassword')
->label('audits.event', 'user.create')
@@ -297,9 +296,8 @@ App::post('/v1/account')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('queueForEvents')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Hooks $hooks) {
$email = \strtolower($email);
if ('console' === $project->getId()) {
@@ -332,7 +330,7 @@ App::post('/v1/account')
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
if (!$identityWithMatchingEmail->isEmpty()) {
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
}
@@ -395,7 +393,7 @@ App::post('/v1/account')
$existingTarget = $dbForProject->findOne('targets', [
Query::equal('identifier', [$email]),
]);
if ($existingTarget) {
if (!$existingTarget->isEmpty()) {
$user->setAttribute('targets', $existingTarget, Document::SET_TYPE_APPEND);
}
}
@@ -409,8 +407,6 @@ App::post('/v1/account')
Authorization::setRole(Role::user($user->getId())->toString());
Authorization::setRole(Role::users()->toString());
$queueForEvents->setParam('userId', $user->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($user, Response::MODEL_ACCOUNT);
@@ -442,7 +438,6 @@ App::get('/v1/account')
App::delete('/v1/account')
->desc('Delete account')
->groups(['api', 'account'])
->label('event', 'users.[userId].delete')
->label('scope', 'account')
->label('audits.event', 'user.delete')
->label('audits.resource', 'user/{response.$id}')
@@ -834,7 +829,7 @@ App::post('/v1/account/sessions/email')
Query::equal('email', [$email]),
]);
if (!$profile || empty($profile->getAttribute('passwordUpdate')) || !Auth::passwordVerify($password, $profile->getAttribute('password'), $profile->getAttribute('hash'), $profile->getAttribute('hashOptions'))) {
if ($profile->isEmpty() || empty($profile->getAttribute('passwordUpdate')) || !Auth::passwordVerify($password, $profile->getAttribute('password'), $profile->getAttribute('hash'), $profile->getAttribute('hashOptions'))) {
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
@@ -1374,7 +1369,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
Query::equal('providerEmail', [$email]),
Query::notEqual('userInternalId', $user->getInternalId()),
]);
if (!empty($identityWithMatchingEmail)) {
if (!$identityWithMatchingEmail->isEmpty()) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
@@ -1405,7 +1400,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
Query::equal('provider', [$provider]),
Query::equal('providerUid', [$oauth2ID]),
]);
if ($session !== false && !$session->isEmpty()) {
if (!$session->isEmpty()) {
$user->setAttributes($dbForProject->getDocument('users', $session->getAttribute('userId'))->getArrayCopy());
}
}
@@ -1423,7 +1418,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$userWithEmail = $dbForProject->findOne('users', [
Query::equal('email', [$email]),
]);
if ($userWithEmail !== false && !$userWithEmail->isEmpty()) {
if (!$userWithEmail->isEmpty()) {
$user->setAttributes($userWithEmail->getArrayCopy());
}
@@ -1434,7 +1429,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
Query::equal('providerUid', [$oauth2ID]),
]);
if ($identity !== false && !$identity->isEmpty()) {
if (!$identity->isEmpty()) {
$user = $dbForProject->getDocument('users', $identity->getAttribute('userId'));
}
}
@@ -1454,7 +1449,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
if (!$identityWithMatchingEmail->isEmpty()) {
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
}
@@ -1499,6 +1494,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'providerType' => MESSAGE_TYPE_EMAIL,
'identifier' => $email,
]));
} catch (Duplicate) {
$failureRedirect(Exception::USER_ALREADY_EXISTS);
}
@@ -1517,7 +1513,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
Query::equal('provider', [$provider]),
Query::equal('providerUid', [$oauth2ID]),
]);
if ($identity === false || $identity->isEmpty()) {
if ($identity->isEmpty()) {
// Before creating the identity, check if the email is already associated with another user
$userId = $user->getId();
@@ -1801,7 +1797,7 @@ App::post('/v1/account/tokens/magic-url')
$isAppUser = Auth::isAppUser($roles);
$result = $dbForProject->findOne('users', [Query::equal('email', [$email])]);
if ($result !== false && !$result->isEmpty()) {
if (!$result->isEmpty()) {
$user->setAttributes($result->getArrayCopy());
} else {
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
@@ -1818,7 +1814,7 @@ App::post('/v1/account/tokens/magic-url')
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
if (!$identityWithMatchingEmail->isEmpty()) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
@@ -2042,7 +2038,7 @@ App::post('/v1/account/tokens/email')
$isAppUser = Auth::isAppUser($roles);
$result = $dbForProject->findOne('users', [Query::equal('email', [$email])]);
if ($result !== false && !$result->isEmpty()) {
if (!$result->isEmpty()) {
$user->setAttributes($result->getArrayCopy());
} else {
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
@@ -2059,7 +2055,7 @@ App::post('/v1/account/tokens/email')
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
if (!$identityWithMatchingEmail->isEmpty()) {
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
}
@@ -2329,7 +2325,7 @@ App::post('/v1/account/tokens/phone')
$isAppUser = Auth::isAppUser($roles);
$result = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]);
if ($result !== false && !$result->isEmpty()) {
if (!$result->isEmpty()) {
$user->setAttributes($result->getArrayCopy());
} else {
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
@@ -2386,7 +2382,7 @@ App::post('/v1/account/tokens/phone')
$existingTarget = $dbForProject->findOne('targets', [
Query::equal('identifier', [$phone]),
]);
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]);
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget->isEmpty() ? false : $existingTarget]);
}
$dbForProject->purgeCachedDocument('users', $user->getId());
}
@@ -2753,7 +2749,7 @@ App::patch('/v1/account/email')
Query::equal('providerEmail', [$email]),
Query::notEqual('userInternalId', $user->getInternalId()),
]);
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
if (!$identityWithMatchingEmail->isEmpty()) {
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
}
@@ -2774,7 +2770,7 @@ App::patch('/v1/account/email')
Query::equal('identifier', [$email]),
]));
if ($target instanceof Document && !$target->isEmpty()) {
if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
@@ -2840,7 +2836,7 @@ App::patch('/v1/account/phone')
Query::equal('identifier', [$phone]),
]));
if ($target instanceof Document && !$target->isEmpty()) {
if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
@@ -2999,7 +2995,7 @@ App::post('/v1/account/recovery')
Query::equal('email', [$email]),
]);
if (!$profile) {
if ($profile->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
+4 -2
View File
@@ -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);
+104 -113
View File
@@ -3,7 +3,6 @@
use Appwrite\Auth\Auth;
use Appwrite\Detector\Detector;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Usage;
use Appwrite\Extend\Exception;
@@ -21,11 +20,11 @@ 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;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\NotFound as NotFoundException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Exception\Restricted as RestrictedException;
use Utopia\Database\Exception\Structure as StructureException;
@@ -352,13 +351,16 @@ function updateAttribute(
if ($type === Database::VAR_RELATIONSHIP) {
$primaryDocumentOptions = \array_merge($attribute->getAttribute('options', []), $options);
$attribute->setAttribute('options', $primaryDocumentOptions);
$dbForProject->updateRelationship(
collection: $collectionId,
id: $key,
newKey: $newKey,
onDelete: $primaryDocumentOptions['onDelete'],
);
try {
$dbForProject->updateRelationship(
collection: $collectionId,
id: $key,
newKey: $newKey,
onDelete: $primaryDocumentOptions['onDelete'],
);
} catch (NotFoundException) {
throw new Exception(Exception::ATTRIBUTE_NOT_FOUND);
}
if ($primaryDocumentOptions['twoWay']) {
$relatedCollection = $dbForProject->getDocument('database_' . $db->getInternalId(), $primaryDocumentOptions['relatedCollection']);
@@ -388,23 +390,35 @@ function updateAttribute(
);
} catch (TruncateException) {
throw new Exception(Exception::ATTRIBUTE_INVALID_RESIZE);
} catch (NotFoundException) {
throw new Exception(Exception::ATTRIBUTE_NOT_FOUND);
}
}
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);
@@ -530,12 +544,7 @@ App::get('/v1/databases')
->inject('response')
->inject('dbForProject')
->action(function (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());
}
$queries = Query::parseQueries($queries);
if (!empty($search)) {
$queries[] = Query::search('search', $search);
@@ -816,22 +825,21 @@ App::post('/v1/databases/:databaseId/collections')
$collectionId = $collectionId == 'unique()' ? ID::unique() : $collectionId;
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
$permissions = Permission::aggregate($permissions) ?? [];
try {
$dbForProject->createDocument('database_' . $database->getInternalId(), new Document([
$collection = $dbForProject->createDocument('database_' . $database->getInternalId(), new Document([
'$id' => $collectionId,
'databaseInternalId' => $database->getInternalId(),
'databaseId' => $databaseId,
'$permissions' => $permissions ?? [],
'$permissions' => $permissions,
'documentSecurity' => $documentSecurity,
'enabled' => $enabled,
'name' => $name,
'search' => implode(' ', [$collectionId, $name]),
]));
$collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId);
$dbForProject->createCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), permissions: $permissions ?? [], documentSecurity: $documentSecurity);
$dbForProject->createCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), permissions: $permissions, documentSecurity: $documentSecurity);
} catch (DuplicateException) {
throw new Exception(Exception::COLLECTION_ALREADY_EXISTS);
} catch (LimitException) {
@@ -849,7 +857,7 @@ App::post('/v1/databases/:databaseId/collections')
});
App::get('/v1/databases/:databaseId/collections')
->alias('/v1/database/collections', ['databaseId' => 'default'])
->alias('/v1/database/collections')
->desc('List collections')
->groups(['api', 'database'])
->label('scope', 'collections.read')
@@ -875,11 +883,7 @@ App::get('/v1/databases/:databaseId/collections')
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$queries = Query::parseQueries($queries);
if (!empty($search)) {
$queries[] = Query::search('search', $search);
@@ -919,7 +923,7 @@ App::get('/v1/databases/:databaseId/collections')
});
App::get('/v1/databases/:databaseId/collections/:collectionId')
->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId')
->desc('Get collection')
->groups(['api', 'database'])
->label('scope', 'collections.read')
@@ -954,7 +958,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId')
});
App::get('/v1/databases/:databaseId/collections/:collectionId/logs')
->alias('/v1/database/collections/:collectionId/logs', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/logs')
->desc('List collection logs')
->groups(['api', 'database'])
->label('scope', 'collections.read')
@@ -988,12 +992,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs')
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$queries = Query::parseQueries($queries);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
$offset = $grouped['offset'] ?? 0;
@@ -1055,7 +1054,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs')
App::put('/v1/databases/:databaseId/collections/:collectionId')
->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId')
->desc('Update collection')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
@@ -1101,12 +1100,16 @@ App::put('/v1/databases/:databaseId/collections/:collectionId')
$enabled ??= $collection->getAttribute('enabled', true);
$collection = $dbForProject->updateDocument('database_' . $database->getInternalId(), $collectionId, $collection
$collection = $dbForProject->updateDocument(
'database_' . $database->getInternalId(),
$collectionId,
$collection
->setAttribute('name', $name)
->setAttribute('$permissions', $permissions)
->setAttribute('documentSecurity', $documentSecurity)
->setAttribute('enabled', $enabled)
->setAttribute('search', implode(' ', [$collectionId, $name])));
->setAttribute('search', \implode(' ', [$collectionId, $name]))
);
$dbForProject->updateCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $permissions, $documentSecurity);
@@ -1119,7 +1122,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId')
});
App::delete('/v1/databases/:databaseId/collections/:collectionId')
->alias('/v1/database/collections/:collectionId', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId')
->desc('Delete collection')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
@@ -1175,7 +1178,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId')
});
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string')
->alias('/v1/database/collections/:collectionId/attributes/string', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/attributes/string')
->desc('Create string attribute')
->groups(['api', 'database', 'schema'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
@@ -1226,14 +1229,13 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string
'filters' => $filters,
]), $response, $dbForProject, $queueForDatabase, $queueForEvents);
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($attribute, Response::MODEL_ATTRIBUTE_STRING);
});
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email')
->alias('/v1/database/collections/:collectionId/attributes/email', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/attributes/email')
->desc('Create email attribute')
->groups(['api', 'database', 'schema'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
@@ -1276,7 +1278,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email'
});
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum')
->alias('/v1/database/collections/:collectionId/attributes/enum', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/attributes/enum')
->desc('Create enum attribute')
->groups(['api', 'database', 'schema'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
@@ -1324,7 +1326,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum')
});
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip')
->alias('/v1/database/collections/:collectionId/attributes/ip', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/attributes/ip')
->desc('Create IP address attribute')
->groups(['api', 'database', 'schema'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
@@ -1367,7 +1369,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip')
});
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url')
->alias('/v1/database/collections/:collectionId/attributes/url', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/attributes/url')
->desc('Create URL attribute')
->groups(['api', 'database', 'schema'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
@@ -1410,7 +1412,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url')
});
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/integer')
->alias('/v1/database/collections/:collectionId/attributes/integer', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/attributes/integer')
->desc('Create integer attribute')
->groups(['api', 'database', 'schema'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
@@ -1440,8 +1442,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) {
// Ensure attribute default is within range
$min = (is_null($min)) ? PHP_INT_MIN : \intval($min);
$max = (is_null($max)) ? PHP_INT_MAX : \intval($max);
$min = \is_null($min) ? PHP_INT_MIN : $min;
$max = \is_null($max) ? PHP_INT_MAX : $max;
if ($min > $max) {
throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Minimum value must be lesser than maximum value');
@@ -1482,7 +1484,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege
});
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float')
->alias('/v1/database/collections/:collectionId/attributes/float', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/attributes/float')
->desc('Create float attribute')
->groups(['api', 'database', 'schema'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
@@ -1512,21 +1514,16 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float'
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) {
// Ensure attribute default is within range
$min = (is_null($min)) ? -PHP_FLOAT_MAX : \floatval($min);
$max = (is_null($max)) ? PHP_FLOAT_MAX : \floatval($max);
$min = \is_null($min) ? -PHP_FLOAT_MAX : $min;
$max = \is_null($max) ? PHP_FLOAT_MAX : $max;
if ($min > $max) {
throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Minimum value must be lesser than maximum value');
}
// Ensure default value is a float
if (!is_null($default)) {
$default = \floatval($default);
}
$validator = new Range($min, $max, Database::VAR_FLOAT);
if (!is_null($default) && !$validator->isValid($default)) {
if (!\is_null($default) && !$validator->isValid($default)) {
throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, $validator->getDescription());
}
@@ -1557,7 +1554,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float'
});
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean')
->alias('/v1/database/collections/:collectionId/attributes/boolean', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/attributes/boolean')
->desc('Create boolean attribute')
->groups(['api', 'database', 'schema'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
@@ -1599,7 +1596,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea
});
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime')
->alias('/v1/database/collections/:collectionId/attributes/datetime', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/attributes/datetime')
->desc('Create datetime attribute')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
@@ -1618,7 +1615,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new DatetimeValidator(), 'Default value for the attribute in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.', true)
->param('default', null, fn (Database $dbForProject) => new DatetimeValidator($dbForProject->getAdapter()->getMinDateTime(), $dbForProject->getAdapter()->getMaxDateTime()), 'Default value for the attribute in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.', true, ['dbForProject'])
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
@@ -1644,7 +1641,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti
});
App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relationship')
->alias('/v1/database/collections/:collectionId/attributes/relationship', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/attributes/relationship')
->desc('Create relationship attribute')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
@@ -1773,7 +1770,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relati
});
App::get('/v1/databases/:databaseId/collections/:collectionId/attributes')
->alias('/v1/database/collections/:collectionId/attributes', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/attributes')
->desc('List attributes')
->groups(['api', 'database'])
->label('scope', 'collections.read')
@@ -1804,16 +1801,12 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes')
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$queries = Query::parseQueries($queries);
\array_push(
$queries,
Query::equal('databaseInternalId', [$database->getInternalId()]),
Query::equal('collectionInternalId', [$collection->getInternalId()]),
Query::equal('databaseInternalId', [$database->getInternalId()])
);
/**
@@ -1822,6 +1815,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes')
$cursor = \array_filter($queries, function ($query) {
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
$cursor = \reset($cursor);
if ($cursor) {
@@ -1832,8 +1826,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes')
$attributeId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->find('attributes', [
Query::equal('collectionInternalId', [$collection->getInternalId()]),
Query::equal('databaseInternalId', [$database->getInternalId()]),
Query::equal('collectionInternalId', [$collection->getInternalId()]),
Query::equal('key', [$attributeId]),
Query::limit(1),
]));
@@ -1857,7 +1851,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes')
});
App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key')
->alias('/v1/database/collections/:collectionId/attributes/:key', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/attributes/:key')
->desc('Get attribute')
->groups(['api', 'database'])
->label('scope', 'collections.read')
@@ -1951,7 +1945,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/strin
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Nullable(new Text(0, 0)), 'Default value for attribute when not provided. Cannot be set when attribute is required.')
->param('size', null, new Integer(), 'Maximum size of the string attribute.', true)
->param('size', null, new Range(1, APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH, Range::TYPE_INTEGER), 'Maximum size of the string attribute.', true)
->param('newKey', null, new Key(), 'New attribute key.', true)
->inject('response')
->inject('dbForProject')
@@ -2309,7 +2303,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/datet
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Nullable(new DatetimeValidator()), 'Default value for attribute when not provided. Cannot be set when attribute is required.')
->param('default', null, fn (Database $dbForProject) => new Nullable(new DatetimeValidator($dbForProject->getAdapter()->getMinDateTime(), $dbForProject->getAdapter()->getMaxDateTime())), 'Default value for attribute when not provided. Cannot be set when attribute is required.', injections: ['dbForProject'])
->param('newKey', null, new Key(), 'New attribute key.', true)
->inject('response')
->inject('dbForProject')
@@ -2390,7 +2384,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/
});
App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key')
->alias('/v1/database/collections/:collectionId/attributes/:key', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/attributes/:key')
->desc('Delete attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
@@ -2504,7 +2498,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key
});
App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
->alias('/v1/database/collections/:collectionId/indexes', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/indexes')
->desc('Create index')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].create')
@@ -2602,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) {
@@ -2616,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;
@@ -2675,7 +2664,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
});
App::get('/v1/databases/:databaseId/collections/:collectionId/indexes')
->alias('/v1/database/collections/:collectionId/indexes', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/indexes')
->desc('List indexes')
->groups(['api', 'database'])
->label('scope', 'collections.read')
@@ -2706,13 +2695,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes')
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$queries = Query::parseQueries($queries);
\array_push($queries, Query::equal('collectionId', [$collectionId]), Query::equal('databaseId', [$databaseId]));
\array_push(
$queries,
Query::equal('databaseId', [$databaseId]),
Query::equal('collectionId', [$collectionId]),
);
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
@@ -3042,10 +3031,12 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
try {
$document = $dbForProject->createDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $document);
} catch (StructureException $exception) {
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $exception->getMessage());
} catch (DuplicateException $exception) {
} catch (StructureException $e) {
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage());
} catch (DuplicateException $e) {
throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS);
} catch (NotFoundException $e) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
// Add $collectionId and $databaseId for all documents
@@ -3176,14 +3167,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
$cursor->setValue($cursorDocument);
}
try {
$documents = $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries);
$total = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries, APP_LIMIT_COUNT);
} catch (AuthorizationException) {
throw new Exception(Exception::USER_UNAUTHORIZED);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$documents = $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries);
$total = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries, APP_LIMIT_COUNT);
// Add $collectionId and $databaseId for all documents
$processDocument = (function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database): bool {
@@ -3469,7 +3454,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
});
App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId')
->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default'])
->alias('/v1/database/collections/:collectionId/documents/:documentId')
->desc('Update document')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].update')
@@ -3647,8 +3632,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
throw new Exception(Exception::USER_UNAUTHORIZED);
} catch (DuplicateException) {
throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS);
} catch (StructureException $exception) {
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $exception->getMessage());
} catch (StructureException $e) {
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage());
} catch (NotFoundException $e) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
// Add $collectionId and $databaseId for all documents
@@ -3757,12 +3744,16 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
throw new Exception(Exception::DOCUMENT_NOT_FOUND);
}
$dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $database, $collection, $documentId) {
$dbForProject->deleteDocument(
'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(),
$documentId
);
});
try {
$dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $database, $collection, $documentId) {
$dbForProject->deleteDocument(
'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(),
$documentId
);
});
} catch (NotFoundException $e) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
// Add $collectionId and $databaseId for all documents
$processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) {
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -135,6 +135,7 @@ App::get('/v1/health/cache')
foreach ($configs as $key => $config) {
foreach ($config as $database) {
try {
/** @var \Utopia\Cache\Adapter $adapter */
$adapter = $pools->get($database)->pop()->getResource();
$checkStart = \microtime(true);
@@ -191,11 +192,11 @@ App::get('/v1/health/queue')
foreach ($configs as $key => $config) {
foreach ($config as $database) {
$checkStart = \microtime(true);
try {
/** @var Connection $adapter */
$adapter = $pools->get($database)->pop()->getResource();
$checkStart = \microtime(true);
if ($adapter->ping()) {
$output[] = new Document([
'name' => $key . " ($database)",
@@ -249,6 +250,7 @@ App::get('/v1/health/pubsub')
foreach ($configs as $key => $config) {
foreach ($config as $database) {
try {
/** @var \Appwrite\PubSub\Adapter $adapter */
$adapter = $pools->get($database)->pop()->getResource();
$checkStart = \microtime(true);
+6 -6
View File
@@ -121,7 +121,7 @@ App::post('/v1/migrations/firebase/oauth')
Query::equal('provider', ['firebase']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity === false || $identity->isEmpty()) {
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
@@ -576,7 +576,7 @@ App::get('/v1/migrations/firebase/report/oauth')
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity === false || $identity->isEmpty()) {
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
@@ -751,13 +751,13 @@ App::get('/v1/migrations/firebase/redirect')
Query::equal('providerEmail', [$email]),
]);
if ($identity !== false && !$identity->isEmpty()) {
if (!$identity->isEmpty()) {
if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
}
if ($identity !== false && !$identity->isEmpty()) {
if (!$identity->isEmpty()) {
$identity = $identity
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
@@ -820,7 +820,7 @@ App::get('/v1/migrations/firebase/projects')
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity === false || $identity->isEmpty()) {
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
@@ -900,7 +900,7 @@ App::get('/v1/migrations/firebase/deauthorize')
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity === false || $identity->isEmpty()) {
if ($identity->isEmpty()) {
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase'); //TODO: Replace with USER_IDENTITY_NOT_FOUND
}
+4 -1
View File
@@ -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']),
]);
+48 -10
View File
@@ -111,6 +111,9 @@ App::post('/v1/projects')
'personalDataCheck' => false,
'mockNumbers' => [],
'sessionAlerts' => false,
'membershipsUserName' => false,
'membershipsUserEmail' => false,
'membershipsMfa' => false,
];
foreach ($auth as $method) {
@@ -648,6 +651,41 @@ App::patch('/v1/projects/:projectId/auth/session-alerts')
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/auth/memberships-privacy')
->desc('Update project memberships privacy attributes')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'projects')
->label('sdk.method', 'updateMembershipsPrivacy')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROJECT)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('userName', true, new Boolean(true), 'Set to true to show userName to members of a team.')
->param('userEmail', true, new Boolean(true), 'Set to true to show email to members of a team.')
->param('mfa', true, new Boolean(true), 'Set to true to show mfa to members of a team.')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, bool $userName, bool $userEmail, bool $mfa, Response $response, Database $dbForConsole) {
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
$auths['membershipsUserName'] = $userName;
$auths['membershipsUserEmail'] = $userEmail;
$auths['membershipsMfa'] = $mfa;
$dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
App::patch('/v1/projects/:projectId/auth/limit')
->desc('Update project users limit')
->groups(['api', 'projects'])
@@ -1060,7 +1098,7 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($webhook === false || $webhook->isEmpty()) {
if ($webhook->isEmpty()) {
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
}
@@ -1103,7 +1141,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($webhook === false || $webhook->isEmpty()) {
if ($webhook->isEmpty()) {
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
}
@@ -1153,7 +1191,7 @@ App::patch('/v1/projects/:projectId/webhooks/:webhookId/signature')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($webhook === false || $webhook->isEmpty()) {
if ($webhook->isEmpty()) {
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
}
@@ -1191,7 +1229,7 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($webhook === false || $webhook->isEmpty()) {
if ($webhook->isEmpty()) {
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
}
@@ -1313,7 +1351,7 @@ App::get('/v1/projects/:projectId/keys/:keyId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($key === false || $key->isEmpty()) {
if ($key->isEmpty()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
@@ -1350,7 +1388,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($key === false || $key->isEmpty()) {
if ($key->isEmpty()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
@@ -1392,7 +1430,7 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($key === false || $key->isEmpty()) {
if ($key->isEmpty()) {
throw new Exception(Exception::KEY_NOT_FOUND);
}
@@ -1550,7 +1588,7 @@ App::get('/v1/projects/:projectId/platforms/:platformId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($platform === false || $platform->isEmpty()) {
if ($platform->isEmpty()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
@@ -1587,7 +1625,7 @@ App::put('/v1/projects/:projectId/platforms/:platformId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($platform === false || $platform->isEmpty()) {
if ($platform->isEmpty()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
@@ -1631,7 +1669,7 @@ App::delete('/v1/projects/:projectId/platforms/:platformId')
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
if ($platform === false || $platform->isEmpty()) {
if ($platform->isEmpty()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
+70 -20
View File
@@ -11,7 +11,6 @@ use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
@@ -37,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')
@@ -51,20 +50,25 @@ 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) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.');
}
$document = $dbForConsole->findOne('rules', [
Query::equal('domain', [$domain]),
]);
$ruleId = md5($domain);
$document = $dbForConsole->getDocument('rules', $ruleId);
if ($document && !$document->isEmpty()) {
if (!$document->isEmpty()) {
if ($document->getAttribute('projectId') === $project->getId()) {
$resourceType = $document->getAttribute('resourceType');
$resourceId = $document->getAttribute('resourceId');
@@ -83,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 {
@@ -103,7 +122,7 @@ App::post('/v1/proxy/rules')
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
$ruleId = ID::unique();
$ruleId = md5($domain->get());
$rule = new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
@@ -116,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';
}
@@ -350,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();
});
+51 -38
View File
@@ -16,7 +16,8 @@ use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\NotFound as NotFoundException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
@@ -131,7 +132,7 @@ App::post('/v1/storage/buckets')
$bucket = $dbForProject->getDocument('buckets', $bucketId);
$dbForProject->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes, permissions: $permissions ?? [], documentSecurity: $fileSecurity);
} catch (Duplicate) {
} catch (DuplicateException) {
throw new Exception(Exception::STORAGE_BUCKET_ALREADY_EXISTS);
}
@@ -273,10 +274,6 @@ App::put('/v1/storage/buckets/:bucketId')
$encryption ??= $bucket->getAttribute('encryption', true);
$antivirus ??= $bucket->getAttribute('antivirus', true);
/**
* Map aggregate permissions into the multiple permissions they represent,
* accounting for the resource type given that some types not allowed specific permissions.
*/
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
@@ -290,11 +287,11 @@ App::put('/v1/storage/buckets/:bucketId')
->setAttribute('encryption', $encryption)
->setAttribute('compression', $compression)
->setAttribute('antivirus', $antivirus));
$dbForProject->updateCollection('bucket_' . $bucket->getInternalId(), $permissions, $fileSecurity);
$queueForEvents
->setParam('bucketId', $bucket->getId())
;
->setParam('bucketId', $bucket->getId());
$response->dynamic($bucket, Response::MODEL_BUCKET);
});
@@ -342,7 +339,7 @@ App::delete('/v1/storage/buckets/:bucketId')
});
App::post('/v1/storage/buckets/:bucketId/files')
->alias('/v1/storage/files', ['bucketId' => 'default'])
->alias('/v1/storage/files')
->desc('Create file')
->groups(['api', 'storage'])
->label('scope', 'files.write')
@@ -670,7 +667,11 @@ App::post('/v1/storage/buckets/:bucketId/files')
'metadata' => $metadata,
]);
$file = $dbForProject->createDocument('bucket_' . $bucket->getInternalId(), $doc);
try {
$file = $dbForProject->createDocument('bucket_' . $bucket->getInternalId(), $doc);
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
} else {
$file = $file
->setAttribute('chunksUploaded', $chunksUploaded)
@@ -686,15 +687,19 @@ App::post('/v1/storage/buckets/:bucketId/files')
if (!$validator->isValid($bucket->getCreate())) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
try {
$file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
}
}
$queueForEvents
->setParam('bucketId', $bucket->getId())
->setParam('fileId', $file->getId())
->setContext('bucket', $bucket)
;
->setContext('bucket', $bucket);
$metadata = null; // was causing leaks as it was passed by reference
@@ -704,7 +709,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
});
App::get('/v1/storage/buckets/:bucketId/files')
->alias('/v1/storage/files', ['bucketId' => 'default'])
->alias('/v1/storage/files')
->desc('List files')
->groups(['api', 'storage'])
->label('scope', 'files.read')
@@ -739,11 +744,7 @@ App::get('/v1/storage/buckets/:bucketId/files')
throw new Exception(Exception::USER_UNAUTHORIZED);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$queries = Query::parseQueries($queries);
if (!empty($search)) {
$queries[] = Query::search('search', $search);
@@ -781,12 +782,16 @@ App::get('/v1/storage/buckets/:bucketId/files')
$filterQueries = Query::groupByType($queries)['filters'];
if ($fileSecurity && !$valid) {
$files = $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries);
$total = $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT);
} else {
$files = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries));
$total = Authorization::skip(fn () => $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT));
try {
if ($fileSecurity && !$valid) {
$files = $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries);
$total = $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT);
} else {
$files = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries));
$total = Authorization::skip(fn () => $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT));
}
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$response->dynamic(new Document([
@@ -796,7 +801,7 @@ App::get('/v1/storage/buckets/:bucketId/files')
});
App::get('/v1/storage/buckets/:bucketId/files/:fileId')
->alias('/v1/storage/files/:fileId', ['bucketId' => 'default'])
->alias('/v1/storage/files/:fileId')
->desc('Get file')
->groups(['api', 'storage'])
->label('scope', 'files.read')
@@ -844,7 +849,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
});
App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->alias('/v1/storage/files/:fileId/preview', ['bucketId' => 'default'])
->alias('/v1/storage/files/:fileId/preview')
->desc('Get file preview')
->groups(['api', 'storage'])
->label('scope', 'files.read')
@@ -1017,7 +1022,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
});
App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
->alias('/v1/storage/files/:fileId/download', ['bucketId' => 'default'])
->alias('/v1/storage/files/:fileId/download')
->desc('Get file for download')
->groups(['api', 'storage'])
->label('scope', 'files.read')
@@ -1158,7 +1163,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
});
App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
->alias('/v1/storage/files/:fileId/view', ['bucketId' => 'default'])
->alias('/v1/storage/files/:fileId/view')
->desc('Get file for view')
->groups(['api', 'storage'])
->label('scope', 'files.read')
@@ -1465,7 +1470,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
});
App::put('/v1/storage/buckets/:bucketId/files/:fileId')
->alias('/v1/storage/files/:fileId', ['bucketId' => 'default'])
->alias('/v1/storage/files/:fileId')
->desc('Update file')
->groups(['api', 'storage'])
->label('scope', 'files.write')
@@ -1555,10 +1560,14 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
$file->setAttribute('name', $name);
}
if ($fileSecurity && !$valid) {
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
} else {
$file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
try {
if ($fileSecurity && !$valid) {
$file = $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file);
} else {
$file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file));
}
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$queueForEvents
@@ -1641,10 +1650,14 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->setResource('file/' . $fileId)
;
if ($fileSecurity && !$valid) {
$deleted = $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId);
} else {
$deleted = Authorization::skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId));
try {
if ($fileSecurity && !$valid) {
$deleted = $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId);
} else {
$deleted = Authorization::skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId));
}
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
if (!$deleted) {
+86 -38
View File
@@ -467,17 +467,17 @@ App::post('/v1/teams/:teamId/memberships')
$name = empty($name) ? $invitee->getAttribute('name', '') : $name;
} elseif (!empty($email)) {
$invitee = $dbForProject->findOne('users', [Query::equal('email', [$email])]); // Get user by email address
if (!empty($invitee) && !empty($phone) && $invitee->getAttribute('phone', '') !== $phone) {
if (!$invitee->isEmpty() && !empty($phone) && $invitee->getAttribute('phone', '') !== $phone) {
throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given email and phone doesn\'t match', 409);
}
} elseif (!empty($phone)) {
$invitee = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]);
if (!empty($invitee) && !empty($email) && $invitee->getAttribute('email', '') !== $email) {
if (!$invitee->isEmpty() && !empty($email) && $invitee->getAttribute('email', '') !== $email) {
throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given phone and email doesn\'t match', 409);
}
}
if (empty($invitee)) { // Create new user if no user with same email found
if ($invitee->isEmpty()) { // Create new user if no user with same email found
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
if (!$isPrivilegedUser && !$isAppUser && $limit !== 0 && $project->getId() !== 'console') { // check users limit, console invites are allways allowed.
@@ -492,7 +492,7 @@ App::post('/v1/teams/:teamId/memberships')
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
if (!$identityWithMatchingEmail->isEmpty()) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
@@ -727,9 +727,9 @@ App::get('/v1/teams/:teamId/memberships')
->param('queries', [], new Memberships(), '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(', ', Memberships::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')
->action(function (string $teamId, array $queries, string $search, Response $response, Database $dbForProject) {
->action(function (string $teamId, array $queries, string $search, Response $response, Document $project, Database $dbForProject) {
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
@@ -790,27 +790,51 @@ App::get('/v1/teams/:teamId/memberships')
$memberships = array_filter($memberships, fn (Document $membership) => !empty($membership->getAttribute('userId')));
$memberships = array_map(function ($membership) use ($dbForProject, $team) {
$user = $dbForProject->getDocument('users', $membership->getAttribute('userId'));
$membershipsPrivacy = [
'userName' => $project->getAttribute('auths', [])['membershipsUserName'] ?? true,
'userEmail' => $project->getAttribute('auths', [])['membershipsUserEmail'] ?? true,
'mfa' => $project->getAttribute('auths', [])['membershipsMfa'] ?? true,
];
$mfa = $user->getAttribute('mfa', false);
if ($mfa) {
$totp = TOTP::getAuthenticatorFromUser($user);
$totpEnabled = $totp && $totp->getAttribute('verified', false);
$emailEnabled = $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false);
$phoneEnabled = $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false);
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
if (!$totpEnabled && !$emailEnabled && !$phoneEnabled) {
$mfa = false;
$membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) {
return $privacy || $isPrivilegedUser || $isAppUser;
}, $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']) {
$mfa = $user->getAttribute('mfa', false);
if ($mfa) {
$totp = TOTP::getAuthenticatorFromUser($user);
$totpEnabled = $totp && $totp->getAttribute('verified', false);
$emailEnabled = $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false);
$phoneEnabled = $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false);
if (!$totpEnabled && !$emailEnabled && !$phoneEnabled) {
$mfa = false;
}
}
$membership->setAttribute('mfa', $mfa);
}
$membership
->setAttribute('mfa', $mfa)
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email'))
;
if ($membershipsPrivacy['userName']) {
$membership->setAttribute('userName', $user->getAttribute('name'));
}
if ($membershipsPrivacy['userEmail']) {
$membership->setAttribute('userEmail', $user->getAttribute('email'));
}
$membership->setAttribute('teamName', $team->getAttribute('name'));
return $membership;
}, $memberships);
@@ -837,8 +861,9 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
->param('teamId', '', new UID(), 'Team ID.')
->param('membershipId', '', new UID(), 'Membership ID.')
->inject('response')
->inject('project')
->inject('dbForProject')
->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject) {
->action(function (string $teamId, string $membershipId, Response $response, Document $project, Database $dbForProject) {
$team = $dbForProject->getDocument('teams', $teamId);
@@ -852,27 +877,50 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
throw new Exception(Exception::MEMBERSHIP_NOT_FOUND);
}
$user = $dbForProject->getDocument('users', $membership->getAttribute('userId'));
$membershipsPrivacy = [
'userName' => $project->getAttribute('auths', [])['membershipsUserName'] ?? true,
'userEmail' => $project->getAttribute('auths', [])['membershipsUserEmail'] ?? true,
'mfa' => $project->getAttribute('auths', [])['membershipsMfa'] ?? true,
];
$mfa = $user->getAttribute('mfa', false);
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
if ($mfa) {
$totp = TOTP::getAuthenticatorFromUser($user);
$totpEnabled = $totp && $totp->getAttribute('verified', false);
$emailEnabled = $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false);
$phoneEnabled = $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false);
$membershipsPrivacy = array_map(function ($privacy) use ($isPrivilegedUser, $isAppUser) {
return $privacy || $isPrivilegedUser || $isAppUser;
}, $membershipsPrivacy);
if (!$totpEnabled && !$emailEnabled && !$phoneEnabled) {
$mfa = false;
$user = !empty(array_filter($membershipsPrivacy))
? $dbForProject->getDocument('users', $membership->getAttribute('userId'))
: new Document();
if ($membershipsPrivacy['mfa']) {
$mfa = $user->getAttribute('mfa', false);
if ($mfa) {
$totp = TOTP::getAuthenticatorFromUser($user);
$totpEnabled = $totp && $totp->getAttribute('verified', false);
$emailEnabled = $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false);
$phoneEnabled = $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false);
if (!$totpEnabled && !$emailEnabled && !$phoneEnabled) {
$mfa = false;
}
}
$membership->setAttribute('mfa', $mfa);
}
$membership
->setAttribute('mfa', $mfa)
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email'))
;
if ($membershipsPrivacy['userName']) {
$membership->setAttribute('userName', $user->getAttribute('name'));
}
if ($membershipsPrivacy['userEmail']) {
$membership->setAttribute('userEmail', $user->getAttribute('email'));
}
$membership->setAttribute('teamName', $team->getAttribute('name'));
$response->dynamic($membership, Response::MODEL_MEMBERSHIP);
});
+21 -39
View File
@@ -51,7 +51,7 @@ use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
/** TODO: Remove function when we move to using utopia/platform */
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks): Document
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
{
$plaintextPassword = $password;
$hashOptionsObject = (\is_string($hashOptions)) ? \json_decode($hashOptions, true) : $hashOptions; // Cast to JSON array
@@ -64,7 +64,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
$identityWithMatchingEmail = $dbForProject->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
if (!$identityWithMatchingEmail->isEmpty()) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
}
@@ -141,7 +141,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
$existingTarget = $dbForProject->findOne('targets', [
Query::equal('identifier', [$email]),
]);
if ($existingTarget) {
if (!$existingTarget->isEmpty()) {
$user->setAttribute('targets', $existingTarget, Document::SET_TYPE_APPEND);
}
}
@@ -165,7 +165,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
$existingTarget = $dbForProject->findOne('targets', [
Query::equal('identifier', [$phone]),
]);
if ($existingTarget) {
if (!$existingTarget->isEmpty()) {
$user->setAttribute('targets', $existingTarget, Document::SET_TYPE_APPEND);
}
}
@@ -176,15 +176,12 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
$queueForEvents->setParam('userId', $user->getId());
return $user;
}
App::post('/v1/users')
->desc('Create user')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.event', 'user.create')
->label('audits.resource', 'user/{response.$id}')
@@ -203,10 +200,9 @@ App::post('/v1/users')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('queueForEvents')
->inject('hooks')
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $queueForEvents, $hooks);
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($user, Response::MODEL_USER);
@@ -215,7 +211,6 @@ App::post('/v1/users')
App::post('/v1/users/bcrypt')
->desc('Create user with bcrypt password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.event', 'user.create')
->label('audits.resource', 'user/{response.$id}')
@@ -233,10 +228,9 @@ App::post('/v1/users/bcrypt')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('queueForEvents')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -246,7 +240,6 @@ App::post('/v1/users/bcrypt')
App::post('/v1/users/md5')
->desc('Create user with MD5 password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.event', 'user.create')
->label('audits.resource', 'user/{response.$id}')
@@ -264,10 +257,9 @@ App::post('/v1/users/md5')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('queueForEvents')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -277,7 +269,6 @@ App::post('/v1/users/md5')
App::post('/v1/users/argon2')
->desc('Create user with Argon2 password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.event', 'user.create')
->label('audits.resource', 'user/{response.$id}')
@@ -295,10 +286,9 @@ App::post('/v1/users/argon2')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('queueForEvents')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -308,7 +298,6 @@ App::post('/v1/users/argon2')
App::post('/v1/users/sha')
->desc('Create user with SHA password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.event', 'user.create')
->label('audits.resource', 'user/{response.$id}')
@@ -327,16 +316,15 @@ App::post('/v1/users/sha')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('queueForEvents')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$options = '{}';
if (!empty($passwordVersion)) {
$options = '{"version":"' . $passwordVersion . '"}';
}
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -346,7 +334,6 @@ App::post('/v1/users/sha')
App::post('/v1/users/phpass')
->desc('Create user with PHPass password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.event', 'user.create')
->label('audits.resource', 'user/{response.$id}')
@@ -364,10 +351,9 @@ App::post('/v1/users/phpass')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('queueForEvents')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -377,7 +363,6 @@ App::post('/v1/users/phpass')
App::post('/v1/users/scrypt')
->desc('Create user with Scrypt password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.event', 'user.create')
->label('audits.resource', 'user/{response.$id}')
@@ -400,9 +385,8 @@ App::post('/v1/users/scrypt')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('queueForEvents')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$options = [
'salt' => $passwordSalt,
'costCpu' => $passwordCpu,
@@ -411,7 +395,7 @@ App::post('/v1/users/scrypt')
'length' => $passwordLength
];
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -421,7 +405,6 @@ App::post('/v1/users/scrypt')
App::post('/v1/users/scrypt-modified')
->desc('Create user with Scrypt modified password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.event', 'user.create')
->label('audits.resource', 'user/{response.$id}')
@@ -442,10 +425,9 @@ App::post('/v1/users/scrypt-modified')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('queueForEvents')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks);
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -1232,7 +1214,7 @@ App::patch('/v1/users/:userId/email')
Query::equal('providerEmail', [$email]),
Query::notEqual('userInternalId', $user->getInternalId()),
]);
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
if (!$identityWithMatchingEmail->isEmpty()) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
+68 -42
View File
@@ -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,23 +104,24 @@ $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]),
Query::orderDesc('$createdAt'),
]));
if ($latestComment !== false && !$latestComment->isEmpty()) {
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) {
@@ -371,13 +399,11 @@ App::get('/v1/vcs/github/callback')
$identity = $dbForConsole->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if ($identity !== false && !$identity->isEmpty()) {
if (!$identity->isEmpty()) {
if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
}
if ($identity !== false && !$identity->isEmpty()) {
$identity = $identity
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
@@ -418,7 +444,7 @@ App::get('/v1/vcs/github/callback')
Query::equal('projectInternalId', [$projectInternalId])
]);
if ($installation === false || $installation->isEmpty()) {
if ($installation->isEmpty()) {
$teamId = $project->getAttribute('teamId', '');
$installation = new Document([
@@ -726,7 +752,7 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories')
Query::equal('provider', ['github']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity === false || $identity->isEmpty()) {
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
@@ -936,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),
@@ -948,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', [
+150 -87
View File
@@ -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;
@@ -29,7 +30,6 @@ use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Domains\Domain;
use Utopia\DSN\DSN;
@@ -38,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;
@@ -52,19 +53,14 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
$host = $request->getHostname() ?? '';
$route = Authorization::skip(
fn () => $dbForConsole->find('rules', [
Query::equal('domain', [$host]),
Query::limit(1)
])
)[0] ?? null;
$rule = Authorization::skip(fn () => $dbForConsole->getDocument('rules', md5($host)));
if ($route === null) {
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.');
}
@@ -76,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) {
@@ -96,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'] ?? '');
@@ -121,7 +121,6 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
$path .= '?' . $query;
}
$body = $swooleRequest->getContent() ?? '';
$method = $swooleRequest->server['request_method'];
@@ -129,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');
}
@@ -171,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);
@@ -218,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
@@ -232,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);
@@ -255,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', '');
}
@@ -271,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', ''),
@@ -299,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
);
@@ -337,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);
@@ -361,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', '');
@@ -471,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;
}
}
@@ -518,19 +564,18 @@ App::init()
if (!empty($envDomain) && $envDomain !== 'localhost') {
$mainDomain = $envDomain;
} else {
$domainDocument = $dbForConsole->findOne('rules', [Query::orderAsc('$id')]);
$mainDomain = $domainDocument ? $domainDocument->getAttribute('domain') : $domain->get();
$domainDocument = $dbForConsole->getDocument('rules', md5($envDomain));
$mainDomain = !$domainDocument->isEmpty() ? $domainDocument->getAttribute('domain') : $domain->get();
}
if ($mainDomain !== $domain->get()) {
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
} else {
$domainDocument = $dbForConsole->findOne('rules', [
Query::equal('domain', [$domain->get()])
]);
$domainDocument = $dbForConsole->getDocument('rules', md5($domain->get()));
if (!$domainDocument) {
if ($domainDocument->isEmpty()) {
$domainDocument = new Document([
'$id' => md5($domain->get()),
'domain' => $domain->get(),
'resourceType' => 'api',
'status' => 'verifying',
@@ -690,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;
}
}
@@ -775,6 +821,8 @@ App::error()
case 'Utopia\Database\Exception\Relationship':
$error = new AppwriteException(AppwriteException::RELATIONSHIP_VALUE_INVALID, $error->getMessage(), previous: $error);
break;
case 'Utopia\Database\Exception\NotFound':
$error = new AppwriteException(AppwriteException::COLLECTION_NOT_FOUND, $error->getMessage(), previous: $error);
}
$code = $error->getCode();
@@ -982,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');
}
}
});
@@ -1009,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');
}
}
});
@@ -1101,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);
+73 -52
View File
@@ -11,10 +11,11 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Messaging;
use Appwrite\Event\Realtime;
use Appwrite\Event\Usage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Abuse;
@@ -28,6 +29,7 @@ use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Queue\Connection;
use Utopia\System\System;
use Utopia\Validator\WhiteList;
@@ -57,8 +59,36 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
return $label;
};
$databaseListener = function (string $event, Document $document, Document $project, Usage $queueForUsage, Database $dbForProject) {
$eventDatabaseListener = function (Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
// Only trigger events for user creation with the database listener.
if ($document->getCollection() !== 'users') {
return;
}
$queueForEvents
->setEvent('users.[userId].create')
->setParam('userId', $document->getId())
->setPayload($response->output($document, Response::MODEL_USER));
// Trigger functions, webhooks, and realtime events
$queueForFunctions
->from($queueForEvents)
->trigger();
$queueForWebhooks
->from($queueForEvents)
->trigger();
if ($queueForEvents->getProject()->getId() === 'console') {
return;
}
$queueForRealtime
->from($queueForEvents)
->trigger();
};
$usageDatabaseListener = function (string $event, Document $document, Usage $queueForUsage) {
$value = 1;
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$value = -1;
@@ -133,6 +163,15 @@ $databaseListener = function (string $event, Document $document, Document $proje
$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);
@@ -353,6 +392,7 @@ App::init()
->inject('response')
->inject('project')
->inject('user')
->inject('queue')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('queueForAudits')
@@ -362,7 +402,7 @@ App::init()
->inject('queueForUsage')
->inject('dbForProject')
->inject('mode')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, string $mode) use ($databaseListener) {
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Connection $queue, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, string $mode) use ($usageDatabaseListener, $eventDatabaseListener) {
$route = $utopia->getRoute();
@@ -456,9 +496,24 @@ App::init()
$queueForBuilds->setProject($project);
$queueForMessaging->setProject($project);
// Clone the queues, to prevent events triggered by the database listener
// from overwriting the events that are supposed to be triggered in the shutdown hook.
$queueForEventsClone = new Event($queue);
$queueForFunctions = new Func($queue);
$queueForWebhooks = new Webhook($queue);
$queueForRealtime = new Realtime();
$dbForProject
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $databaseListener($event, $document, $project, $queueForUsage, $dbForProject))
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $databaseListener($event, $document, $project, $queueForUsage, $dbForProject));
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForUsage))
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForUsage))
->on(Database::EVENT_DOCUMENT_CREATE, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener(
$document,
$response,
$queueForEventsClone->from($queueForEvents),
$queueForFunctions->from($queueForEvents),
$queueForWebhooks->from($queueForEvents),
$queueForRealtime->from($queueForEvents)
));
$useCache = $route->getLabel('cache', false);
if ($useCache) {
@@ -591,11 +646,13 @@ App::shutdown()
->inject('queueForDatabase')
->inject('queueForBuilds')
->inject('queueForMessaging')
->inject('dbForProject')
->inject('queueForFunctions')
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
->inject('mode')
->inject('dbForConsole')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Usage $queueForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Database $dbForProject, Func $queueForFunctions, string $mode, Database $dbForConsole) use ($parseLabel) {
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Usage $queueForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, string $mode, Database $dbForConsole) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -604,54 +661,18 @@ App::shutdown()
$queueForEvents->setPayload($responsePayload);
}
/**
* Trigger functions.
*/
if (!$queueForEvents->isPaused()) {
$queueForFunctions
->from($queueForEvents)
->trigger();
}
/**
* Trigger webhooks.
*/
$queueForEvents
->setClass(Event::WEBHOOK_CLASS_NAME)
->setQueue(Event::WEBHOOK_QUEUE_NAME)
$queueForWebhooks
->from($queueForEvents)
->trigger();
$queueForFunctions
->from($queueForEvents)
->trigger();
/**
* Trigger realtime.
*/
if ($project->getId() !== 'console') {
$allEvents = Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams());
$payload = new Document($queueForEvents->getPayload());
$db = $queueForEvents->getContext('database');
$collection = $queueForEvents->getContext('collection');
$bucket = $queueForEvents->getContext('bucket');
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $payload,
project: $project,
database: $db,
collection: $collection,
bucket: $bucket,
);
Realtime::send(
projectId: $target['projectId'] ?? $project->getId(),
payload: $queueForEvents->getRealtimePayload(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles'],
options: [
'permissionsChanged' => $target['permissionsChanged'],
'userId' => $queueForEvents->getParam('userId')
]
);
$queueForRealtime
->from($queueForEvents)
->trigger();
}
}
+5 -2
View File
@@ -39,8 +39,7 @@ $http
->set([
'worker_num' => $workerNumber,
'open_http2_protocol' => true,
'http_compression' => true,
'http_compression_level' => 6,
'http_compression' => false,
'package_max_length' => $payloadSize,
'buffer_output_size' => $payloadSize,
]);
@@ -61,6 +60,8 @@ include __DIR__ . '/controllers/general.php';
$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $register) {
$app = new App('UTC');
$app->setCompression(true);
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
go(function () use ($register, $app) {
$pools = $register->get('pools');
@@ -244,6 +245,8 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
}
$app = new App('UTC');
$app->setCompression(true);
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
$pools = $register->get('pools');
App::setResource('pools', fn () => $pools);
+41 -9
View File
@@ -31,7 +31,9 @@ use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Realtime;
use Appwrite\Event\Usage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Functions\Specification;
use Appwrite\GraphQL\Promises\Adapter\Swoole;
@@ -40,6 +42,7 @@ use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Origin;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\PubSub\Adapter\Redis as PubSub;
use Appwrite\URL\URL as AppwriteURL;
use Appwrite\Utopia\Request;
use MaxMind\Db\Reader;
@@ -132,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';
@@ -150,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';
@@ -255,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';
@@ -273,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';
@@ -310,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
@@ -317,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');
@@ -342,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
@@ -564,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),
]);
}
@@ -971,7 +993,10 @@ $register->set('pools', function () {
$adapter->setDatabase($dsn->getPath());
break;
case 'pubsub':
$adapter = $resource();
$adapter = match ($dsn->getScheme()) {
'redis' => new PubSub($resource()),
default => null
};
break;
case 'queue':
$adapter = match ($dsn->getScheme()) {
@@ -1134,6 +1159,12 @@ App::setResource('queueForDeletes', function (Connection $queue) {
App::setResource('queueForEvents', function (Connection $queue) {
return new Event($queue);
}, ['queue']);
App::setResource('queueForWebhooks', function (Connection $queue) {
return new Webhook($queue);
}, ['queue']);
App::setResource('queueForRealtime', function () {
return new Realtime();
}, []);
App::setResource('queueForAudits', function (Connection $queue) {
return new Audit($queue);
}, ['queue']);
@@ -1530,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']);
@@ -1812,9 +1847,6 @@ App::setResource('team', function (Document $project, Database $dbForConsole, Ap
]);
});
if (!$team) {
$team = new Document([]);
}
return $team;
}, ['project', 'dbForConsole', 'utopia', 'request']);
+31 -9
View File
@@ -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();
@@ -365,17 +379,16 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
}
$start = time();
$redis = $register->get('pools')->get('pubsub')->pop()->getResource(); /** @var Redis $redis */
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
if ($redis->ping(true)) {
/** @var \Appwrite\PubSub\Adapter $pubsub */
$pubsub = $register->get('pools')->get('pubsub')->pop()->getResource();
if ($pubsub->ping(true)) {
$attempts = 0;
Console::success('Pub/sub connection established (worker: ' . $workerId . ')');
} else {
Console::error('Pub/sub failed (worker: ' . $workerId . ')');
}
$redis->subscribe(['realtime'], function (Redis $redis, string $channel, string $payload) use ($server, $workerId, $stats, $register, $realtime) {
$pubsub->subscribe(['realtime'], function (mixed $redis, string $channel, string $payload) use ($server, $workerId, $stats, $register, $realtime) {
$event = json_decode($payload, true);
if ($event['permissionsChanged'] && isset($event['userId'])) {
@@ -417,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);
}
});
@@ -520,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')
@@ -593,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.');
@@ -653,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);
});
+23 -20
View File
@@ -73,6 +73,7 @@ $image = $this->getParam('image', '');
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_LOCALE
- _APP_COMPRESSION_MIN_SIZE_BYTES
- _APP_CONSOLE_WHITELIST_ROOT
- _APP_CONSOLE_WHITELIST_EMAILS
- _APP_CONSOLE_SESSION_ALERTS
@@ -85,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
@@ -131,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
@@ -399,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
@@ -489,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
@@ -795,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
@@ -807,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
View File
@@ -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();
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
php /usr/src/code/app/worker.php insights $@
+7 -2
View File
@@ -48,10 +48,10 @@
"utopia-php/abuse": "0.43.0",
"utopia-php/analytics": "0.10.*",
"utopia-php/audit": "0.43.0",
"utopia-php/cache": "0.10.*",
"utopia-php/cache": "0.11.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.53.8",
"utopia-php/database": "0.53.16",
"utopia-php/domains": "0.5.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.*",
@@ -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
+1956 -404
View File
File diff suppressed because it is too large Load Diff
+96 -16
View File
@@ -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
@@ -96,6 +97,7 @@ services:
- _APP_EDITION
- _APP_WORKER_PER_CORE
- _APP_LOCALE
- _APP_COMPRESSION_MIN_SIZE_BYTES
- _APP_CONSOLE_WHITELIST_ROOT
- _APP_CONSOLE_WHITELIST_EMAILS
- _APP_CONSOLE_SESSION_ALERTS
@@ -109,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
@@ -155,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
@@ -418,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:
@@ -542,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
@@ -869,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
@@ -887,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
@@ -1053,4 +1133,4 @@ volumes:
appwrite-certificates:
appwrite-functions:
appwrite-builds:
appwrite-config:
appwrite-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).
+1 -1
View File
@@ -1 +1 @@
Get a team member by the membership unique id. All team members have read access for this resource.
Get a team member by the membership unique id. All team members have read access for this resource. Hide sensitive attributes from the response by toggling membership privacy in the Console.
+1 -1
View File
@@ -1 +1 @@
Use this endpoint to list a team's members using the team's ID. All team members have read access to this endpoint.
Use this endpoint to list a team's members using the team's ID. All team members have read access to this endpoint. Hide sensitive attributes from the response by toggling membership privacy in the Console.
+4
View File
@@ -121,6 +121,10 @@ class Audit extends Event
*/
public function trigger(): string|bool
{
if ($this->paused) {
return false;
}
$client = new Client($this->queue, $this->connection);
return $client->enqueue([
+4
View File
@@ -112,6 +112,10 @@ class Build extends Event
*/
public function trigger(): string|bool
{
if ($this->paused) {
return false;
}
$client = new Client($this->queue, $this->connection);
return $client->enqueue([
+4
View File
@@ -74,6 +74,10 @@ class Certificate extends Event
*/
public function trigger(): string|bool
{
if ($this->paused) {
return false;
}
$client = new Client($this->queue, $this->connection);
return $client->enqueue([
+4
View File
@@ -108,6 +108,10 @@ class Database extends Event
*/
public function trigger(): string|bool
{
if ($this->paused) {
return false;
}
try {
$dsn = new DSN($this->getProject()->getAttribute('database'));
} catch (\InvalidArgumentException) {
+4
View File
@@ -140,6 +140,10 @@ class Delete extends Event
*/
public function trigger(): string|bool
{
if ($this->paused) {
return false;
}
$client = new Client($this->queue, $this->connection);
return $client->enqueue([
+37 -25
View File
@@ -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 = '';
@@ -65,6 +71,24 @@ class Event
{
}
/**
* Set paused state for this event.
*/
public function setPaused(bool $paused): self
{
$this->paused = $paused;
return $this;
}
/**
* Get paused state for this event.
*/
public function getPaused(): bool
{
return $this->paused;
}
/**
* Set queue used for this event.
*
@@ -204,19 +228,6 @@ class Event
return $this->payload;
}
public function getRealtimePayload(): array
{
$payload = [];
foreach ($this->payload as $key => $value) {
if (!isset($this->sensitive[$key])) {
$payload[$key] = $value;
}
}
return $payload;
}
/**
* Set context for this event.
*
@@ -530,20 +541,21 @@ class Event
}
/**
* Get the value of paused
* Generate a function event from a base event
*
* @param Event $event
*
* @return self
*
*/
public function isPaused(): bool
public function from(Event $event): self
{
return $this->paused;
}
/**
* Set the value of paused
*/
public function setPaused(bool $paused): self
{
$this->paused = $paused;
$this->project = $event->getProject();
$this->user = $event->getUser();
$this->payload = $event->getPayload();
$this->event = $event->getEvent();
$this->params = $event->getParams();
$this->context = $event->context;
return $this;
}
}
-18
View File
@@ -238,22 +238,4 @@ class Func extends Event
'method' => $this->method,
]);
}
/**
* Generate a function event from a base event
*
* @param Event $event
*
* @return self
*
*/
public function from(Event $event): self
{
$this->project = $event->getProject();
$this->user = $event->getUser();
$this->payload = $event->getPayload();
$this->event = $event->getEvent();
$this->params = $event->getParams();
return $this;
}
}
+141
View File
@@ -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;
}
}
+4
View File
@@ -404,6 +404,10 @@ class Mail extends Event
*/
public function trigger(): string|bool
{
if ($this->paused) {
return false;
}
$client = new Client($this->queue, $this->connection);
return $client->enqueue([
+4
View File
@@ -182,6 +182,10 @@ class Messaging extends Event
*/
public function trigger(): string | bool
{
if ($this->paused) {
return false;
}
$client = new Client($this->queue, $this->connection);
return $client->enqueue([
+3
View File
@@ -75,6 +75,9 @@ class Migration extends Event
*/
public function trigger(): string|bool
{
if ($this->paused) {
return false;
}
$client = new Client($this->queue, $this->connection);
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace Appwrite\Event;
use Appwrite\Messaging\Adapter\Realtime as RealtimeAdapter;
use Utopia\Database\Document;
class Realtime extends Event
{
public function __construct()
{
}
public function getRealtimePayload(): array
{
$payload = [];
foreach ($this->payload as $key => $value) {
if (!isset($this->sensitive[$key])) {
$payload[$key] = $value;
}
}
return $payload;
}
/**
* Execute Event.
*
* @return string|bool
* @throws InvalidArgumentException
*/
public function trigger(): string|bool
{
if ($this->paused || empty($this->event)) {
return false;
}
$allEvents = Event::generateEvents($this->getEvent(), $this->getParams());
$payload = new Document($this->getPayload());
$db = $this->getContext('database');
$collection = $this->getContext('collection');
$bucket = $this->getContext('bucket');
$target = RealtimeAdapter::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $payload,
project: $this->getProject(),
database: $db,
collection: $collection,
bucket: $bucket,
);
RealtimeAdapter::send(
projectId: $target['projectId'] ?? $this->getProject()->getId(),
payload: $this->getRealtimePayload(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles'],
options: [
'permissionsChanged' => $target['permissionsChanged'],
'userId' => $this->getParam('userId')
]
);
return true;
}
}
+4
View File
@@ -57,6 +57,10 @@ class Usage extends Event
*/
public function trigger(): string|bool
{
if ($this->paused) {
return false;
}
$client = new Client($this->queue, $this->connection);
return $client->enqueue([
'project' => $this->getProject(),
+4
View File
@@ -38,6 +38,10 @@ class UsageDump extends Event
*/
public function trigger(): string|bool
{
if ($this->paused) {
return false;
}
$client = new Client($this->queue, $this->connection);
return $client->enqueue([
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace Appwrite\Event;
use Utopia\Queue\Connection;
class Webhook extends Event
{
public function __construct(protected Connection $connection)
{
parent::__construct($connection);
$this
->setQueue(Event::WEBHOOK_QUEUE_NAME)
->setClass(Event::WEBHOOK_CLASS_NAME);
}
}
+5
View File
@@ -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';
+30 -15
View File
@@ -7,7 +7,6 @@ use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\System\System;
class Realtime extends Adapter
{
@@ -139,20 +138,26 @@ class Realtime extends Adapter
$permissionsChanged = array_key_exists('permissionsChanged', $options) && $options['permissionsChanged'];
$userId = array_key_exists('userId', $options) ? $options['userId'] : null;
$redis = new \Redis(); //TODO: make this part of the constructor
$redis->connect(System::getEnv('_APP_REDIS_HOST', ''), System::getEnv('_APP_REDIS_PORT', ''));
$redis->publish('realtime', json_encode([
'project' => $projectId,
'roles' => $roles,
'permissionsChanged' => $permissionsChanged,
'userId' => $userId,
'data' => [
'events' => $events,
'channels' => $channels,
'timestamp' => DateTime::formatTz(DateTime::now()),
'payload' => $payload
]
]));
global $register;
$pubsub = $register->get('pools')->get('pubsub')->pop();
try {
/** @var \Appwrite\PubSub\Adapter $redis */
$redis = $pubsub->getResource();
$redis->publish('realtime', json_encode([
'project' => $projectId,
'roles' => $roles,
'permissionsChanged' => $permissionsChanged,
'userId' => $userId,
'data' => [
'events' => $events,
'channels' => $channels,
'timestamp' => DateTime::formatTz(DateTime::now()),
'payload' => $payload
]
]));
} finally {
$pubsub->reclaim();
}
}
/**
@@ -344,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';
+1 -1
View File
@@ -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;
+4
View File
@@ -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());
}
}
@@ -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&timestamped 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);
}
}

Some files were not shown because too many files have changed in this diff Show More