Compare commits

...
Author SHA1 Message Date
eldadfux a0167d6c6c Fix for when vcs comment is empty 2026-03-09 20:17:32 +01:00
Matej BačoandGitHub 74ab41f2b5 Merge pull request #11448 from appwrite/revert-11447-revert-10862-feat-1.8.x-new-schema
Revert "Revert "Features with schema changes""
2026-03-09 17:19:08 +01:00
Matej Bačo d8954f424c Fix specs tests 2026-03-09 15:09:57 +01:00
Matej Bačo 38493094db Fix leftover 2026-03-09 13:31:01 +01:00
Matej Bačo 58e5e69fec Vuln fixes 2026-03-09 13:01:00 +01:00
Matej Bačo 895ec7fd20 security fix 2026-03-09 12:56:51 +01:00
Matej Bačo d3442d86c1 Rework time travel to CLI task 2026-03-09 12:54:12 +01:00
Matej Bačo c20dfc7063 PR simplification 2026-03-09 11:51:32 +01:00
Matej Bačo 591cc70e76 Merge branch '1.8.x' into revert-11447-revert-10862-feat-1.8.x-new-schema 2026-03-09 11:42:09 +01:00
Jake BarnbyandGitHub 2de28cfcb7 Merge pull request #11483 from appwrite/count-buckets 2026-03-09 17:34:07 +13:00
fogelito c6d476b978 catch error 2026-03-08 16:52:22 +02:00
Luke B. SilverandGitHub 5dca3ad875 Merge pull request #11469 from appwrite/fix/blocks-response-code
fix: blocks response code
2026-03-06 20:46:33 +00:00
loks0nandClaude Sonnet 4.5 b6793dc0b5 Fix remaining blocked account test assertions
Update testBlockedAccount and testSelfBlockedAccount to expect 403
instead of 401 for blocked user responses. These were missed in the
previous test assertion update.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-06 17:43:36 +00:00
loks0nandClaude Sonnet 4.5 a2ad25a00a Update test assertions for blocked user status code
Change expected status code from 401 to 403 for USER_BLOCKED errors
to match the semantic change in error codes.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-06 17:30:51 +00:00
loks0nandClaude Sonnet 4.5 16ad05792d Change blocked user/resource errors from 401 to 403
- Update USER_BLOCKED from 401 to 403 and simplify description
- Update GENERAL_RESOURCE_BLOCKED from 401 to 403

Rationale: 403 Forbidden is the correct HTTP status for authorization
failures where the user is authenticated but not permitted access.
401 Unauthorized is for authentication failures.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-06 17:30:51 +00:00
Luke B. SilverandGitHub e5f0bc2df6 Merge pull request #11457 from appwrite/fix-migration-errors
Fix migration errors
2026-03-06 17:20:39 +00:00
Chirag AggarwalandGitHub 0d3d14d9e1 Merge pull request #11465 from appwrite/optimize-update-document-calls
perf: optimize updateDocument() calls to use sparse documents
2026-03-06 21:17:31 +05:30
Chirag Aggarwal f2826189c6 fix: remove asserting z does not exist in truncated logs 2026-03-06 18:24:04 +05:30
Chirag Aggarwal 8b026d3459 perf: optimize updateDocument() calls to use sparse documents
Optimize updateDocument() calls across the codebase to pass only changed
attributes as sparse Document objects rather than full documents. This is
more efficient because updateDocument() internally performs array_merge().

Changes:
- Updated 58 files to use sparse Document objects
- Added Performance Patterns section to AGENTS.md with optimization guidelines
- Applied pattern to Workers, Functions, Sites, Teams, VCS modules
- Updated app/controllers/api files (account, users, messaging)
- Updated app infrastructure files (realtime, general, init/resources, shared/api)

Exceptions maintained:
- Migration files (need full document updates by design)
- Cases with 6+ attributes (marginal benefit)
- Complex nested relationship logic
2026-03-06 17:05:19 +05:30
Jake Barnby 2306072dda Merge branch 'fix-ce-test-flakes' into 1.8.x 2026-03-06 14:18:37 +13:00
Jake BarnbyandClaude Opus 4.6 91edf82060 fix: database index length assertion and email race in password recovery
- Remove specific index length number from testPatchAttribute assertion
  since the value differs between shared/non-shared table modes (767 vs
  768) and the console API returns the console project's value, not the
  user project's
- Use getLastEmailByAddress in testPasswordRecoveryUrlParams to avoid
  retrieving emails from parallel test classes sharing the same maildev

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 03:41:35 +13:00
Chirag AggarwalandGitHub 3fc50e4d8d Merge pull request #11464 from appwrite/fix/schedule-base-project-access-update
fix: Update in-memory project document after accessedAt update
2026-03-05 20:07:49 +05:30
Chirag Aggarwal 71dff441ed fix: Update in-memory project document after accessedAt update
Previously, the updateProjectAccess method updated the database with the new

accessedAt timestamp but did not update the in-memory project document. This

caused the if statement to constantly evaluate to true on subsequent calls,

triggering unnecessary database updates.
2026-03-05 20:03:42 +05:30
Jake BarnbyandClaude Opus 4.6 65780d75f9 fix: test flakes — correct index length fallback, add retries
- Fix getMaxIndexLength() fallback from 768 to 767 to match MariaDB's
  actual InnoDB index key limit
- Add retry logic to getConsoleVariables() to handle intermittent 401s
- Add retry logic to API key creation in ProjectCustom to prevent
  cascading 401 failures in test methods

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 03:01:23 +13:00
Jake BarnbyandClaude Opus 4.6 ee107d30b3 fix: make Projects tests parallel-safe
- Use getLastEmailByAddress for SMTP tests instead of getLastEmail(2) to
  avoid shared mail server state issues under parallel execution
- Add retry logic to setupProject, setupProjectData, and
  setupScheduleProjectData for intermittent 401 errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:20:09 +13:00
Shmuel FogelandGitHub 499ca71c28 Merge pull request #11459 from appwrite/stop-on-error
Phpunit stop on error
2026-03-05 14:06:04 +02:00
fogelito cfe60dd2a2 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into stop-on-error 2026-03-05 12:32:33 +02:00
Jake BarnbyandClaude Opus 4.6 ab85942276 fix: add retry and session verification to getRoot() for parallel tests
getRoot() now retries up to 5 times with session verification to handle
race conditions when multiple paratest workers initialize simultaneously.
Previously, if account creation or session creation failed under load,
all subsequent test requests would fail with 401.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:19:54 +13:00
Chirag AggarwalandGitHub 0214f22d20 Merge pull request #11461 from appwrite/fix/stale-project-overwrites-oauth
fix: prevent stale project documents from overwriting OAuth providers
2026-03-05 15:46:51 +05:30
Chirag Aggarwal e0269e268f fix: re-read project from DB before updating accessedAt to prevent stale writes
Stale in-memory project documents in ScheduleBase (and request-scoped
copies in api.php/general.php) were overwriting current DB state when
updateProjectAccess triggered. Because Database::updateDocument uses
array_merge with the passed document taking priority, cached projects
missing recent OAuth provider changes would silently disable them.

Now fetches a fresh project document from the DB before writing, so only
accessedAt is updated without clobbering other fields.
2026-03-05 15:44:21 +05:30
fogelito a4844599c6 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into stop-on-error 2026-03-05 12:00:07 +02:00
Shmuel FogelandGitHub ce876adb3a Merge pull request #11460 from appwrite/stats-count-buckets
Stats count image transformations errors
2026-03-05 11:58:40 +02:00
fogelito 59edcf1934 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into stop-on-error 2026-03-05 11:42:16 +02:00
fogelito 5821832da6 count_for_image_transformations_bucket_ 2026-03-05 11:08:45 +02:00
Chirag AggarwalandGitHub d4df2c51de Merge pull request #11442 from appwrite/fix/sdk-beta-versioning
refactor: improve SDK generation task reliability and maintainability
2026-03-05 14:17:25 +05:30
fogelito dbbc57bc37 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into stop-on-error 2026-03-05 10:31:32 +02:00
fogelito d772220414 stopOnError 2026-03-05 10:28:42 +02:00
eldadfux 9d2036024a Refactor migration error handling to provide clearer connection error messages. Updated exception messages for migration sources to guide users on potential credential and network issues. 2026-03-05 08:24:32 +01:00
premtsd-codeandGitHub cf9637288c Merge pull request #11443 from appwrite/bump-migration-1.6.3
chore: bump utopia-php/migration to 1.6.3
2026-03-05 06:59:08 +00:00
Chirag AggarwalandGitHub 5510231b8e Merge branch '1.8.x' into fix/sdk-beta-versioning 2026-03-05 09:52:47 +05:30
Chirag Aggarwal 152de6c584 remove goto 2026-03-05 09:32:07 +05:30
Prem Palanisamy d1ffa5daf3 chore: bump utopia-php/migration to 1.6.3 2026-03-04 22:42:55 +00:00
Chirag Aggarwal 161c5af66f fixes 2026-03-04 19:35:30 +05:30
Chirag Aggarwal c497a68613 fixes 2026-03-04 19:33:41 +05:30
Chirag Aggarwal 50f9c67862 fixes 2026-03-04 19:10:39 +05:30
Chirag Aggarwal 198f9a64a3 refactoring 2026-03-04 19:03:21 +05:30
Matej BačoandGitHub 248b3c8b32 Revert "Revert "Features with schema changes"" 2026-03-04 14:31:27 +01:00
Matej BačoandGitHub 4574385b31 Merge pull request #11447 from appwrite/revert-10862-feat-1.8.x-new-schema
Revert "Features with schema changes"
2026-03-04 14:31:22 +01:00
Matej BačoandGitHub f8b31e7db7 Revert "Features with schema changes" 2026-03-04 14:31:05 +01:00
Matej BačoandGitHub cf16e3e2a3 Merge pull request #10862 from appwrite/feat-1.8.x-new-schema
Features with schema changes
2026-03-04 14:29:48 +01:00
Matej Bačo ff0f132984 Fix build 2026-03-04 14:20:48 +01:00
Chirag Aggarwal 300aaeb251 fix: escape shell arguments in SDK git and gh CLI commands
Replace raw string interpolation with escapeshellarg() for all
arguments passed to exec/shell_exec calls that build git commit,
gh pr create, gh api, and gh release commands. This prevents
shell injection from AI-generated changelog text or any other
dynamically constructed values.
2026-03-04 18:46:49 +05:30
Chirag Aggarwal f989ccde57 fix: add post-parse guard for beta SDK version bumps
Add a programmatic guard after parsing the AI response that rejects
major bumps or versions >= 1.0.0 for beta SDKs. When triggered, the
SDK is skipped with a warning instead of proceeding with an invalid
version.
2026-03-04 18:44:29 +05:30
Matej Bačo 63756ddf1d Merge branch 'main' into feat-1.8.x-new-schema 2026-03-04 14:13:06 +01:00
Matej Bačo 1f5fd919c9 Merge branch '1.8.x' into feat-1.8.x-new-schema 2026-03-04 14:13:01 +01:00
Luke B. SilverandGitHub 718d3377b0 Merge pull request #11445 from appwrite/fix/publisher
fix: stats publisher
2026-03-04 11:43:10 +00:00
loks0n c81029d8aa fix: stats publisher 2026-03-04 11:17:50 +00:00
Shmuel FogelandGitHub 0291f8f943 Merge pull request #11436 from appwrite/executions-collection-index
Executions collection index functionInternalId
2026-03-04 11:14:24 +02:00
fogelito da30142b4d Merge branch '1.8.x' of https://github.com/appwrite/appwrite into executions-collection-index 2026-03-04 10:29:20 +02:00
fogelito f5047afec9 Remove index 2026-03-04 10:28:56 +02:00
Chirag Aggarwal fc88d4b4ab feat: auto-generate commit messages for SDK pushes
Remove the manual commit message prompt. When AI is available and
produces a changelog, use it as the commit message. Otherwise fall
back to a descriptive message based on SDK name and version. A
manually provided --message flag still takes priority.
2026-03-04 13:19:48 +05:30
Chirag Aggarwal df604cb2ef refactor: derive supported SDKs from config instead of hardcoded array
Replace the static supportedSDKS array with a getSupportedSDKs() method
that reads SDK keys from the sdks.php config file. This eliminates the
need to maintain the list in two places.
2026-03-04 13:16:55 +05:30
Chirag Aggarwal a6d58a847d fix: handle beta SDK versioning in DiffCheck prompt
Beta SDKs (version < 1.0.0) should not be bumped to 1.0.0. The prompt
now checks the beta flag from sdk config and instructs the AI to use
minor bumps for both breaking changes and new features, and patch for
bug fixes only. Also adds a rule to wrap code identifiers in backticks
for better changelog rendering.
2026-03-04 13:15:26 +05:30
Jake BarnbyandGitHub fdd3a58d81 Merge pull request #11440 from appwrite/perf/realtime-tests 2026-03-04 14:19:32 +13:00
loks0nandClaude Sonnet 4.5 cf7710b580 perf: reduce WebSocket timeout in realtime tests from 45s to 2s
Realtime E2E tests were taking 24+ minutes due to intentional timeout waits. Many tests verify event filtering by expecting a TimeoutException, and each was waiting 45 seconds.

Changes:
- Reduce default WebSocket timeout from 45s to 2s in RealtimeBase
- Add optional timeout parameter to getWebsocket() methods
- Use longer timeouts (5-10s) for tests that legitimately wait for slow operations (function executions, test channel events)
- Use named parameters for improved readability

Performance impact:
- Before: 24:07 minutes (1,447 seconds)
- After: 1:09 minutes (69 seconds)
- Speedup: ~21x faster

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-03 22:47:28 +00:00
Luke B. SilverandGitHub ccaaffed48 Merge pull request #11316 from appwrite/refactor-log-emitter
refactor: log emitter
2026-03-03 22:04:44 +00:00
loks0nandClaude Sonnet 4.5 c073743989 fix: lint - order imports in general controller
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-03 20:11:34 +00:00
loks0nandClaude Sonnet 4.5 a0854e0591 refactor: make Bus dispatch synchronous
Remove async coroutine wrapper from event dispatch to simplify execution model and improve trace hierarchy. Listeners now execute synchronously in the caller's context, with dependency resolution inlined.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-03 20:06:06 +00:00
loks0nandClaude Opus 4.6 20f248a6ae refactor: consolidate SiteRequestCompleted into RequestCompleted with optional deployment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:36:48 +00:00
loks0nandClaude Opus 4.6 c171e0c3a2 refactor: add bus.event span attribute to listener invocations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:36:48 +00:00
loks0nandClaude Opus 4.6 2081c4c42c refactor: replace bandwidth queueForStatsUsage with Bus events
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:36:48 +00:00
loks0nandClaude Opus 4.6 b45ff6b646 refactor: replace queueForExecutions with Bus event bus
Introduce a generic event bus (Utopia\Bus) with typed events, listener
base class, Span instrumentation, and coroutine dispatch. Replace all
direct queueForExecutions and inline execution usage calls with
ExecutionCompleted event and dedicated listeners (Log, Usage).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:36:48 +00:00
fogelito 5999db295d Message 2026-03-03 15:44:20 +02:00
fogelito b6b44efdab Revert length 2026-03-03 13:58:00 +02:00
fogelito f4125b8859 Remove length 2026-03-03 13:35:23 +02:00
fogelito 1ccd61ece5 functionInternalId 2026-03-03 12:15:21 +02:00
HemachandarandGitHub 598c71fb11 Move VCS events APIs to Modules (#11403)
* Move VCS events APIs to Modules

* trait

* lint

* fix external flow

* fix overriden vars

* feedback
2026-03-03 00:15:09 +05:30
Chirag AggarwalandGitHub 40fc4edb25 Merge pull request #11433 from appwrite/update-sdk-diff-check
chore: improve sdk diff check
2026-03-02 23:43:50 +05:30
Chirag Aggarwal feedd0eb4a improve prompt 2026-03-02 23:30:47 +05:30
Chirag Aggarwal f41c19ed3e improve prompt 2026-03-02 23:26:48 +05:30
Chirag Aggarwal 94bd9661b3 change 2026-03-02 23:24:48 +05:30
Chirag Aggarwal e31843be4b change 2026-03-02 23:20:29 +05:30
Chirag Aggarwal e7424e70aa chore: improve sdk diff check 2026-03-02 23:14:08 +05:30
Jake BarnbyandGitHub 5dadb9a998 Merge pull request #11430 from appwrite/realtime-tablesdb-prefix 2026-03-02 23:52:02 +13:00
HemachandarandGitHub 164c372b21 Per-project permissions for installations (#11423)
* Per-project permissions for installations

* lint
2026-03-02 16:10:58 +05:30
HemachandarandGitHub cb260fa3d8 Add authorized field to VCS repository model (#11420)
* Add authorized field to VCS repository model

* use new API
2026-03-02 15:48:08 +05:30
ArnabChatterjee20k 05a2f56945 updated tests 2026-03-02 14:27:54 +05:30
ArnabChatterjee20k 82a5d138e0 refactor: enhance event handling by ensuring unique events and improving channel merging in Realtime adapter 2026-03-02 14:22:44 +05:30
ArnabChatterjee20k 3d4f37c482 refactor: improve channel handling for Realtime events and update tests for TablesDB compatibility 2026-03-02 14:12:13 +05:30
Jake BarnbyandGitHub b03ad6d523 Merge pull request #11392 from appwrite/migration-resource-enum-fix 2026-03-02 20:09:05 +13:00
Eldad A. FuxandGitHub a5fa09b4ce Merge pull request #11185 from Shivangisharma4/doc-fix-typos-contributing
doc: Fix typos in CONTRIBUTING.md
2026-02-27 20:42:57 +01:00
Prem Palanisamy 5c2b1be581 chore: update sdk-generator to 1.11.3 2026-02-27 09:04:18 +00:00
Prem Palanisamy b3b2fe3ba2 Merge remote-tracking branch 'origin/1.8.x' into migration-resource-enum-fix 2026-02-27 09:01:31 +00:00
Jake BarnbyandGitHub 9667ec2434 Merge pull request #11414 from appwrite/fix-realtime-regions 2026-02-27 08:10:01 +00:00
Jake BarnbyandGitHub 3c4c78d086 Merge branch '1.8.x' into fix-realtime-regions 2026-02-27 06:07:06 +00:00
Jake BarnbyandGitHub 98aab7b34d Merge pull request #11421 from appwrite/realtime-log-improve 2026-02-27 05:55:13 +00:00
Prem Palanisamy 81e828a200 use named arguments and elvis operator for realtime error logging 2026-02-26 15:45:46 +00:00
Prem Palanisamy 218d25ba1b improve realtime error logging with project, user, and auth context 2026-02-26 15:45:46 +00:00
Eldad A. FuxandGitHub 908a74f69d Merge pull request #11413 from appwrite/fix-users-optional-name-error
Fix users optional name error
2026-02-26 16:23:03 +01:00
Jake BarnbyandGitHub 8368a28ff5 Merge pull request #10832 from appwrite/cached-documents-list 2026-02-26 12:55:05 +00:00
Jake BarnbyandGitHub abe21d51cb Merge pull request #11418 from appwrite/copilot/sub-pr-11414-again 2026-02-26 12:54:03 +00:00
copilot-swe-agent[bot]andabnegate 339ecc418d test: remove testConnectionFailureRegionMismatch and mock PATCH route
Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-02-26 12:46:47 +00:00
copilot-swe-agent[bot]andabnegate 5ff7e2af24 fix: revert regions.php, add mock PATCH route for project region in tests
Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-02-26 12:31:05 +00:00
copilot-swe-agent[bot]andabnegate 9aa47c267d test: add region mismatch failure test and fra region to config
Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-02-26 12:14:23 +00:00
Prem Palanisamy 12dca64b60 Merge remote-tracking branch 'origin/1.8.x' into migration-resource-enum-fix 2026-02-26 12:04:16 +00:00
copilot-swe-agent[bot]andabnegate 952f48c72a fix: use AppwriteException, fix console region, add region check test
Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-02-26 11:55:15 +00:00
copilot-swe-agent[bot] 4e80f7eb9a Initial plan 2026-02-26 11:35:32 +00:00
Jake BarnbyandGitHub 9db037722c Merge pull request #11416 from appwrite/copilot/sub-pr-11414 2026-02-26 11:33:44 +00:00
eldadfux ded05ebea2 Merge remote-tracking branch 'origin/1.8.x' into fix-users-optional-name-error 2026-02-26 12:09:12 +01:00
Eldad A. FuxandGitHub a71f3555ae Merge pull request #11412 from appwrite/fix-better-error-for-functions
Fix better error for functions
2026-02-26 12:07:31 +01:00
copilot-swe-agent[bot]andabnegate 4500349fda fix: use 'default' fallback for _APP_REGION in realtime.php
Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-02-26 11:00:09 +00:00
copilot-swe-agent[bot] 611495ec28 Initial plan 2026-02-26 10:50:52 +00:00
Jake Barnby 347436d370 Add region check 2026-02-26 23:37:26 +13:00
Jake BarnbyandGitHub dd925e335b Merge pull request #11411 from appwrite/bump-pools 2026-02-26 10:35:46 +00:00
Chirag AggarwalandGitHub 0e8b5f1d04 Merge pull request #11400 from appwrite/fix-execution-timeout-status
fix: show timed-out executions as failed across API endpoints
2026-02-26 15:43:00 +05:30
Jake BarnbyandClaude Opus 4.6 8dfaf4a9a9 fix: remove unused collectionUpdatedAt from cache key
The schema hash already captures all collection changes, making the
$updatedAt field redundant (and unreliable since Utopia preserves it).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 23:12:38 +13:00
Jake BarnbyandClaude Opus 4.6 d5b2f97252 fix: use md5 hash of attributes/indexes instead of counts in cache key
Counts alone miss delete/re-add scenarios where the count stays the same
but the schema is different. MD5 of the full attribute and index arrays
captures any schema change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 23:07:24 +13:00
eldadfux 47a81d165e Fix for user name to accept null and cast as string 2026-02-26 10:30:41 +01:00
eldadfux d7c8b9d43a Better error message when a function fail instead of general_unknown 2026-02-26 10:24:46 +01:00
ArnabChatterjee20k 7562946434 bump pools 2026-02-26 14:29:31 +05:30
Jake BarnbyandClaude Opus 4.6 4d19a5e855 fix: include attribute and index counts in document list cache key
The collection $updatedAt is preserved by Utopia Database's updateDocument()
when the value is already set, so it doesn't change when attributes/indexes
are created. Include attribute and index counts in the cache key to ensure
schema changes properly invalidate the document list cache.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:10:57 +13:00
Jake BarnbyandClaude Opus 4.6 1c85789a6e fix: scope cache tests to setup document IDs to avoid count mismatch
The testListDocumentsWithCache and testListDocumentsCacheBustedByAttributeChange
tests were failing because testCreateDocument (which runs earlier) adds 3 more
documents to the same movies collection, resulting in 6 docs instead of expected 3.
Additionally, the cache bust test was getting a cache 'hit' instead of 'miss' on
its first request due to sharing the same cache key as the previous test.

Fix by filtering all cache test queries to the 3 known setup document IDs, and
using distinct select/order queries in each test to avoid cache key collisions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 20:34:46 +13:00
eldadfux 79d219bf50 fix cache duplication 2026-02-26 07:50:51 +01:00
eldadfux 9b2143a2a5 Fixed cache duplication 2026-02-26 07:44:35 +01:00
Jake BarnbyandClaude Opus 4.6 b174a50b2a fix: address coderabbit review comments
- Fix indentation alignment in cache hit detection condition
- Add total count assertions for cached and non-cached responses in test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 19:00:25 +13:00
Jake Barnby 3e0ee30428 Add tests for collection bust 2026-02-26 18:31:52 +13:00
Jake Barnby 0e7c50ef71 Include collection update time in key 2026-02-26 18:26:30 +13:00
Jake Barnby b8ee9fbe2c Update lock 2026-02-26 17:11:51 +13:00
Jake Barnby d49dea9790 Merge remote-tracking branch 'origin/1.8.x' into cached-documents-list
# Conflicts:
#	composer.lock
#	tests/e2e/Services/Databases/DatabasesBase.php
2026-02-26 17:02:33 +13:00
Chirag Aggarwal 8891890601 fix: show timed-out executions as failed across API endpoints
Executions that time out can remain stuck in waiting or processing status
in the database. This mirrors the frontend workaround from console#2788
across the relevant API endpoints for both functions and sites.

Changes:
- GET execution/log: override status to failed in response if elapsed time
  since creation exceeds the resource timeout
- LIST executions/logs: same in-response override; when caller filters by
  failed, expands DB query with OR to also fetch waiting/processing entries
  created before the timeout threshold so they appear in results; skips
  in-response override when caller explicitly requests a non-failed status
  to avoid contradicting the filter
- DELETE execution: allows deletion of timed-out executions that are still
  stored as waiting/processing by treating them as failed for the status guard

All changes are in-memory only — the database records are not modified.
Includes a note to remove once a proper DB-level fix is applied.
2026-02-26 09:22:33 +05:30
premtsd-codeandGitHub db85004b12 Merge branch '1.8.x' into migration-resource-enum-fix 2026-02-25 23:16:52 +00:00
eldadfux b7e4f78c56 fix 2026-02-26 00:00:10 +01:00
eldadfux 159da8ba31 Fix 500 errors where we don't report duplication properly 2026-02-25 23:58:22 +01:00
Prem Palanisamy 88ce622e4a chore: update utopia-php/migration to 1.6.2 2026-02-25 12:06:12 +00:00
premtsd-codeandGitHub c041e57b8f Merge branch '1.8.x' into migration-resource-enum-fix 2026-02-25 11:57:48 +00:00
Prem Palanisamy 4a0f00f7db chore: update utopia-migration to 134efdc 2026-02-25 11:35:07 +00:00
premtsd-codeandGitHub de21cab22a Merge branch '1.8.x' into migration-resource-enum-fix 2026-02-25 10:28:28 +00:00
Prem Palanisamy f8929e1f2e fix: use utopia-migration dev branch with deployment download fixes 2026-02-25 10:25:05 +00:00
Prem Palanisamy 754ddbef3c fix: remove deployment from MigrationReport model 2026-02-25 06:47:08 +00:00
Prem Palanisamy ed93b77faa fix: remove strict WhiteList for Appwrite migration endpoints 2026-02-25 06:44:42 +00:00
Prem Palanisamy 5a6cfdbe4c fix: revert composer to 1.8.x and remove child resource counts from MigrationReport 2026-02-25 06:42:46 +00:00
Prem Palanisamy 6b90d00cc2 fix: update utopia-migration lock to include strtolower removal 2026-02-25 00:24:16 +00:00
Prem Palanisamy 3ba5e12f89 fix: add deployment and environmentVariable to MigrationReport model 2026-02-24 23:19:21 +00:00
Prem Palanisamy dc441c3973 fix: enable strict mode for Appwrite migration resource WhiteList 2026-02-24 22:44:57 +00:00
Prem Palanisamy 9745df4d37 fix: update utopia-migration to camelCase resource types 2026-02-24 22:30:32 +00:00
Prem Palanisamy 715adf62ca fix: update utopia-migration to snake_case resource types 2026-02-24 15:54:42 +00:00
Prem Palanisamy 90ab70dfc6 fix: use dev branch of utopia-php/migration for site camelCase fix 2026-02-24 15:54:42 +00:00
Prem Palanisamy b0c5185ebd feat: add site migration 2026-02-24 15:54:42 +00:00
Prem Palanisamy d58873ef14 added site migration E2E test 2026-02-24 15:54:42 +00:00
Prem Palanisamy 105c885c3f added site migration 2026-02-24 15:54:42 +00:00
Prem Palanisamy ad625029e8 feat: add migration resource enums to SDK specification 2026-02-24 15:54:42 +00:00
Jake Barnby aeaa184aec Use auth roles 2026-02-24 22:12:20 +13:00
Jake BarnbyandGitHub 9dd707cd1b Merge branch '1.8.x' into cached-documents-list 2026-02-24 09:04:32 +00:00
10e2edac90 Update CONTRIBUTING.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-24 14:03:11 +05:30
Shivangisharma4 340081a67d doc: Fix typos in CONTRIBUTING.md 2026-01-24 13:58:20 +05:30
Jake BarnbyandGitHub 9f0ca2bc53 Update src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php 2026-01-20 20:42:36 +13:00
Jake BarnbyandGitHub 605cf0c985 Update src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php 2026-01-20 18:21:14 +13:00
shimon 7e1838dda6 typo 2026-01-19 14:02:14 +02:00
shimon 2016e9bc2f Update XList parameters: remove 'cache' and adjust 'ttl' default value to 0, reflecting changes in caching logic across tests. 2026-01-19 13:14:26 +02:00
shimon b3d1410ca9 Refactor XList parameters: rename 'total' to 'includeTotal' for clarity and update related logic in action method to maintain functionality. 2026-01-18 14:27:57 +02:00
shimon a2d56a47cd merge with 1.8x 2026-01-18 11:13:45 +02:00
shimon 316bbfe43d Merge branch '1.8.x', commit '3b5351afb6473e559c945846f0bf7e928b636d83' of github.com:appwrite/appwrite into cached-documents-list
# Conflicts:
#	composer.lock
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php
2026-01-18 11:13:08 +02:00
Shimon NewmanandGitHub 3b5351afb6 Merge branch '1.8.x' into cached-documents-list 2026-01-06 13:03:18 +02:00
shimon ecc4052bdc Clarify TTL parameter description in XList class for cached responses to enhance documentation accuracy. 2026-01-06 12:58:58 +02:00
shimon fc84012977 Inject user dependency into XList class for enhanced functionality and improved caching accuracy. 2026-01-06 12:56:18 +02:00
shimon c882afbd2c Enhance XList class by injecting user dependency and updating cache key format to include user ID for improved caching accuracy. 2026-01-06 12:52:36 +02:00
Matej Bačo 9c3b78e18f PR review fixes 2026-01-05 13:55:12 +01:00
Matej Bačo 4365e1fee5 FIx default team labels 2026-01-05 11:33:45 +01:00
Matej Bačo d19e7c169e Merge branch '1.8.x' into feat-1.8.x-new-schema 2026-01-05 10:32:18 +01:00
Matej Bačo 226bb6b830 AI core review changes 2026-01-05 10:32:03 +01:00
Matej Bačo da5b68490c Add team labels schema 2026-01-05 10:31:58 +01:00
Matej Bačo 4dc5df3923 Fix test lockfile 2026-01-05 09:27:38 +01:00
Matej Bačo bc7bdf040f Fix vuln in test file 2026-01-02 18:42:33 +01:00
Matej Bačo f8968f69f0 Upgrade test for osv vulns 2026-01-02 18:25:30 +01:00
Matej BačoandGitHub 030e89ac7d Merge branch '1.8.x' into feat-1.8.x-new-schema 2026-01-02 18:08:37 +01:00
Matej BačoandGitHub 37330522a4 Merge pull request #11066 from appwrite/feat-specs-dual-reading
Feat: Add dual read for specs
2026-01-02 18:08:14 +01:00
Matej Bačo 807bb0039a Fix formatting 2026-01-02 16:34:20 +01:00
Matej Bačo c4c260f3cf Add dual read for specs 2026-01-02 16:34:05 +01:00
Matej Bačo 87d6c73b7b Fix failing tests 2026-01-02 12:58:53 +01:00
Matej Bačo a71875eedf Merge branch '1.8.x' into feat-1.8.x-new-schema 2026-01-02 12:47:44 +01:00
Matej Bačo 51e03353dd Merge branch '1.8.x' into feat-1.8.x-new-schema 2025-12-31 15:13:35 +01:00
Matej BačoandGitHub 175b0d92f2 Merge pull request #10959 from appwrite/feat-auto-delete-depoyments
Feat: Auto-delete deployments
2025-12-16 13:30:44 +01:00
Matej Bačo c88a77a31c AI suggestion fixes 2025-12-16 12:53:06 +01:00
Matej Bačo a3a5e05b5c bug fix 2025-12-15 20:13:45 +01:00
Matej Bačo bd2db5e249 AI review fixes 2025-12-15 19:45:57 +01:00
Matej Bačo 6cbc79026f Finish site tests for auto deletion 2025-12-15 18:55:34 +01:00
Matej Bačo 0ec755911e Finish maintenance test 2025-12-15 18:44:27 +01:00
Matej Bačo be626ad0fc Add deployment retention to sites and functions 2025-12-15 17:33:10 +01:00
shimon 305697a5d3 Update composer dependencies and refactor cache handling in XList class for improved performance 2025-12-09 11:29:09 +02:00
shimon d954681132 matcing documents xlist params order 2025-12-08 15:41:13 +02:00
shimon 872345366a linter 2025-12-08 13:43:05 +02:00
shimon 5b1aec0f0b Enhance XList functionality by adding cache and TTL parameters for improved query performance and response management. 2025-12-08 13:34:09 +02:00
shimon 98d063132c composer.lock 2025-12-08 09:57:49 +02:00
shimon fcb3699e99 Merge branch 'refactor-auth-single-instance' of github.com:appwrite/appwrite into cached-documents-list
# Conflicts:
#	composer.json
#	composer.lock
2025-12-08 09:56:48 +02:00
shimon 39658a7ee1 fixing comments 2025-12-08 09:55:15 +02:00
Matej BačoandGitHub 914723c53a Merge branch '1.8.x' into feat-1.8.x-new-schema 2025-11-24 19:10:44 +01:00
Matej BačoandGitHub db965f8144 Merge pull request #10842 from appwrite/feat-custom-start-commands
Feat: Custom start commands
2025-11-24 16:01:01 +01:00
Matej Bačo d99a52741d Merge branch 'feat-1.8.x-new-schema' into feat-custom-start-commands 2025-11-24 15:41:52 +01:00
Matej BačoandGitHub 96f79fac0e Merge pull request #10849 from appwrite/feat-separate-specifications
Separate specifications
2025-11-24 15:39:02 +01:00
Matej Bačo 856c1d685e Remove functions refference to start command 2025-11-24 15:19:41 +01:00
Matej Bačo 5817aea2ee Remove todo 2025-11-24 14:30:45 +01:00
shimon 6213df9fb3 Update composer.lock for sdk-generator version 1.5.8 and adjust caching TTL in XList to 30 seconds; modify related tests for consistency in cache behavior. 2025-11-23 12:46:16 +02:00
Matej Bačo 4bc624ea5c Fix response filters order 2025-11-20 18:05:32 +01:00
Matej Bačo 7444f5cf60 leftover fix 2025-11-20 17:50:15 +01:00
Matej Bačo cbb96f8b82 AI review fixes 2025-11-20 17:40:56 +01:00
Matej Bačo 63f6840d1b Finish implementation, update tests 2025-11-20 17:39:19 +01:00
Matej Bačo 62173b8f61 WIP: Separate specifications 2025-11-20 13:56:54 +01:00
Matej Bačo 7f3c0c9c03 AI review fixes 2025-11-19 16:38:47 +01:00
Matej Bačo f907f76eb5 Custom start command implementation 2025-11-19 15:27:54 +01:00
shimon 30df7a43d5 Refactor caching logic in XList to use JSON encoding for query serialization and update cache header keys to lowercase in tests 2025-11-18 20:11:02 +02:00
shimon 8fc9fdb7dc cached documents list 2025-11-18 16:37:52 +02:00
shimon 77905e241f cached documents list 2025-11-18 16:26:56 +02:00
144 changed files with 16342 additions and 1975 deletions
+30
View File
@@ -47,6 +47,36 @@ Examples:
'resourceType' => 'deployments'
```
## Performance Patterns
### Document Update Optimization
When updating documents, always pass only the changed attributes as a sparse `Document` rather than the full document. This is more efficient because `updateDocument()` internally performs `array_merge($old, $new)`.
**Correct Pattern:**
```php
// Good: Pass only changed attributes directly
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
'name' => $name,
'email' => $email,
]));
```
**Incorrect Pattern:**
```php
$user->setAttribute('name', $name);
$user->setAttribute('email', $email);
// Bad: Passing full document is inefficient
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
```
**Exceptions:**
- Migration files (need full document updates by design)
- Cases already using `array_merge()` with `getArrayCopy()`
- Updates where almost all attributes of the document change at once (sparse update provides little benefit compared to passing the full document)
- Complex nested relationship logic where full document state is required
## Security Considerations
### Critical Security Practices
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+7 -7
View File
@@ -111,7 +111,7 @@ $ git push origin [name_of_your_new_branch]
## Setup From Source
To set up a working **development environment**, just fork the project git repository and install the backend and frontend dependencies using the proper package manager and create run the docker-compose stack.
To set up a working **development environment**, just fork the project git repository and install the backend and frontend dependencies using the proper package manager and run the docker-compose stack.
> If you just want to install Appwrite for day-to-day use and not as a contributor, you can reference the [installation guide](https://github.com/appwrite/appwrite#installation), the [getting started guide](https://appwrite.io/docs/quick-starts), or the main [README](README.md) file.
@@ -173,12 +173,12 @@ Learn more at our [Technology Stack](#technology-stack) section.
- [MVVM](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) - Appwrite console architecture
##### Container Namespace Conventions
To keep our services easy to understand within Docker we follow a naming convention for all our containers depending on it's intended use.
To keep our services easy to understand within Docker we follow a naming convention for all our containers depending on its intended use.
`appwrite-worker-X` - Workers (`src/Appwrite/Platform/Workers/*`)
`appwrite-task-X` - Tasks (`src/Appwrite/Platform/Tasks/*`)
Other containes should be named the same as their service, for example `redis` should just be called `redis`.
Other containers should be named the same as their service, for example `redis` should just be called `redis`.
##### Security
@@ -189,7 +189,7 @@ Other containes should be named the same as their service, for example `redis` s
## Modules
As Appwrite grows, we noticed approach of having all service endpoints in `app/controllers/api/[service].php` is not maintainable. Not only it creates massive files, it also doesnt contain all product's features such as workers or tasks. While there might still be some occurances of those controller files, we avoid it in all new development, and gradually migrate existing controllers to **HTTP modules**.
As Appwrite grows, we noticed approach of having all service endpoints in `app/controllers/api/[service].php` is not maintainable. Not only it creates massive files, it also doesn't contain all product's features such as workers or tasks. While there might still be some occurrences of those controller files, we avoid it in all new development, and gradually migrate existing controllers to **HTTP modules**.
### HTTP Endpoints
@@ -204,7 +204,7 @@ Tips and tricks:
1. If endpoint doesn't have resource, use service name as resource name too
> Example: `Modules/Sites/Http/Sites/Get.php`
2. If there are multiple resources, use then all in folder structure
2. If there are multiple resources, use them all in folder structure
> Example: `Modules/Sites/Http/Deployments/Builds/Create.php`
3. Action can only be `Get`, `Create`, `Update`, `Delete` or `XList`
@@ -395,7 +395,7 @@ These are the current metrics we collect usage stats for:
> Note: The curly brackets in the metric name represents a template and is replaced with a value when the metric is processed.
Metrics are collected within 3 scopes Daily, monthly, an infinity. Adding new usage metric in order to aggregate usage stats is very simple, but very much dependent on where do you want to collect
Metrics are collected within 3 scopes Daily, monthly, and infinity. Adding new usage metric in order to aggregate usage stats is very simple, but very much dependent on where do you want to collect
statistics ,via API or via background worker. For both cases you will need to add a `const` variable in `app/init.php` under the usage metrics list using the naming convention `METRIC_<RESOURCE_NAME>` as shown below.
```php
@@ -661,7 +661,7 @@ docker compose exec redis redis-cli FLUSHALL
## Using preview domains locally
Appwrite Functions are automatically given a domain you can visit to execute the function. This domain has format `[SOMETHING].functions.localhost` unless you changed `_APP_DOMAIN_FUNCTIONS` environment variable. This default value works great when running Appwrite locally, but it can be impossible to use preview domains with Cloud woekspaces such as Gitpod or GitHub Codespaces.
Appwrite Functions are automatically given a domain you can visit to execute the function. This domain has format `[SOMETHING].functions.localhost` unless you changed `_APP_DOMAIN_FUNCTIONS` environment variable. This default value works great when running Appwrite locally, but it can be impossible to use preview domains with Cloud workspaces such as Gitpod or GitHub Codespaces.
To use preview domains on Cloud workspaces, you can visit hostname provided by them, and supply function's preview domain as URL parameter:
+1
View File
@@ -70,6 +70,7 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/sdks && \
chmod +x /usr/local/bin/specs && \
chmod +x /usr/local/bin/ssl && \
chmod +x /usr/local/bin/time-travel && \
chmod +x /usr/local/bin/screenshot && \
chmod +x /usr/local/bin/test && \
chmod +x /usr/local/bin/upgrade && \
+4
View File
@@ -318,6 +318,10 @@ $setResource('logError', function (Registry $register) {
$setResource('executor', fn () => new Executor(), []);
$setResource('bus', function (Registry $register) use ($cli) {
return $register->get('bus')->setResolver(fn (string $name) => $cli->getResource($name));
}, ['register']);
$setResource('telemetry', fn () => new NoTelemetry(), []);
$cli
+1 -29
View File
@@ -777,6 +777,7 @@ return [
'filters' => [],
],
[
// At the moment, always empty (no runtime supports it yet)
'array' => false,
'$id' => ID::custom('startCommand'),
'type' => Database::VAR_STRING,
@@ -787,17 +788,6 @@ return [
'default' => null,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('specification'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => false,
'required' => false,
'default' => APP_COMPUTE_SPECIFICATION_DEFAULT,
'filters' => [],
],
[
'array' => false,
'$id' => ID::custom('buildSpecification'),
@@ -1255,17 +1245,6 @@ return [
'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' => [],
],
[
'array' => false,
'$id' => ID::custom('buildSpecification'),
@@ -2190,13 +2169,6 @@ return [
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_function_internal_id'),
'type' => Database::INDEX_KEY,
'attributes' => ['resourceInternalId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_resourceType'),
'type' => Database::INDEX_KEY,
+1 -1
View File
@@ -26,7 +26,7 @@ $console = [
'hostname' => 'localhost',
], // Current host is added on app init
],
'region' => 'fra',
'region' => System::getEnv('_APP_REGION', 'default'),
'legalName' => '',
'legalCountry' => '',
'legalState' => '',
+13 -3
View File
@@ -27,7 +27,7 @@ return [
Exception::GENERAL_RESOURCE_BLOCKED => [
'name' => Exception::GENERAL_RESOURCE_BLOCKED,
'description' => 'Access to this resource is blocked.',
'code' => 401,
'code' => 403,
],
Exception::GENERAL_UNKNOWN_ORIGIN => [
'name' => Exception::GENERAL_UNKNOWN_ORIGIN,
@@ -168,8 +168,8 @@ return [
],
Exception::USER_BLOCKED => [
'name' => Exception::USER_BLOCKED,
'description' => 'The current user has been blocked. You can unblock the user by making a request to the User API\'s "Update User Status" endpoint or in the Appwrite Console\'s Auth section.',
'code' => 401,
'description' => 'The current user has been blocked.',
'code' => 403,
],
Exception::USER_INVALID_TOKEN => [
'name' => Exception::USER_INVALID_TOKEN,
@@ -630,6 +630,11 @@ return [
'description' => 'Site with the requested ID could not be found.',
'code' => 404,
],
Exception::SITE_ALREADY_EXISTS => [
'name' => Exception::SITE_ALREADY_EXISTS,
'description' => 'Site with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::SITE_TEMPLATE_NOT_FOUND => [
'name' => Exception::SITE_TEMPLATE_NOT_FOUND,
'description' => 'Site Template with the requested ID could not be found.',
@@ -1291,6 +1296,11 @@ return [
'description' => 'Message with the requested ID could not be found.',
'code' => 404,
],
Exception::MESSAGE_ALREADY_EXISTS => [
'name' => Exception::MESSAGE_ALREADY_EXISTS,
'description' => 'Message with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::MESSAGE_MISSING_TARGET => [
'name' => Exception::MESSAGE_MISSING_TARGET,
'description' => 'Message with the requested ID has no recipients (topics or users or targets).',
+1 -1
View File
@@ -231,7 +231,7 @@ return [
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
'beta' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_SDK_PLATFORM_CONSOLE,
+1 -1
View File
@@ -188,7 +188,7 @@ return [
'name' => 'VCS',
'subtitle' => 'The VCS service allows you to interact with providers like GitHub, GitLab etc.',
'description' => '',
'controller' => 'api/vcs.php',
'controller' => '', // Uses modules
'sdk' => false,
'docs' => false,
'docsUrl' => '',
+37 -15
View File
@@ -288,7 +288,10 @@ $createSession = function (string $userId, string $secret, Request $request, Res
}
try {
$dbForProject->updateDocument('users', $user->getId(), $user);
$dbForProject->updateDocument('users', $user->getId(), new Document([
'emailVerification' => $user->getAttribute('emailVerification'),
'phoneVerification' => $user->getAttribute('phoneVerification'),
]));
} catch (\Throwable $th) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB');
}
@@ -1032,7 +1035,11 @@ Http::post('/v1/account/sessions/email')
->setAttribute('password', $proofForPasswordUpdated->hash($password))
->setAttribute('hash', $proofForPasswordUpdated->getHash()->getName())
->setAttribute('hashOptions', $proofForPasswordUpdated->getHash()->getOptions());
$dbForProject->updateDocument('users', $user->getId(), $user);
$dbForProject->updateDocument('users', $user->getId(), new Document([
'password' => $user->getAttribute('password'),
'hash' => $user->getAttribute('hash'),
'hashOptions' => $user->getAttribute('hashOptions'),
]));
}
$dbForProject->purgeCachedDocument('users', $user->getId());
@@ -1822,7 +1829,11 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $accessTokenExpiry));
$dbForProject->updateDocument('identities', $identity->getId(), $identity);
$dbForProject->updateDocument('identities', $identity->getId(), new Document([
'providerAccessToken' => $identity->getAttribute('providerAccessToken'),
'providerRefreshToken' => $identity->getAttribute('providerRefreshToken'),
'providerAccessTokenExpiry' => $identity->getAttribute('providerAccessTokenExpiry'),
]));
}
if (empty($user->getAttribute('email'))) {
@@ -1960,7 +1971,10 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->setAttribute('sessionId', $session->getId())
->setAttribute('sessionInternalId', $session->getSequence());
$dbForProject->updateDocument('targets', $target->getId(), $target);
$dbForProject->updateDocument('targets', $target->getId(), new Document([
'sessionId' => $target->getAttribute('sessionId'),
'sessionInternalId' => $target->getAttribute('sessionInternalId'),
]));
}
}
@@ -3145,7 +3159,9 @@ Http::patch('/v1/account/name')
$user->setAttribute('name', $name);
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
'name' => $user->getAttribute('name'),
]));
$queueForEvents->setParam('userId', $user->getId());
@@ -3798,13 +3814,15 @@ Http::put('/v1/account/recovery')
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile
->setAttribute('password', $newPassword)
->setAttribute('passwordHistory', $history)
->setAttribute('passwordUpdate', DateTime::now())
->setAttribute('hash', $proofForPassword->getHash()->getName())
->setAttribute('hashOptions', $proofForPassword->getHash()->getOptions())
->setAttribute('emailVerification', true));
$profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(
[
'password' => $newPassword,
'passwordHistory' => $history,
'passwordUpdate' => DateTime::now(),
'hash' => $proofForPassword->getHash()->getName(),
'hashOptions' => $proofForPassword->getHash()->getOptions(),
'emailVerification' => true]
));
$user->setAttributes($profile->getArrayCopy());
@@ -4126,7 +4144,7 @@ Http::put('/v1/account/verifications/email')
$authorization->addRole(Role::user($profile->getId())->toString());
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true));
$profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(['emailVerification' => true]));
$user->setAttributes($profile->getArrayCopy());
@@ -4342,7 +4360,7 @@ Http::put('/v1/account/verifications/phone')
$authorization->addRole(Role::user($profile->getId())->toString());
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true));
$profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(['phoneVerification' => true]));
$user->setAttributes($profile->getArrayCopy());
@@ -4500,7 +4518,11 @@ Http::put('/v1/account/targets/:targetId/push')
$target->setAttribute('name', "{$device['deviceBrand']} {$device['deviceModel']}");
$target = $dbForProject->updateDocument('targets', $target->getId(), $target);
$target = $dbForProject->updateDocument('targets', $target->getId(), new Document([
'identifier' => $target->getAttribute('identifier'),
'expired' => $target->getAttribute('expired'),
'name' => $target->getAttribute('name'),
]));
$dbForProject->purgeCachedDocument('users', $user->getId());
+21 -6
View File
@@ -3251,7 +3251,7 @@ Http::post('/v1/messaging/messages/email')
}
}
$message = $dbForProject->createDocument('messages', new Document([
$message = new Document([
'$id' => $messageId,
'providerType' => MESSAGE_TYPE_EMAIL,
'topics' => $topics,
@@ -3267,7 +3267,12 @@ Http::post('/v1/messaging/messages/email')
'attachments' => $attachments,
],
'status' => $status,
]));
]);
try {
$message = $dbForProject->createDocument('messages', $message);
} catch (DuplicateException) {
throw new Exception(Exception::MESSAGE_ALREADY_EXISTS);
}
switch ($status) {
case MessageStatus::PROCESSING:
@@ -3400,7 +3405,7 @@ Http::post('/v1/messaging/messages/sms')
}
}
$message = $dbForProject->createDocument('messages', new Document([
$message = new Document([
'$id' => $messageId,
'providerType' => MESSAGE_TYPE_SMS,
'topics' => $topics,
@@ -3410,7 +3415,12 @@ Http::post('/v1/messaging/messages/sms')
'content' => $content,
],
'status' => $status,
]));
]);
try {
$message = $dbForProject->createDocument('messages', $message);
} catch (DuplicateException) {
throw new Exception(Exception::MESSAGE_ALREADY_EXISTS);
}
switch ($status) {
case MessageStatus::PROCESSING:
@@ -3620,7 +3630,7 @@ Http::post('/v1/messaging/messages/push')
$pushData['priority'] = $priority;
}
$message = $dbForProject->createDocument('messages', new Document([
$message = new Document([
'$id' => $messageId,
'providerType' => MESSAGE_TYPE_PUSH,
'topics' => $topics,
@@ -3629,7 +3639,12 @@ Http::post('/v1/messaging/messages/push')
'scheduledAt' => $scheduledAt,
'data' => $pushData,
'status' => $status,
]));
]);
try {
$message = $dbForProject->createDocument('messages', $message);
} catch (DuplicateException) {
throw new Exception(Exception::MESSAGE_ALREADY_EXISTS);
}
switch ($status) {
case MessageStatus::PROCESSING:
+20 -49
View File
@@ -717,22 +717,14 @@ Http::get('/v1/migrations/appwrite/report')
->inject('project')
->inject('user')
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response) {
$appwrite = new Appwrite($projectID, $endpoint, $key);
try {
$appwrite = new Appwrite($projectID, $endpoint, $key);
$report = $appwrite->report($resources);
} catch (\Throwable $e) {
switch ($e->getCode()) {
case 401:
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
case 429:
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
case 500:
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
throw new Exception(
Exception::MIGRATION_PROVIDER_ERROR,
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
);
}
$response
@@ -771,21 +763,14 @@ Http::get('/v1/migrations/firebase/report')
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Invalid Service Account JSON');
}
$firebase = new Firebase($serviceAccount);
try {
$firebase = new Firebase($serviceAccount);
$report = $firebase->report($resources);
} catch (\Throwable $e) {
switch ($e->getCode()) {
case 401:
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
case 429:
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
case 500:
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
throw new Exception(
Exception::MIGRATION_PROVIDER_ERROR,
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
);
}
$response
@@ -820,21 +805,14 @@ Http::get('/v1/migrations/supabase/report')
->inject('response')
->inject('dbForProject')
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response) {
$supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port);
try {
$supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port);
$report = $supabase->report($resources);
} catch (\Throwable $e) {
switch ($e->getCode()) {
case 401:
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
case 429:
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
case 500:
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
throw new Exception(
Exception::MIGRATION_PROVIDER_ERROR,
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
);
}
$response
@@ -869,21 +847,14 @@ Http::get('/v1/migrations/nhost/report')
->param('port', 5432, new Integer(true), 'Source\'s Database Port.', true)
->inject('response')
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response) {
$nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port);
try {
$nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port);
$report = $nhost->report($resources);
} catch (\Throwable $e) {
switch ($e->getCode()) {
case 401:
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
case 429:
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
case 500:
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
throw new Exception(
Exception::MIGRATION_PROVIDER_ERROR,
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
);
}
$response
+54 -25
View File
@@ -73,8 +73,9 @@ use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
/** TODO: Remove function when we move to using utopia/platform */
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, ?string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
{
$name = $name ?? '';
$plaintextPassword = $password;
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
@@ -255,7 +256,7 @@ Http::post('/v1/users')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$plaintext = new Plaintext();
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
@@ -291,7 +292,7 @@ Http::post('/v1/users/bcrypt')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$bcrypt = new Bcrypt();
$bcrypt->setCost(8); // Default cost
@@ -329,7 +330,7 @@ Http::post('/v1/users/md5')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$md5 = new MD5();
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
@@ -366,7 +367,7 @@ Http::post('/v1/users/argon2')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$argon2 = new Argon2();
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
@@ -404,7 +405,7 @@ Http::post('/v1/users/sha')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->action(function (string $userId, string $email, string $password, string $passwordVersion, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$sha = new Sha();
if (!empty($passwordVersion)) {
$sha->setVersion($passwordVersion);
@@ -444,7 +445,7 @@ Http::post('/v1/users/phpass')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$phpass = new PHPass();
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
@@ -486,7 +487,7 @@ Http::post('/v1/users/scrypt')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$scrypt = new Scrypt();
$scrypt
->setSalt($passwordSalt)
@@ -532,7 +533,7 @@ Http::post('/v1/users/scrypt-modified')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
$scryptModified = new ScryptModified();
$scryptModified
->setSalt($passwordSalt)
@@ -1160,7 +1161,7 @@ Http::patch('/v1/users/:userId/status')
throw new Exception(Exception::USER_NOT_FOUND);
}
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', (bool) $status));
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['status' => (bool) $status]));
$queueForEvents
->setParam('userId', $user->getId());
@@ -1203,7 +1204,7 @@ Http::put('/v1/users/:userId/labels')
$user->setAttribute('labels', (array) \array_values(\array_unique($labels)));
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['labels' => $user->getAttribute('labels')]));
$queueForEvents
->setParam('userId', $user->getId());
@@ -1244,7 +1245,7 @@ Http::patch('/v1/users/:userId/verification/phone')
throw new Exception(Exception::USER_NOT_FOUND);
}
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('phoneVerification', $phoneVerification));
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['phoneVerification' => $phoneVerification]));
$queueForEvents
->setParam('userId', $user->getId());
@@ -1288,7 +1289,7 @@ Http::patch('/v1/users/:userId/name')
$user->setAttribute('name', $name);
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['name' => $user->getAttribute('name')]));
$queueForEvents->setParam('userId', $user->getId());
@@ -1343,7 +1344,10 @@ Http::patch('/v1/users/:userId/password')
->setAttribute('password', '')
->setAttribute('passwordUpdate', DateTime::now());
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
'password' => $user->getAttribute('password'),
'passwordUpdate' => $user->getAttribute('passwordUpdate'),
]));
$queueForEvents->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
}
@@ -1376,7 +1380,13 @@ Http::patch('/v1/users/:userId/password')
->setAttribute('hash', $hasher->getName())
->setAttribute('hashOptions', $hasher->getOptions());
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
'password' => $user->getAttribute('password'),
'passwordHistory' => $user->getAttribute('passwordHistory'),
'passwordUpdate' => $user->getAttribute('passwordUpdate'),
'hash' => $user->getAttribute('hash'),
'hashOptions' => $user->getAttribute('hashOptions'),
]));
$sessions = $user->getAttribute('sessions', []);
$invalidate = $project->getAttribute('auths', default: [])['invalidateSessions'] ?? false;
@@ -1468,7 +1478,15 @@ Http::patch('/v1/users/:userId/email')
;
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
'email' => $user->getAttribute('email'),
'emailVerification' => $user->getAttribute('emailVerification'),
'emailCanonical' => $user->getAttribute('emailCanonical'),
'emailIsCanonical' => $user->getAttribute('emailIsCanonical'),
'emailIsCorporate' => $user->getAttribute('emailIsCorporate'),
'emailIsDisposable' => $user->getAttribute('emailIsDisposable'),
'emailIsFree' => $user->getAttribute('emailIsFree'),
]));
/**
* @var Document $oldTarget
*/
@@ -1476,7 +1494,8 @@ Http::patch('/v1/users/:userId/email')
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
if (\strlen($email) !== 0) {
$dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email));
$dbForProject->updateDocument('targets', $oldTarget->getId(), new Document(['identifier' => $email]));
$oldTarget->setAttribute('identifier', $email);
} else {
$dbForProject->deleteDocument('targets', $oldTarget->getId());
}
@@ -1557,7 +1576,10 @@ Http::patch('/v1/users/:userId/phone')
}
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
'phone' => $user->getAttribute('phone'),
'phoneVerification' => $user->getAttribute('phoneVerification'),
]));
/**
* @var Document $oldTarget
*/
@@ -1565,7 +1587,8 @@ Http::patch('/v1/users/:userId/phone')
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
if (\strlen($number) !== 0) {
$dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $number));
$dbForProject->updateDocument('targets', $oldTarget->getId(), new Document(['identifier' => $number]));
$oldTarget->setAttribute('identifier', $number);
} else {
$dbForProject->deleteDocument('targets', $oldTarget->getId());
}
@@ -1629,7 +1652,7 @@ Http::patch('/v1/users/:userId/verification')
throw new Exception(Exception::USER_NOT_FOUND);
}
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification));
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['emailVerification' => $emailVerification]));
$queueForEvents->setParam('userId', $user->getId());
@@ -1667,7 +1690,7 @@ Http::patch('/v1/users/:userId/prefs')
throw new Exception(Exception::USER_NOT_FOUND);
}
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs));
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['prefs' => $prefs]));
$queueForEvents
->setParam('userId', $user->getId());
@@ -1767,7 +1790,13 @@ Http::patch('/v1/users/:userId/targets/:targetId')
$target->setAttribute('name', $name);
}
$target = $dbForProject->updateDocument('targets', $target->getId(), $target);
$target = $dbForProject->updateDocument('targets', $target->getId(), new Document([
'identifier' => $target->getAttribute('identifier'),
'expired' => $target->getAttribute('expired'),
'providerId' => $target->getAttribute('providerId'),
'providerInternalId' => $target->getAttribute('providerInternalId'),
'name' => $target->getAttribute('name'),
]));
$dbForProject->purgeCachedDocument('users', $user->getId());
$queueForEvents
@@ -1835,7 +1864,7 @@ Http::patch('/v1/users/:userId/mfa')
$user->setAttribute('mfa', $mfa);
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['mfa' => $user->getAttribute('mfa')]));
$queueForEvents->setParam('userId', $user->getId());
@@ -2023,7 +2052,7 @@ Http::patch('/v1/users/:userId/mfa/recovery-codes')
$mfaRecoveryCodes = Type::generateBackupCodes();
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
$dbForProject->updateDocument('users', $user->getId(), $user);
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
$queueForEvents->setParam('userId', $user->getId());
@@ -2095,7 +2124,7 @@ Http::put('/v1/users/:userId/mfa/recovery-codes')
$mfaRecoveryCodes = Type::generateBackupCodes();
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
$dbForProject->updateDocument('users', $user->getId(), $user);
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
$queueForEvents->setParam('userId', $user->getId());
-705
View File
@@ -1,705 +0,0 @@
<?php
use Appwrite\Event\Build;
use Appwrite\Extend\Exception;
use Appwrite\Filter\BranchDomain as BranchDomainFilter;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\Comment;
use Utopia\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Http\Http;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\RepositoryNotFound;
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForPlatform, Authorization $authorization, Build $queueForBuilds, callable $getProjectDB, Request $request, array $platform) {
$errors = [];
foreach ($repositories as $repository) {
try {
$resourceType = $repository->getAttribute('resourceType');
if ($resourceType !== "function" && $resourceType !== "site") {
continue;
}
$projectId = $repository->getAttribute('projectId');
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND, 'Repository references non-existent project');
}
$dbForProject = $getProjectDB($project);
$resourceCollection = $resourceType === "function" ? 'functions' : 'sites';
$resourceId = $repository->getAttribute('resourceId');
$resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
$resourceInternalId = $resource->getSequence();
$deploymentId = ID::unique();
$repositoryId = $repository->getId();
$repositoryInternalId = $repository->getSequence();
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
$installationId = $repository->getAttribute('installationId');
$installationInternalId = $repository->getAttribute('installationInternalId');
$productionBranch = $resource->getAttribute('providerBranch');
$activate = false;
if ($providerBranch == $productionBranch && $external === false) {
$activate = true;
}
$owner = $github->getOwnerName($providerInstallationId) ?? '';
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$isAuthorized = !$external;
if (!$isAuthorized && !empty($providerPullRequestId)) {
if (\in_array($providerPullRequestId, $repository->getAttribute('providerPullRequestIds', []))) {
$isAuthorized = true;
}
}
$commentStatus = $isAuthorized ? 'waiting' : 'failed';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = $platform['consoleHostname'] ?? '';
$authorizeUrl = $protocol . '://' . $hostname . "/console/git/authorize-contributor?projectId={$projectId}&installationId={$installationId}&repositoryId={$repositoryId}&providerPullRequestId={$providerPullRequestId}";
$action = $isAuthorized ? ['type' => 'logs'] : ['type' => 'authorize', 'url' => $authorizeUrl];
$latestCommentId = '';
if (!empty($providerPullRequestId) && $resource->getAttribute('providerSilentMode', false) === false) {
$latestComment = $authorization->skip(fn () => $dbForPlatform->findOne('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerPullRequestId', [$providerPullRequestId]),
Query::orderDesc('$createdAt'),
]));
if (!$latestComment->isEmpty()) {
$latestCommentId = $latestComment->getAttribute('providerCommentId', '');
$retries = 0;
$lockAcquired = false;
while ($retries < 9) {
$retries++;
try {
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
'$id' => $latestCommentId
]));
$lockAcquired = true;
break;
} catch (\Throwable $err) {
if ($retries >= 9) {
Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage());
}
\sleep(1);
}
}
if ($lockAcquired) {
// Wrap in try/finally to ensure lock file gets deleted
try {
$comment = new Comment($platform);
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
} else {
$comment = new Comment($platform);
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
$latestCommentId = \strval($github->createComment($owner, $repositoryName, $providerPullRequestId, $comment->generateComment()));
if (!empty($latestCommentId)) {
$teamId = $project->getAttribute('teamId', '');
$latestComment = $authorization->skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'installationInternalId' => $installationInternalId,
'installationId' => $installationId,
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'providerRepositoryId' => $providerRepositoryId,
'providerBranch' => $providerBranch,
'providerPullRequestId' => $providerPullRequestId,
'providerCommentId' => $latestCommentId
])));
}
}
} elseif (!empty($providerBranch)) {
$latestComments = $authorization->skip(fn () => $dbForPlatform->find('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerBranch', [$providerBranch]),
Query::orderDesc('$createdAt'),
]));
foreach ($latestComments as $comment) {
$latestCommentId = $comment->getAttribute('providerCommentId', '');
$retries = 0;
$lockAcquired = false;
while ($retries < 9) {
$retries++;
try {
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
'$id' => $latestCommentId
]));
$lockAcquired = true;
break;
} catch (\Throwable $err) {
if ($retries >= 9) {
Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage());
}
\sleep(1);
}
}
if ($lockAcquired) {
// Wrap in try/finally to ensure lock file gets deleted
try {
$comment = new Comment($platform);
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
}
}
if (!$isAuthorized) {
$resourceName = $resource->getAttribute('name');
$projectName = $project->getAttribute('name');
$name = "{$resourceName} ({$projectName})";
$message = 'Authorization required for external contributor.';
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$owner = $github->getOwnerName($providerInstallationId);
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'failure', $message, $authorizeUrl, $name);
continue;
}
if ($external) {
$pullRequestResponse = $github->getPullRequest($owner, $repositoryName, $providerPullRequestId);
$providerRepositoryName = $pullRequestResponse['head']['repo']['owner']['login'];
$providerRepositoryOwner = $pullRequestResponse['head']['repo']['name'];
}
$commands = [];
if (!empty($resource->getAttribute('installCommand', ''))) {
$commands[] = $resource->getAttribute('installCommand', '');
}
if (!empty($resource->getAttribute('buildCommand', ''))) {
$commands[] = $resource->getAttribute('buildCommand', '');
}
if (!empty($resource->getAttribute('commands', ''))) {
$commands[] = $resource->getAttribute('commands', '');
}
$deployment = $authorization->skip(fn () => $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceId' => $resourceId,
'resourceInternalId' => $resourceInternalId,
'resourceType' => $resourceCollection,
'entrypoint' => $resource->getAttribute('entrypoint', ''),
'buildCommands' => \implode(' && ', $commands),
'startCommand' => $resource->getAttribute('startCommand', ''),
'buildOutput' => $resource->getAttribute('outputDirectory', ''),
'adapter' => $resource->getAttribute('adapter', ''),
'fallbackFile' => $resource->getAttribute('fallbackFile', ''),
'type' => 'vcs',
'installationId' => $installationId,
'installationInternalId' => $installationInternalId,
'providerRepositoryId' => $providerRepositoryId,
'repositoryId' => $repositoryId,
'repositoryInternalId' => $repositoryInternalId,
'providerBranchUrl' => $providerBranchUrl,
'providerRepositoryName' => $providerRepositoryName,
'providerRepositoryOwner' => $providerRepositoryOwner,
'providerRepositoryUrl' => $providerRepositoryUrl,
'providerCommitHash' => $providerCommitHash,
'providerCommitAuthorUrl' => $providerCommitAuthorUrl,
'providerCommitAuthor' => $providerCommitAuthor,
'providerCommitMessage' => mb_strimwidth($providerCommitMessage, 0, 255, '...'),
'providerCommitUrl' => $providerCommitUrl,
'providerCommentId' => \strval($latestCommentId),
'providerBranch' => $providerBranch,
'activate' => $activate,
])));
$resource = $resource
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$authorization->skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource));
if ($resource->getCollection() === 'sites') {
$projectId = $project->getId();
// Deployment preview
$sitesDomain = $platform['sitesDomain'];
$domain = ID::unique() . "." . $sitesDomain;
$ruleId = md5($domain);
$previewRuleId = $ruleId;
$authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getSequence(),
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $resourceId,
'deploymentResourceInternalId' => $resourceInternalId,
'deploymentVcsProviderBranch' => $providerBranch,
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
// VCS branch preview
if (!empty($providerBranch)) {
$domain = (new BranchDomainFilter())->apply([
'branch' => $providerBranch,
'resourceId' => $resource->getId(),
'projectId' => $project->getId(),
'sitesDomain' => $sitesDomain,
]);
$ruleId = md5($domain);
try {
$authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getSequence(),
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $resourceId,
'deploymentResourceInternalId' => $resourceInternalId,
'deploymentVcsProviderBranch' => $providerBranch,
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
} catch (Duplicate $err) {
// Ignore, rule already exists; will be updated by builds worker
}
}
// VCS commit preview
if (!empty($providerCommitHash)) {
$domain = "commit-" . substr($providerCommitHash, 0, 16) . ".{$sitesDomain}";
$ruleId = md5($domain);
try {
$authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getSequence(),
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $resourceId,
'deploymentResourceInternalId' => $resourceInternalId,
'deploymentVcsProviderBranch' => $providerBranch,
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
} catch (Duplicate $err) {
// Ignore, rule already exists; will be updated by builds worker
}
}
}
if ($resource->getCollection() === 'sites' && !empty($latestCommentId) && !empty($previewRuleId)) {
$retries = 0;
$lockAcquired = false;
while ($retries < 9) {
$retries++;
try {
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
'$id' => $latestCommentId
]));
$lockAcquired = true;
break;
} catch (\Throwable $err) {
if ($retries >= 9) {
Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage());
}
\sleep(1);
}
}
if ($lockAcquired) {
// Wrap in try/finally to ensure lock file gets deleted
try {
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
if (!empty($previewUrl)) {
$comment = new Comment($platform);
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, $previewUrl);
$github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
}
} finally {
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
}
if (!empty($providerCommitHash) && $resource->getAttribute('providerSilentMode', false) === false) {
$resourceName = $resource->getAttribute('name');
$projectName = $project->getAttribute('name');
$region = $project->getAttribute('region', 'default');
$name = "{$resourceName} ({$projectName})";
$message = 'Starting...';
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$owner = $github->getOwnerName($providerInstallationId);
$providerTargetUrl = $protocol . '://' . $hostname . "/console/project-$region-$projectId/$resourceCollection/$resourceType-$resourceId";
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'pending', $message, $providerTargetUrl, $name);
}
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($resource)
->setDeployment($deployment)
->setProject($project); // set the project because it won't be set for git deployments
$queueForBuilds->trigger(); // must trigger here so that we create a build for each function/site
//TODO: Add event?
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
$queueForBuilds->reset(); // prevent shutdown hook from triggering again
if (!empty($errors)) {
throw new Exception(Exception::GENERAL_UNKNOWN, \implode("\n", $errors));
}
};
Http::post('/v1/vcs/github/events')
->desc('Create event')
->groups(['api', 'vcs'])
->label('scope', 'public')
->inject('gitHub')
->inject('request')
->inject('response')
->inject('dbForPlatform')
->inject('authorization')
->inject('getProjectDB')
->inject('queueForBuilds')
->inject('platform')
->action(
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
$payload = $request->getRawPayload();
$signatureRemote = $request->getHeader('x-hub-signature-256', '');
$signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', '');
$valid = empty($signatureRemote) ? true : $github->validateWebhookEvent($payload, $signatureRemote, $signatureLocal);
if (!$valid) {
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, "Invalid webhook payload signature. Please make sure the webhook secret has same value in your GitHub app and in the _APP_VCS_GITHUB_WEBHOOK_SECRET environment variable");
}
$event = $request->getHeader('x-github-event', '');
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$parsedPayload = $github->getEvent($event, $payload);
if ($event == $github::EVENT_PUSH) {
$providerBranchCreated = $parsedPayload["branchCreated"] ?? false;
$providerBranchDeleted = $parsedPayload["branchDeleted"] ?? false;
$providerBranch = $parsedPayload["branch"] ?? '';
$providerBranchUrl = $parsedPayload["branchUrl"] ?? '';
$providerRepositoryId = $parsedPayload["repositoryId"] ?? '';
$providerRepositoryName = $parsedPayload["repositoryName"] ?? '';
$providerInstallationId = $parsedPayload["installationId"] ?? '';
$providerRepositoryUrl = $parsedPayload["repositoryUrl"] ?? '';
$providerCommitHash = $parsedPayload["commitHash"] ?? '';
$providerRepositoryOwner = $parsedPayload["owner"] ?? '';
$providerCommitAuthorName = $parsedPayload["headCommitAuthorName"] ?? '';
$providerCommitAuthorEmail = $parsedPayload["headCommitAuthorEmail"] ?? '';
$providerCommitAuthorUrl = $parsedPayload["authorUrl"] ?? '';
$providerCommitMessage = $parsedPayload["headCommitMessage"] ?? '';
$providerCommitUrl = $parsedPayload["headCommitUrl"] ?? '';
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
//find resourceId from relevant resources table
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::limit(100),
]));
// create new deployment only on push (not committed by us) and not when branch is created or deleted
if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) {
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
}
} elseif ($event == $github::EVENT_INSTALLATION) {
if ($parsedPayload["action"] == "deleted") {
// TODO: Use worker for this job instead (update function/site as well)
$providerInstallationId = $parsedPayload["installationId"];
$installations = $dbForPlatform->find('installations', [
Query::equal('providerInstallationId', [$providerInstallationId]),
Query::limit(1000)
]);
foreach ($installations as $installation) {
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('installationInternalId', [$installation->getSequence()]),
Query::limit(1000)
]));
foreach ($repositories as $repository) {
$authorization->skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId()));
}
$authorization->skip(fn () => $dbForPlatform->deleteDocument('installations', $installation->getId()));
}
}
} elseif ($event == $github::EVENT_PULL_REQUEST) {
if ($parsedPayload["action"] == "opened" || $parsedPayload["action"] == "reopened" || $parsedPayload["action"] == "synchronize") {
$providerBranch = $parsedPayload["branch"] ?? '';
$providerBranchUrl = $parsedPayload["branchUrl"] ?? '';
$providerRepositoryId = $parsedPayload["repositoryId"] ?? '';
$providerRepositoryName = $parsedPayload["repositoryName"] ?? '';
$providerInstallationId = $parsedPayload["installationId"] ?? '';
$providerRepositoryUrl = $parsedPayload["repositoryUrl"] ?? '';
$providerPullRequestId = $parsedPayload["pullRequestNumber"] ?? '';
$providerCommitHash = $parsedPayload["commitHash"] ?? '';
$providerRepositoryOwner = $parsedPayload["owner"] ?? '';
$external = $parsedPayload["external"] ?? true;
$providerCommitUrl = $parsedPayload["headCommitUrl"] ?? '';
$providerCommitAuthorUrl = $parsedPayload["authorUrl"] ?? '';
// Ignore sync for non-external. We handle it in push webhook
if (!$external && $parsedPayload["action"] == "synchronize") {
return $response->json($parsedPayload);
}
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$commitDetails = $github->getCommit($providerRepositoryOwner, $providerRepositoryName, $providerCommitHash);
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
$providerCommitMessage = $commitDetails["commitMessage"] ?? '';
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
} elseif ($parsedPayload["action"] == "closed") {
// Allowed external contributions cleanup
$providerRepositoryId = $parsedPayload["repositoryId"] ?? '';
$providerPullRequestId = $parsedPayload["pullRequestNumber"] ?? '';
$external = $parsedPayload["external"] ?? true;
if ($external) {
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
foreach ($repositories as $repository) {
$providerPullRequestIds = $repository->getAttribute('providerPullRequestIds', []);
if (\in_array($providerPullRequestId, $providerPullRequestIds)) {
$providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]);
$repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds);
$repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
}
}
}
}
}
$response->json($parsedPayload);
}
);
Http::patch('/v1/vcs/github/installations/:installationId/repositories/:repositoryId')
->desc('Update external deployment (authorize)')
->groups(['api', 'vcs'])
->label('scope', 'vcs.write')
->label('sdk', new Method(
namespace: 'vcs',
group: 'repositories',
name: 'updateExternalDeployments',
description: '/docs/references/vcs/update-external-deployments.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
]
))
->param('installationId', '', new Text(256), 'Installation Id')
->param('repositoryId', '', new Text(256), 'VCS Repository Id')
->param('providerPullRequestId', '', new Text(256), 'GitHub Pull Request Id')
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->inject('getProjectDB')
->inject('queueForBuilds')
->inject('platform')
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$repository = $authorization->skip(fn () => $dbForPlatform->findOne('repositories', [
Query::equal('$id', [$repositoryId]),
Query::equal('projectInternalId', [$project->getSequence()])
]));
if ($repository->isEmpty()) {
throw new Exception(Exception::REPOSITORY_NOT_FOUND);
}
if (\in_array($providerPullRequestId, $repository->getAttribute('providerPullRequestIds', []))) {
throw new Exception(Exception::PROVIDER_CONTRIBUTION_CONFLICT);
}
$providerPullRequestIds = \array_unique(\array_merge($repository->getAttribute('providerPullRequestIds', []), [$providerPullRequestId]));
$repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds);
// TODO: Delete from array when PR is closed
$repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$providerInstallationId = $installation->getAttribute('providerInstallationId');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$repositories = [$repository];
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
$owner = $github->getOwnerName($providerInstallationId);
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$pullRequestResponse = $github->getPullRequest($owner, $repositoryName, $providerPullRequestId);
$providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? '';
$providerCommitHash = $pullRequestResponse['head']['sha'] ?? '';
$providerBranchUrl = $pullRequestResponse['head']['repo']['html_url'] ?? '';
$providerRepositoryName = $pullRequestResponse['head']['repo']['name'] ?? '';
$providerRepositoryUrl = $pullRequestResponse['head']['repo']['html_url'] ?? '';
$providerRepositoryOwner = $pullRequestResponse['head']['repo']['owner']['login'] ?? '';
$providerCommitAuthor = $pullRequestResponse['head']['user']['login'] ?? '';
$providerCommitAuthorUrl = $pullRequestResponse['head']['user']['html_url'] ?? '';
$providerCommitMessage = $pullRequestResponse['title'] ?? '';
$providerCommitUrl = $pullRequestResponse['html_url'] ?? '';
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, '', '', '', '', $providerCommitHash, '', '', '', '', $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
$response->noContent();
});
+66 -119
View File
@@ -5,11 +5,11 @@ require_once __DIR__ . '/../init.php';
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Event\Execution;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Cors;
use Appwrite\Platform\Appwrite;
@@ -30,11 +30,14 @@ use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
use Appwrite\Utopia\Response\Filters\V18 as ResponseV18;
use Appwrite\Utopia\Response\Filters\V19 as ResponseV19;
use Appwrite\Utopia\Response\Filters\V20 as ResponseV20;
use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
use Appwrite\Utopia\View;
use Executor\Executor;
use MaxMind\Db\Reader;
use Swoole\Http\Request as SwooleRequest;
use Swoole\Table;
use Utopia\Bus\Bus;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Database;
@@ -62,7 +65,7 @@ Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
{
$host = $request->getHostname() ?? '';
if (!empty($previewHostname)) {
@@ -131,8 +134,9 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'accessedAt' => DateTime::now()
])));
}
/**
@@ -320,7 +324,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
};
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
$spec = Config::getParam('specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$spec = Config::getParam('specifications')[$resource->getAttribute('runtimeSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$runtime = match ($type) {
'function' => $runtimes[$resource->getAttribute('runtime')] ?? null,
@@ -550,6 +554,10 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
}
if (!empty($deployment->getAttribute('startCommand', ''))) {
$startCommand = 'cd /usr/local/server/src/function/ && ' . $deployment->getAttribute('startCommand', '');
}
$runtimeEntrypoint = match ($version) {
'v2' => '',
default => "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$startCommand\"",
@@ -706,10 +714,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
} finally {
if ($type === 'function' || $type === 'site') {
$queueForExecutions
->setExecution($execution)
->setProject($project)
->trigger();
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
spec: $spec,
resource: $resource->getArrayCopy(),
));
}
}
@@ -754,70 +764,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
->setStatusCode($execution['responseStatusCode'] ?? 200)
->send($body);
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
if (!empty($apiKey) && !empty($apiKey->getDisabledMetrics())) {
foreach ($apiKey->getDisabledMetrics() as $key) {
$queueForStatsUsage->disableMetric($key);
}
}
$metricTypeExecutions = str_replace(['{resourceType}'], [$deployment->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_EXECUTIONS);
$metricTypeIdExecutions = str_replace(['{resourceType}', '{resourceInternalId}'], [$deployment->getAttribute('resourceType'), $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS);
$metricTypeExecutionsCompute = str_replace(['{resourceType}'], [$deployment->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE);
$metricTypeIdExecutionsCompute = str_replace(['{resourceType}', '{resourceInternalId}'], [$deployment->getAttribute('resourceType'), $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE);
$metricTypeExecutionsMbSeconds = str_replace(['{resourceType}'], [$deployment->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS);
$metricTypeIdExecutionsMBSeconds = str_replace(['{resourceType}', '{resourceInternalId}'], [$deployment->getAttribute('resourceType'), $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS);
if ($deployment->getAttribute('resourceType') === 'sites') {
$queueForStatsUsage
->disableMetric(METRIC_NETWORK_REQUESTS)
->disableMetric(METRIC_NETWORK_INBOUND)
->disableMetric(METRIC_NETWORK_OUTBOUND);
if ($resource->getAttribute('adapter') !== 'ssr') {
$queueForStatsUsage
->disableMetric(METRIC_EXECUTIONS)
->disableMetric(METRIC_EXECUTIONS_COMPUTE)
->disableMetric(METRIC_EXECUTIONS_MB_SECONDS)
->disableMetric($metricTypeExecutions)
->disableMetric($metricTypeIdExecutions)
->disableMetric($metricTypeExecutionsCompute)
->disableMetric($metricTypeIdExecutionsCompute)
->disableMetric($metricTypeExecutionsMbSeconds)
->disableMetric($metricTypeIdExecutionsMBSeconds);
}
$queueForStatsUsage
->addMetric(METRIC_SITES_REQUESTS, 1)
->addMetric(METRIC_SITES_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_SITES_OUTBOUND, $response->getSize())
->addMetric(str_replace('{siteInternalId}', $resource->getSequence(), METRIC_SITES_ID_REQUESTS), 1)
->addMetric(str_replace('{siteInternalId}', $resource->getSequence(), METRIC_SITES_ID_INBOUND), $request->getSize() + $fileSize)
->addMetric(str_replace('{siteInternalId}', $resource->getSequence(), METRIC_SITES_ID_OUTBOUND), $response->getSize())
;
}
$compute = (int)($execution->getAttribute('duration') * 1000);
$mbSeconds = (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT));
$queueForStatsUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric($metricTypeExecutions, 1)
->addMetric($metricTypeIdExecutions, 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, $compute) // per project
->addMetric($metricTypeExecutionsCompute, $compute) // per function
->addMetric($metricTypeIdExecutionsCompute, $compute) // per function
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, $mbSeconds)
->addMetric($metricTypeExecutionsMbSeconds, $mbSeconds)
->addMetric($metricTypeIdExecutionsMBSeconds, $mbSeconds)
->setProject($project)
->trigger();
$bus->dispatch(new RequestCompleted(
project: $project->getArrayCopy(),
request: $request,
response: $response,
deployment: $deployment->getArrayCopy(),
));
/* cleanup */
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
@@ -881,9 +833,8 @@ Http::init()
->inject('locale')
->inject('localeCodes')
->inject('geodb')
->inject('queueForStatsUsage')
->inject('queueForEvents')
->inject('queueForExecutions')
->inject('bus')
->inject('executor')
->inject('platform')
->inject('isResourceBlocked')
@@ -894,7 +845,7 @@ Http::init()
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Execution $queueForExecutions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
@@ -902,7 +853,7 @@ Http::init()
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -988,17 +939,23 @@ Http::init()
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
if (version_compare($responseFormat, '1.4.0', '<')) {
$response->addFilter(new ResponseV16());
if (version_compare($responseFormat, '1.9.0', '<')) {
$response->addFilter(new ResponseV21());
}
if (version_compare($responseFormat, '1.5.0', '<')) {
$response->addFilter(new ResponseV17());
if (version_compare($responseFormat, '1.8.0', '<')) {
$response->addFilter(new ResponseV20());
}
if (version_compare($responseFormat, '1.7.0', '<')) {
$response->addFilter(new ResponseV19());
}
if (version_compare($responseFormat, '1.6.0', '<')) {
$response->addFilter(new ResponseV18());
}
if (version_compare($responseFormat, '1.7.0', '<')) {
$response->addFilter(new ResponseV19());
if (version_compare($responseFormat, '1.5.0', '<')) {
$response->addFilter(new ResponseV17());
}
if (version_compare($responseFormat, '1.4.0', '<')) {
$response->addFilter(new ResponseV16());
}
if (version_compare($responseFormat, APP_VERSION_STABLE, '>')) {
$warnings[] = "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is " . APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks";
@@ -1178,8 +1135,7 @@ Http::options()
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForExecutions')
->inject('bus')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
@@ -1192,14 +1148,14 @@ Http::options()
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1215,12 +1171,11 @@ Http::options()
/** OPTIONS requests in utopia do not execute shutdown handlers, as a result we need to track the OPTIONS requests explicitly
* @see https://github.com/utopia-php/http/blob/0.33.16/src/App.php#L825-L855
*/
$queueForStatsUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize())
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
->setProject($project)
->trigger();
$bus->dispatch(new RequestCompleted(
project: $project->getArrayCopy(),
request: $request,
response: $response,
));
});
Http::error()
@@ -1231,10 +1186,10 @@ Http::error()
->inject('project')
->inject('logger')
->inject('log')
->inject('queueForStatsUsage')
->inject('bus')
->inject('devKey')
->inject('authorization')
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage, Document $devKey, Authorization $authorization) {
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$route = $utopia->getRoute();
$class = \get_class($error);
@@ -1307,21 +1262,12 @@ Http::error()
*/
if (!$publish && $project->getId() !== 'console') {
if (!DBUser::isPrivileged($authorization->getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$queueForStatsUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
$bus->dispatch(new RequestCompleted(
project: $project->getArrayCopy(),
request: $request,
response: $response,
));
}
$queueForStatsUsage
->setProject($project)
->trigger();
}
if ($logger && $publish) {
@@ -1568,8 +1514,7 @@ Http::get('/robots.txt')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForExecutions')
->inject('bus')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
@@ -1579,13 +1524,13 @@ Http::get('/robots.txt')
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1603,8 +1548,7 @@ Http::get('/humans.txt')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForExecutions')
->inject('bus')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
@@ -1614,13 +1558,13 @@ Http::get('/humans.txt')
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1718,7 +1662,10 @@ Http::get('/v1/ping')
->setAttribute('pingedAt', $pingedAt);
$authorization->skip(function () use ($dbForPlatform, $project) {
$dbForPlatform->updateDocument('projects', $project->getId(), $project);
$dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'pingCount' => $project->getAttribute('pingCount'),
'pingedAt' => $project->getAttribute('pingedAt')
]));
});
$queueForEvents
+40 -25
View File
@@ -2,6 +2,7 @@
use Appwrite\Auth\Key;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Database as EventDatabase;
@@ -21,12 +22,14 @@ use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Abuse;
use Utopia\Bus\Bus;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
@@ -211,6 +214,8 @@ Http::init()
}
if (!$dbKey) {
\var_dump($apiKey);
\var_dump($request->getHeader('x-appwrite-key', ''));
throw new Exception(Exception::USER_UNAUTHORIZED);
}
@@ -357,8 +362,9 @@ Http::init()
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'accessedAt' => DateTime::now()
])));
}
}
@@ -368,9 +374,13 @@ Http::init()
$user->setAttribute('accessedAt', DateTime::now());
if ($project->getId() !== 'console' && APP_MODE_ADMIN !== $mode) {
$dbForProject->updateDocument('users', $user->getId(), $user);
$dbForProject->updateDocument('users', $user->getId(), new Document([
'accessedAt' => $user->getAttribute('accessedAt')
]));
} else {
$authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), $user));
$authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), new Document([
'accessedAt' => $user->getAttribute('accessedAt')
])));
}
}
}
@@ -646,7 +656,9 @@ Http::init()
$transformedAt = $file->getAttribute('transformedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
$file->setAttribute('transformedAt', DateTime::now());
$authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file));
$authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), new Document([
'transformedAt' => $file->getAttribute('transformedAt')
])));
}
}
}
@@ -745,7 +757,8 @@ Http::shutdown()
->inject('authorization')
->inject('timelimit')
->inject('eventProcessor')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor) use ($parseLabel) {
->inject('bus')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -929,17 +942,24 @@ Http::shutdown()
$accessedAt = $cacheLog->getAttribute('accessedAt', 0);
$now = DateTime::now();
if ($cacheLog->isEmpty()) {
$authorization->skip(fn () => $dbForProject->createDocument('cache', new Document([
'$id' => $key,
'resource' => $resource,
'resourceType' => $resourceType,
'mimeType' => $response->getContentType(),
'accessedAt' => $now,
'signature' => $signature,
])));
try {
$authorization->skip(fn () => $dbForProject->createDocument('cache', new Document([
'$id' => $key,
'resource' => $resource,
'resourceType' => $resourceType,
'mimeType' => $response->getContentType(),
'accessedAt' => $now,
'signature' => $signature,
])));
} catch (DuplicateException) {
// Race condition: another concurrent request already created the cache document
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
}
} elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
$cacheLog->setAttribute('accessedAt', $now);
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), new Document([
'accessedAt' => $cacheLog->getAttribute('accessedAt')
])));
// Overwrite the file every APP_CACHE_UPDATE seconds to update the file modified time that is used in the TTL checks in cache->load()
$cache->save($key, $data['payload']);
}
@@ -952,16 +972,11 @@ Http::shutdown()
if ($project->getId() !== 'console') {
if (!User::isPrivileged($authorization->getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$queueForStatsUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
$bus->dispatch(new RequestCompleted(
project: $project->getArrayCopy(),
request: $request,
response: $response,
));
}
$queueForStatsUsage
+4
View File
@@ -188,6 +188,10 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
Console::success('Reload completed...');
});
Http::setResource('bus', function ($register, $utopia) {
return $register->get('bus')->setResolver(fn (string $name) => $utopia->getResource($name));
}, ['register', 'utopia']);
include __DIR__ . '/controllers/general.php';
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
+1
View File
@@ -93,6 +93,7 @@ const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=
const APP_COMPUTE_CPUS_DEFAULT = 0.5;
const APP_COMPUTE_MEMORY_DEFAULT = 512;
const APP_COMPUTE_SPECIFICATION_DEFAULT = Specification::S_1VCPU_512MB;
const APP_COMPUTE_DEPLOYMENT_MAX_RETENTION = 100 * 365; // 100 years
const APP_SDK_PLATFORM_SERVER = 'server';
const APP_SDK_PLATFORM_CLIENT = 'client';
const APP_SDK_PLATFORM_CONSOLE = 'console';
+8
View File
@@ -449,3 +449,11 @@ $register->set('promiseAdapter', function () {
$register->set('hooks', function () {
return new Hooks();
});
$listeners = require __DIR__ . '/../listeners.php';
$register->set('bus', function () use ($listeners) {
$bus = new \Utopia\Bus\Bus();
foreach ($listeners as $listener) {
$bus->subscribe($listener);
}
return $bus;
});
+10 -7
View File
@@ -10,7 +10,6 @@ use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Execution;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
@@ -160,9 +159,6 @@ Http::setResource('queueForAudits', function (Publisher $publisher) {
Http::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
Http::setResource('queueForExecutions', function (Publisher $publisher) {
return new Execution($publisher);
}, ['publisher']);
Http::setResource('eventProcessor', function () {
return new EventProcessor();
}, []);
@@ -1239,7 +1235,9 @@ Http::setResource('devKey', function (Request $request, Document $project, array
$accessedAt = $key->getAttribute('accessedAt', 0);
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
$key->setAttribute('accessedAt', DatabaseDateTime::now());
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
'accessedAt' => $key->getAttribute('accessedAt')
])));
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
@@ -1256,7 +1254,10 @@ Http::setResource('devKey', function (Request $request, Document $project, array
/** Update access time as well */
$key->setAttribute('accessedAt', DatabaseDateTime::now());
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
'sdks' => $key->getAttribute('sdks'),
'accessedAt' => $key->getAttribute('accessedAt')
])));
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
}
@@ -1413,7 +1414,9 @@ Http::setResource('resourceToken', function ($project, $dbForProject, $request,
$accessedAt = $token->getAttribute('accessedAt', 0);
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) {
$token->setAttribute('accessedAt', DatabaseDateTime::now());
$authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token));
$authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), new Document([
'accessedAt' => $token->getAttribute('accessedAt')
])));
}
return new Document([
+9
View File
@@ -0,0 +1,9 @@
<?php
use Appwrite\Bus\Listeners\Log;
use Appwrite\Bus\Listeners\Usage;
return [
new Log(),
new Usage(),
];
+73 -42
View File
@@ -248,48 +248,62 @@ $adapter
$server = new Server($adapter);
$logError = function (Throwable $error, string $action) use ($register) {
$logger = $register->get('realtimeLogger');
// Allows overriding
if (!function_exists('logError')) {
function logError(Throwable $error, string $action, array $tags = [], ?Document $project = null, ?Document $user = null, ?Authorization $authorization = null): void
{
global $register;
if ($logger && !$error instanceof Exception) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$logger = $register->get('realtimeLogger');
$log = new Log();
$log->setNamespace("realtime");
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
if ($logger && !$error instanceof Exception) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', get_class($error));
$log = new Log();
$log->setNamespace("realtime");
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', get_class($error));
$log->addTag('projectId', $project?->getId() ?: 'n/a');
$log->addTag('userId', $user?->getId() ?: 'n/a');
$log->setAction($action);
foreach ($tags as $key => $value) {
$log->addTag($key, $value ?: 'n/a');
}
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('detailedTrace', $error->getTrace());
$log->addExtra('roles', $authorization?->getRoles() ?? []);
try {
$responseCode = $logger->addLog($log);
Console::info('Error log pushed with status code: ' . $responseCode);
} catch (Throwable $th) {
Console::error('Error pushing log: ' . $th->getMessage());
$log->setAction($action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
try {
$responseCode = $logger->addLog($log);
Console::info('Error log pushed with status code: ' . $responseCode);
} catch (Throwable $th) {
Console::error('Error pushing log: ' . $th->getMessage());
}
}
Console::error('[Error] Type: ' . get_class($error));
Console::error('[Error] Message: ' . $error->getMessage());
Console::error('[Error] File: ' . $error->getFile());
Console::error('[Error] Line: ' . $error->getLine());
}
}
Console::error('[Error] Type: ' . get_class($error));
Console::error('[Error] Message: ' . $error->getMessage());
Console::error('[Error] File: ' . $error->getFile());
Console::error('[Error] Line: ' . $error->getLine());
};
$server->error(logError(...));
$server->error($logError);
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument, $logError) {
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument) {
sleep(5); // wait for the initial database schema to be ready
Console::success('Server started successfully');
@@ -326,7 +340,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
*/
// TODO: Remove this if check once it doesn't cause issues for cloud
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') {
Timer::tick(5000, function () use ($register, $stats, &$statsDocument, $logError) {
Timer::tick(5000, function () use ($register, $stats, &$statsDocument) {
$payload = [];
foreach ($stats as $projectId => $value) {
$payload[$projectId] = $stats->get($projectId, 'connectionsTotal');
@@ -342,15 +356,18 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
->setAttribute('timestamp', DateTime::now())
->setAttribute('value', json_encode($payload));
$database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
$database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), new Document([
'timestamp' => $statsDocument->getAttribute('timestamp'),
'value' => $statsDocument->getAttribute('value')
])));
} catch (Throwable $th) {
$logError($th, "updateWorkerDocument");
logError($th, "updateWorkerDocument");
}
});
}
});
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, $logError) {
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime) {
Console::success('Worker ' . $workerId . ' started successfully');
$telemetry = getTelemetry($workerId);
@@ -362,7 +379,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$attempts = 0;
$start = time();
Timer::tick(5000, function () use ($server, $register, $realtime, $stats, $logError) {
Timer::tick(5000, function () use ($server, $register, $realtime, $stats) {
/**
* Sending current connections to project channels on the console project every 5 seconds.
*/
@@ -548,7 +565,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
}
});
} catch (Throwable $th) {
$logError($th, "pubSubConnection");
logError($th, "pubSubConnection");
Console::error('Pub/sub error: ' . $th->getMessage());
$attempts++;
@@ -560,7 +577,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
Console::error('Failed to restart pub/sub...');
});
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $logError) {
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) {
$app = new Http('UTC');
$request = new Request($request);
$response = new Response(new SwooleResponse());
@@ -571,6 +588,10 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
Http::setResource('request', fn () => $request);
Http::setResource('response', fn () => $response);
$project = null;
$logUser = null;
$authorization = null;
try {
/** @var Document $project */
$project = $app->getResource('project');
@@ -591,8 +612,15 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
$projectRegion = $project->getAttribute('region', '');
$currentRegion = System::getEnv('_APP_REGION', 'default');
if (!empty($projectRegion) && $projectRegion !== $currentRegion) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Project is not accessible in this region. Please make sure you are using the correct endpoint');
}
$timelimit = $app->getResource('timelimit');
$user = $app->getResource('user'); /** @var User $user */
$logUser = $user;
/*
* Abuse Check
@@ -683,11 +711,11 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$stats->incr($project->getId(), 'connections');
$stats->incr($project->getId(), 'connectionsTotal');
} catch (Throwable $th) {
$logError($th, "initServer");
logError($th, 'realtime', project: $project, user: $logUser, authorization: $authorization);
// Handle SQL error code is 'HY000'
$code = $th->getCode();
if (!is_int($code)) {
if (!\is_int($code)) {
$code = 500;
}
@@ -718,7 +746,10 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
}
});
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId, $logError) {
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
$project = null;
$authorization = null;
try {
$response = new Response(new SwooleResponse());
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
@@ -844,7 +875,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
}
} catch (Throwable $th) {
$logError($th, "realtimeMessage");
logError($th, 'realtimeMessage', project: $project, authorization: $authorization);
$code = $th->getCode();
if (!is_int($code)) {
$code = 500;
+4 -4
View File
@@ -9,7 +9,6 @@ use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Execution;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
@@ -355,9 +354,6 @@ Server::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
Server::setResource('queueForExecutions', function (Publisher $publisher) {
return new Execution($publisher);
}, ['publisher']);
Server::setResource('queueForRealtime', function () {
return new Realtime();
@@ -542,6 +538,10 @@ try {
$worker = $platform->getWorker();
Server::setResource('bus', function ($register) use ($worker) {
return $register->get('bus')->setResolver(fn (string $name) => $worker->getResource($name));
}, ['register']);
$worker
->error()
->inject('error')
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
exec php /usr/src/code/app/cli.php time-travel "$@"
+3 -1
View File
@@ -19,7 +19,8 @@
"autoload": {
"psr-4": {
"Appwrite\\": "src/Appwrite",
"Executor\\": "src/Executor"
"Executor\\": "src/Executor",
"Utopia\\Bus\\": "src/Utopia/Bus"
}
},
"autoload-dev": {
@@ -99,6 +100,7 @@
"swoole/ide-helper": "6.*",
"phpstan/phpstan": "1.12.*",
"textalk/websocket": "1.5.*",
"czproject/git-php": "4.*",
"laravel/pint": "1.*",
"phpbench/phpbench": "1.*"
},
Generated
+162 -97
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "1fb043a556550f62ab27a0aad0b9332a",
"content-hash": "1cc64e07484256225f56bd525674c3b8",
"packages": [
{
"name": "adhocore/jwt",
@@ -2708,16 +2708,16 @@
},
{
"name": "symfony/http-client",
"version": "v7.4.5",
"version": "v7.4.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f"
"reference": "1010624285470eb60e88ed10035102c75b4ea6af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f",
"reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f",
"url": "https://api.github.com/repos/symfony/http-client/zipball/1010624285470eb60e88ed10035102c75b4ea6af",
"reference": "1010624285470eb60e88ed10035102c75b4ea6af",
"shasum": ""
},
"require": {
@@ -2785,7 +2785,7 @@
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.4.5"
"source": "https://github.com/symfony/http-client/tree/v7.4.7"
},
"funding": [
{
@@ -2805,7 +2805,7 @@
"type": "tidelift"
}
],
"time": "2026-01-27T16:16:02+00:00"
"time": "2026-03-05T11:16:58+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -3850,16 +3850,16 @@
},
{
"name": "utopia-php/database",
"version": "5.3.4",
"version": "5.3.7",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "ba1ee9cb2c7624d0fada782b285bd9958a07bbe5"
"reference": "438cc82af2981cd41ad200dd9b0df5bf00f3046a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/ba1ee9cb2c7624d0fada782b285bd9958a07bbe5",
"reference": "ba1ee9cb2c7624d0fada782b285bd9958a07bbe5",
"url": "https://api.github.com/repos/utopia-php/database/zipball/438cc82af2981cd41ad200dd9b0df5bf00f3046a",
"reference": "438cc82af2981cd41ad200dd9b0df5bf00f3046a",
"shasum": ""
},
"require": {
@@ -3902,9 +3902,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/5.3.4"
"source": "https://github.com/utopia-php/database/tree/5.3.7"
},
"time": "2026-02-24T00:37:36+00:00"
"time": "2026-03-09T04:28:56+00:00"
},
{
"name": "utopia-php/detector",
@@ -4058,16 +4058,16 @@
},
{
"name": "utopia-php/domains",
"version": "1.0.2",
"version": "1.0.5",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/domains.git",
"reference": "b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6"
"reference": "0edf6bb2b07f30db849a267027077bf5abb994c6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/domains/zipball/b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6",
"reference": "b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6",
"url": "https://api.github.com/repos/utopia-php/domains/zipball/0edf6bb2b07f30db849a267027077bf5abb994c6",
"reference": "0edf6bb2b07f30db849a267027077bf5abb994c6",
"shasum": ""
},
"require": {
@@ -4114,9 +4114,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/domains/issues",
"source": "https://github.com/utopia-php/domains/tree/1.0.2"
"source": "https://github.com/utopia-php/domains/tree/1.0.5"
},
"time": "2026-02-25T08:18:25+00:00"
"time": "2026-03-03T09:20:50+00:00"
},
{
"name": "utopia-php/dsn",
@@ -4267,16 +4267,16 @@
},
{
"name": "utopia-php/framework",
"version": "0.33.40",
"version": "0.33.41",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
"reference": "0ba25e1282c6a2f849053f7ccf28d567c2c321b1"
"reference": "0f3bf2377c867e547c929c3733b8224afee6ef06"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/http/zipball/0ba25e1282c6a2f849053f7ccf28d567c2c321b1",
"reference": "0ba25e1282c6a2f849053f7ccf28d567c2c321b1",
"url": "https://api.github.com/repos/utopia-php/http/zipball/0f3bf2377c867e547c929c3733b8224afee6ef06",
"reference": "0f3bf2377c867e547c929c3733b8224afee6ef06",
"shasum": ""
},
"require": {
@@ -4310,9 +4310,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
"source": "https://github.com/utopia-php/http/tree/0.33.40"
"source": "https://github.com/utopia-php/http/tree/0.33.41"
},
"time": "2026-02-19T13:00:08+00:00"
"time": "2026-02-24T12:01:28+00:00"
},
{
"name": "utopia-php/image",
@@ -4517,16 +4517,16 @@
},
{
"name": "utopia-php/migration",
"version": "1.6.1",
"version": "1.6.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "c5c7544d02d2418536d41050794050132f247d62"
"reference": "c2d016944cb029fa5ff822ceee704785a06ef289"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/c5c7544d02d2418536d41050794050132f247d62",
"reference": "c5c7544d02d2418536d41050794050132f247d62",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/c2d016944cb029fa5ff822ceee704785a06ef289",
"reference": "c2d016944cb029fa5ff822ceee704785a06ef289",
"shasum": ""
},
"require": {
@@ -4566,9 +4566,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/1.6.1"
"source": "https://github.com/utopia-php/migration/tree/1.6.3"
},
"time": "2026-02-17T05:49:48+00:00"
"time": "2026-03-04T07:08:22+00:00"
},
{
"name": "utopia-php/mongo",
@@ -4684,16 +4684,16 @@
},
{
"name": "utopia-php/pools",
"version": "1.0.2",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/pools.git",
"reference": "b7d8dd00306cdd8bf3ff6f1dc90caeaf27dabeb1"
"reference": "74de7c5457a2c447f27e7ec4d72e8412a7d68c10"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/pools/zipball/b7d8dd00306cdd8bf3ff6f1dc90caeaf27dabeb1",
"reference": "b7d8dd00306cdd8bf3ff6f1dc90caeaf27dabeb1",
"url": "https://api.github.com/repos/utopia-php/pools/zipball/74de7c5457a2c447f27e7ec4d72e8412a7d68c10",
"reference": "74de7c5457a2c447f27e7ec4d72e8412a7d68c10",
"shasum": ""
},
"require": {
@@ -4731,9 +4731,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/pools/issues",
"source": "https://github.com/utopia-php/pools/tree/1.0.2"
"source": "https://github.com/utopia-php/pools/tree/1.0.3"
},
"time": "2026-01-28T13:12:36+00:00"
"time": "2026-02-26T08:42:40+00:00"
},
{
"name": "utopia-php/preloader",
@@ -5215,16 +5215,16 @@
},
{
"name": "utopia-php/vcs",
"version": "2.0.0",
"version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
"reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14"
"reference": "92a1650824ba0c5e6a1bc46e622ac87c50a08920"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/058049326e04a2a0c2f0ce8ad00c7e84825aba14",
"reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/92a1650824ba0c5e6a1bc46e622ac87c50a08920",
"reference": "92a1650824ba0c5e6a1bc46e622ac87c50a08920",
"shasum": ""
},
"require": {
@@ -5258,9 +5258,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
"source": "https://github.com/utopia-php/vcs/tree/2.0.0"
"source": "https://github.com/utopia-php/vcs/tree/2.0.1"
},
"time": "2026-02-25T11:36:45+00:00"
"time": "2026-02-27T12:18:49+00:00"
},
{
"name": "utopia-php/websocket",
@@ -5438,16 +5438,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.11.1",
"version": "1.11.6",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb"
"reference": "f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6ff411f26f2750eea05c7598c14bb3a2ada898cb",
"reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38",
"reference": "f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38",
"shasum": ""
},
"require": {
@@ -5483,22 +5483,22 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/1.11.1"
"source": "https://github.com/appwrite/sdk-generator/tree/1.11.6"
},
"time": "2026-02-25T07:15:19+00:00"
"time": "2026-03-09T07:12:51+00:00"
},
{
"name": "brianium/paratest",
"version": "v7.19.0",
"version": "v7.19.1",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
"reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6"
"reference": "95b03194f4cdf5c83175ceead673e21cb66465e7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6",
"reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/95b03194f4cdf5c83175ceead673e21cb66465e7",
"reference": "95b03194f4cdf5c83175ceead673e21cb66465e7",
"shasum": ""
},
"require": {
@@ -5512,7 +5512,7 @@
"phpunit/php-code-coverage": "^12.5.3 || ^13.0.1",
"phpunit/php-file-iterator": "^6.0.1 || ^7",
"phpunit/php-timer": "^8 || ^9",
"phpunit/phpunit": "^12.5.9 || ^13",
"phpunit/phpunit": "^12.5.14 || ^13.0.5",
"sebastian/environment": "^8.0.3 || ^9",
"symfony/console": "^7.4.4 || ^8.0.4",
"symfony/process": "^7.4.5 || ^8.0.5"
@@ -5522,10 +5522,10 @@
"ext-pcntl": "*",
"ext-pcov": "*",
"ext-posix": "*",
"phpstan/phpstan": "^2.1.38",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
"phpstan/phpstan-phpunit": "^2.0.12",
"phpstan/phpstan-strict-rules": "^2.0.8",
"phpstan/phpstan": "^2.1.40",
"phpstan/phpstan-deprecation-rules": "^2.0.4",
"phpstan/phpstan-phpunit": "^2.0.16",
"phpstan/phpstan-strict-rules": "^2.0.10",
"symfony/filesystem": "^7.4.0 || ^8.0.1"
},
"bin": [
@@ -5566,7 +5566,7 @@
],
"support": {
"issues": "https://github.com/paratestphp/paratest/issues",
"source": "https://github.com/paratestphp/paratest/tree/v7.19.0"
"source": "https://github.com/paratestphp/paratest/tree/v7.19.1"
},
"funding": [
{
@@ -5578,7 +5578,71 @@
"type": "paypal"
}
],
"time": "2026-02-06T10:53:26+00:00"
"time": "2026-02-25T14:53:45+00:00"
},
{
"name": "czproject/git-php",
"version": "v4.6.0",
"source": {
"type": "git",
"url": "https://github.com/czproject/git-php.git",
"reference": "1f1ecc92aea9ee31120f4f5b759f5aa947420b0a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/czproject/git-php/zipball/1f1ecc92aea9ee31120f4f5b759f5aa947420b0a",
"reference": "1f1ecc92aea9ee31120f4f5b759f5aa947420b0a",
"shasum": ""
},
"require": {
"php": "8.0 - 8.5"
},
"require-dev": {
"nette/tester": "^2.5"
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Jan Pecha",
"email": "janpecha@email.cz"
}
],
"description": "Library for work with Git repository in PHP.",
"keywords": [
"git"
],
"support": {
"issues": "https://github.com/czproject/git-php/issues",
"source": "https://github.com/czproject/git-php/tree/v4.6.0"
},
"funding": [
{
"url": "https://github.com/sponsors/janpecha",
"type": "github"
},
{
"url": "https://www.janpecha.cz/donate/git-php/",
"type": "other"
},
{
"url": "https://donate.stripe.com/7sIcO2a9maTSg2A9AA",
"type": "stripe"
},
{
"url": "https://thanks.dev/u/gh/czproject",
"type": "thanks.dev"
}
],
"time": "2025-11-10T07:24:07+00:00"
},
{
"name": "doctrine/annotations",
@@ -6334,16 +6398,16 @@
},
{
"name": "phpbench/phpbench",
"version": "1.4.3",
"version": "1.5.1",
"source": {
"type": "git",
"url": "https://github.com/phpbench/phpbench.git",
"reference": "b641dde59d969ea42eed70a39f9b51950bc96878"
"reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpbench/phpbench/zipball/b641dde59d969ea42eed70a39f9b51950bc96878",
"reference": "b641dde59d969ea42eed70a39f9b51950bc96878",
"url": "https://api.github.com/repos/phpbench/phpbench/zipball/9a28fd0833f11171b949843c6fd663eb69b6d14c",
"reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c",
"shasum": ""
},
"require": {
@@ -6354,7 +6418,7 @@
"ext-reflection": "*",
"ext-spl": "*",
"ext-tokenizer": "*",
"php": "^8.1",
"php": "^8.2",
"phpbench/container": "^2.2",
"psr/log": "^1.1 || ^2.0 || ^3.0",
"seld/jsonlint": "^1.1",
@@ -6374,8 +6438,9 @@
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^10.4 || ^11.0",
"phpunit/phpunit": "^11.5",
"rector/rector": "^1.2",
"sebastian/exporter": "^6.3.2",
"symfony/error-handler": "^6.1 || ^7.0 || ^8.0",
"symfony/var-dumper": "^6.1 || ^7.0 || ^8.0"
},
@@ -6420,7 +6485,7 @@
],
"support": {
"issues": "https://github.com/phpbench/phpbench/issues",
"source": "https://github.com/phpbench/phpbench/tree/1.4.3"
"source": "https://github.com/phpbench/phpbench/tree/1.5.1"
},
"funding": [
{
@@ -6428,15 +6493,15 @@
"type": "github"
}
],
"time": "2025-11-06T19:07:31+00:00"
"time": "2026-03-05T08:18:58+00:00"
},
{
"name": "phpstan/phpstan",
"version": "1.12.32",
"version": "1.12.33",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8",
"reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/37982d6fc7cbb746dda7773530cda557cdf119e1",
"reference": "37982d6fc7cbb746dda7773530cda557cdf119e1",
"shasum": ""
},
"require": {
@@ -6481,7 +6546,7 @@
"type": "github"
}
],
"time": "2025-09-30T10:16:31+00:00"
"time": "2026-02-28T20:30:03+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -8031,16 +8096,16 @@
},
{
"name": "symfony/console",
"version": "v8.0.4",
"version": "v8.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b"
"reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b",
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b",
"url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
"reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
"shasum": ""
},
"require": {
@@ -8097,7 +8162,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v8.0.4"
"source": "https://github.com/symfony/console/tree/v8.0.7"
},
"funding": [
{
@@ -8117,20 +8182,20 @@
"type": "tidelift"
}
],
"time": "2026-01-13T13:06:50+00:00"
"time": "2026-03-06T14:06:22+00:00"
},
{
"name": "symfony/filesystem",
"version": "v8.0.1",
"version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "d937d400b980523dc9ee946bb69972b5e619058d"
"reference": "7bf9162d7a0dff98d079b72948508fa48018a770"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d",
"reference": "d937d400b980523dc9ee946bb69972b5e619058d",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770",
"reference": "7bf9162d7a0dff98d079b72948508fa48018a770",
"shasum": ""
},
"require": {
@@ -8167,7 +8232,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v8.0.1"
"source": "https://github.com/symfony/filesystem/tree/v8.0.6"
},
"funding": [
{
@@ -8187,20 +8252,20 @@
"type": "tidelift"
}
],
"time": "2025-12-01T09:13:36+00:00"
"time": "2026-02-25T16:59:43+00:00"
},
{
"name": "symfony/finder",
"version": "v8.0.5",
"version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0"
"reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0",
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0",
"url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c",
"reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c",
"shasum": ""
},
"require": {
@@ -8235,7 +8300,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v8.0.5"
"source": "https://github.com/symfony/finder/tree/v8.0.6"
},
"funding": [
{
@@ -8255,7 +8320,7 @@
"type": "tidelift"
}
],
"time": "2026-01-26T15:08:38+00:00"
"time": "2026-01-29T09:41:02+00:00"
},
{
"name": "symfony/options-resolver",
@@ -8725,16 +8790,16 @@
},
{
"name": "symfony/string",
"version": "v8.0.4",
"version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "758b372d6882506821ed666032e43020c4f57194"
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194",
"reference": "758b372d6882506821ed666032e43020c4f57194",
"url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
"shasum": ""
},
"require": {
@@ -8791,7 +8856,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v8.0.4"
"source": "https://github.com/symfony/string/tree/v8.0.6"
},
"funding": [
{
@@ -8811,7 +8876,7 @@
"type": "tidelift"
}
],
"time": "2026-01-12T12:37:40+00:00"
"time": "2026-02-09T10:14:57+00:00"
},
{
"name": "textalk/websocket",
@@ -9067,5 +9132,5 @@
"platform-overrides": {
"php": "8.3"
},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}
-18
View File
@@ -152,7 +152,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_SMTP_HOST
- _APP_SMTP_PORT
- _APP_SMTP_SECURE
@@ -305,7 +304,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_USAGE_STATS
- _APP_LOGGING_CONFIG
- _APP_LOGGING_CONFIG_REALTIME
@@ -340,7 +338,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
@@ -371,7 +368,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -414,7 +410,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_STORAGE_DEVICE
- _APP_STORAGE_S3_ACCESS_KEY
- _APP_STORAGE_S3_SECRET
@@ -474,7 +469,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_WORKERS_NUM
- _APP_QUEUE_NAME
@@ -513,7 +507,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_VCS_GITHUB_APP_NAME
- _APP_VCS_GITHUB_PRIVATE_KEY
@@ -658,7 +651,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
@@ -722,7 +714,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_FUNCTIONS_TIMEOUT
- _APP_SITES_TIMEOUT
- _APP_COMPUTE_BUILD_TIMEOUT
@@ -808,7 +799,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_SMS_FROM
- _APP_SMS_PROVIDER
@@ -875,7 +865,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
@@ -918,7 +907,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
@@ -994,7 +982,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -1028,7 +1015,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -1062,7 +1048,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -1100,7 +1085,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_DATABASE_SHARED_TABLES
appwrite-task-scheduler-executions:
@@ -1131,7 +1115,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
appwrite-task-scheduler-messages:
entrypoint: schedule-messages
@@ -1161,7 +1144,6 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_DATABASE_SHARED_TABLES
appwrite-assistant:
+4
View File
@@ -1,7 +1,11 @@
parameters:
level: 8
paths:
- src/Utopia/Bus
- src/Appwrite/Bus
- src/Appwrite/Transformation
bootstrapFiles:
- app/init/constants.php
scanDirectories:
- vendor/swoole/ide-helper
excludePaths:
+1
View File
@@ -6,6 +6,7 @@
colors="true"
processIsolation="false"
stopOnFailure="false"
stopOnError="false"
cacheDirectory=".phpunit.cache"
>
<extensions>
@@ -0,0 +1,22 @@
<?php
namespace Appwrite\Bus\Events;
use Utopia\Bus\Event;
class ExecutionCompleted implements Event
{
/**
* @param array<string, mixed> $execution
* @param array<string, mixed> $project
* @param array<string, mixed> $spec
* @param array<string, mixed> $resource
*/
public function __construct(
public readonly array $execution,
public readonly array $project,
public readonly array $spec = [],
public readonly array $resource = [],
) {
}
}
@@ -0,0 +1,22 @@
<?php
namespace Appwrite\Bus\Events;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Bus\Event;
class RequestCompleted implements Event
{
/**
* @param array<string, mixed> $project
* @param array<string, mixed> $deployment
*/
public function __construct(
public readonly array $project,
public readonly Request $request,
public readonly Response $response,
public readonly array $deployment = [],
) {
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace Appwrite\Bus\Listeners;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Event\Execution;
use Utopia\Bus\Listener;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
class Log extends Listener
{
public static function getName(): string
{
return 'log';
}
public static function getEvents(): array
{
return [ExecutionCompleted::class];
}
public function __construct()
{
$this
->desc('Persists execution logs to database via queue')
->inject('publisher')
->callback($this->handle(...));
}
public function handle(ExecutionCompleted $event, Publisher $publisher): void
{
$queueForExecutions = new Execution($publisher);
$queueForExecutions
->setExecution(new Document($event->execution))
->setProject(new Document($event->project))
->trigger();
}
}
+114
View File
@@ -0,0 +1,114 @@
<?php
namespace Appwrite\Bus\Listeners;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\StatsUsage;
use Utopia\Bus\Event;
use Utopia\Bus\Listener;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
class Usage extends Listener
{
public static function getName(): string
{
return 'usage';
}
public static function getEvents(): array
{
return [
ExecutionCompleted::class,
RequestCompleted::class,
];
}
public function __construct()
{
$this
->desc('Records usage metrics')
->inject('publisherStatsUsage')
->callback($this->handle(...));
}
public function handle(Event $event, Publisher $publisher): void
{
match (true) {
$event instanceof ExecutionCompleted => $this->handleExecutionCompleted($event, $publisher),
$event instanceof RequestCompleted => $this->handleRequestCompleted($event, $publisher),
default => null,
};
}
private function handleExecutionCompleted(ExecutionCompleted $event, Publisher $publisher): void
{
$execution = new Document($event->execution);
$resource = new Document($event->resource);
// Non-SSR sites don't record execution metrics
if ($execution->getAttribute('resourceType') === 'sites' && $resource->getAttribute('adapter') !== 'ssr') {
return;
}
$project = new Document($event->project);
$spec = $event->spec;
$resourceType = $execution->getAttribute('resourceType', '');
$resourceInternalId = $execution->getAttribute('resourceInternalId', '');
$duration = $execution->getAttribute('duration', 0);
$compute = (int)($duration * 1000);
$mbSeconds = (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $duration * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT));
$queueForStatsUsage = new StatsUsage($publisher);
$queueForStatsUsage
->setProject($project)
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric(str_replace(['{resourceType}'], [$resourceType], METRIC_RESOURCE_TYPE_EXECUTIONS), 1)
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$resourceType, $resourceInternalId], METRIC_RESOURCE_TYPE_ID_EXECUTIONS), 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, $compute)
->addMetric(str_replace(['{resourceType}'], [$resourceType], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE), $compute)
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$resourceType, $resourceInternalId], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE), $compute)
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, $mbSeconds)
->addMetric(str_replace(['{resourceType}'], [$resourceType], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS), $mbSeconds)
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$resourceType, $resourceInternalId], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), $mbSeconds)
->trigger();
}
private function handleRequestCompleted(RequestCompleted $event, Publisher $publisher): void
{
$fileSize = 0;
$file = $event->request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$project = new Document($event->project);
$deployment = new Document($event->deployment);
$queueForStatsUsage = new StatsUsage($publisher);
$inbound = $event->request->getSize() + $fileSize;
$outbound = $event->response->getSize();
$queueForStatsUsage->setProject($project);
if ($deployment->getAttribute('resourceType') === 'sites') {
$siteInternalId = $deployment->getAttribute('resourceInternalId', '');
$queueForStatsUsage
->addMetric(METRIC_SITES_REQUESTS, 1)
->addMetric(METRIC_SITES_INBOUND, $inbound)
->addMetric(METRIC_SITES_OUTBOUND, $outbound)
->addMetric(str_replace('{siteInternalId}', $siteInternalId, METRIC_SITES_ID_REQUESTS), 1)
->addMetric(str_replace('{siteInternalId}', $siteInternalId, METRIC_SITES_ID_INBOUND), $inbound)
->addMetric(str_replace('{siteInternalId}', $siteInternalId, METRIC_SITES_ID_OUTBOUND), $outbound);
} else {
$queueForStatsUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $inbound)
->addMetric(METRIC_NETWORK_OUTBOUND, $outbound);
}
$queueForStatsUsage->trigger();
}
}
+3 -1
View File
@@ -725,7 +725,9 @@ class Event
$events = $pairedEvents;
}
// mirrored events can have duplicates in case of smaller events
return array_unique($events);
// array unique can turns list to hasmap in case duplicates present
// so forcing array value will turn this to array list always
return array_values(array_unique($events));
}
/**
+2
View File
@@ -166,6 +166,7 @@ class Exception extends \Exception
/** Sites */
public const string SITE_NOT_FOUND = 'site_not_found';
public const string SITE_ALREADY_EXISTS = 'site_already_exists';
public const string SITE_TEMPLATE_NOT_FOUND = 'site_template_not_found';
/** Functions */
@@ -365,6 +366,7 @@ class Exception extends \Exception
/** Message */
public const string MESSAGE_NOT_FOUND = 'message_not_found';
public const string MESSAGE_ALREADY_EXISTS = 'message_already_exists';
public const string MESSAGE_MISSING_TARGET = 'message_missing_target';
public const string MESSAGE_ALREADY_SENT = 'message_already_sent';
public const string MESSAGE_ALREADY_PROCESSING = 'message_already_processing';
+7 -18
View File
@@ -510,25 +510,13 @@ class Realtime extends MessagingAdapter
$collectionId = $payload->getAttribute('$collectionId', '');
$resourceId = $tableId ?: $collectionId;
$channels = [];
// backward compat(tablesdb will have databases channels + tablesdb prefixed channels)
if ($parts[0] === 'databases' || $parts[0] === 'tablesdb') {
$prefix = 'databases';
$channels = self::getDatabaseChannels('legacy', $database->getId(), $resourceId, $payload->getId(), $prefix);
$channels = array_unique([
...$channels,
...self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId(), $prefix)
]);
}
// prefixed channels -> tablesdb
if ($parts[0] !== 'databases') {
$channels = array_unique([
...$channels,
...self::getDatabaseChannels($parts[0], $database->getId(), $resourceId, $payload->getId()),
]);
}
// sending legacy + tablesdb events to both legacy and tablesdb
$channels = array_values(array_unique(array_merge(
self::getDatabaseChannels('legacy', $database->getId(), $resourceId, $payload->getId(), 'databases'),
self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId(), 'databases'),
self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId())
)));
$roles = $collection->getAttribute('documentSecurity', false)
? \array_merge($collection->getRead(), $payload->getRead())
@@ -620,6 +608,7 @@ class Realtime extends MessagingAdapter
$channels[] = "{$basePrefix}.{$databaseId}.collections.{$resourceId}.documents";
$channels[] = "{$basePrefix}.{$databaseId}.collections.{$resourceId}.documents.{$payloadId}";
break;
case 'tablesdb':
$channels[] = 'rows';
$channels[] = "{$basePrefix}.{$databaseId}.tables.{$resourceId}.rows";
@@ -119,7 +119,7 @@ class Update extends Action
$authenticator->setAttribute('verified', true);
$dbForProject->updateDocument('authenticators', $authenticator->getId(), $authenticator);
$dbForProject->updateDocument('authenticators', $authenticator->getId(), new Document(['verified' => true]));
$dbForProject->purgeCachedDocument('users', $user->getId());
$factors = $session->getAttribute('factors', []);
@@ -127,7 +127,7 @@ class Update extends Action
$factors = \array_values(\array_unique($factors));
$session->setAttribute('factors', $factors);
$dbForProject->updateDocument('sessions', $session->getId(), $session);
$dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors]));
$queueForEvents->setParam('userId', $user->getId());
@@ -117,7 +117,7 @@ class Update extends Action
$mfaRecoveryCodes = \array_diff($mfaRecoveryCodes, [$otp]);
$mfaRecoveryCodes = \array_values($mfaRecoveryCodes);
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
$dbForProject->updateDocument('users', $user->getId(), $user);
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
return true;
}
@@ -147,11 +147,13 @@ class Update extends Action
$factors[] = $type;
$factors = \array_values(\array_unique($factors));
$mfaUpdatedAt = DateTime::now();
$session
->setAttribute('factors', $factors)
->setAttribute('mfaUpdatedAt', DateTime::now());
->setAttribute('mfaUpdatedAt', $mfaUpdatedAt);
$dbForProject->updateDocument('sessions', $session->getId(), $session);
$dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors, 'mfaUpdatedAt' => $mfaUpdatedAt]));
$queueForEvents
->setParam('userId', $user->getId())
@@ -93,7 +93,7 @@ class Create extends Action
$mfaRecoveryCodes = Type::generateBackupCodes();
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
$dbForProject->updateDocument('users', $user->getId(), $user);
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
$queueForEvents->setParam('userId', $user->getId());
@@ -91,7 +91,7 @@ class Update extends Action
$mfaRecoveryCodes = Type::generateBackupCodes();
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
$dbForProject->updateDocument('users', $user->getId(), $user);
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
$queueForEvents->setParam('userId', $user->getId());
@@ -72,7 +72,7 @@ class Update extends Action
): void {
$user->setAttribute('mfa', $mfa);
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['mfa' => $mfa]));
if ($mfa) {
$factors = $session->getAttribute('factors', []);
@@ -89,7 +89,7 @@ class Update extends Action
$factors = \array_values(\array_unique($factors));
$session->setAttribute('factors', $factors);
$dbForProject->updateDocument('sessions', $session->getId(), $session);
$dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors]));
}
$queueForEvents->setParam('userId', $user->getId());
+12 -2
View File
@@ -143,7 +143,12 @@ class Base extends Action
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), $function);
$dbForProject->updateDocument('functions', $function->getId(), new Document([
'latestDeploymentId' => $deployment->getId(),
'latestDeploymentInternalId' => $deployment->getSequence(),
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
@@ -249,7 +254,12 @@ class Base extends Action
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), $site);
$dbForProject->updateDocument('sites', $site->getId(), new Document([
'latestDeploymentId' => $deployment->getId(),
'latestDeploymentInternalId' => $deployment->getSequence(),
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
$sitesDomain = $platform['sitesDomain'];
$domain = ID::unique() . "." . $sitesDomain;
@@ -12,6 +12,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\IndexDependency as IndexDependencyValidator;
use Utopia\Database\Validator\Key;
@@ -98,7 +99,8 @@ class Delete extends Action
}
if ($attribute->getAttribute('status') === 'available') {
$attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'deleting'));
$attribute->setAttribute('status', 'deleting');
$attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), new Document(['status' => 'deleting']));
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
@@ -118,7 +120,8 @@ class Delete extends Action
}
if ($relatedAttribute->getAttribute('status') === 'available') {
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'deleting'));
$relatedAttribute->setAttribute('status', 'deleting');
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), new Document(['status' => 'deleting']));
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $options['relatedCollection']);
@@ -24,6 +24,7 @@ use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
class XList extends Action
@@ -70,15 +71,17 @@ class XList extends Action
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID to read uncommitted changes within the transaction.', true, ['dbForProject'])
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->param('ttl', 0, new Range(min: 0, max: 86400), 'TTL (seconds) for cached responses when caching is enabled for select queries. Must be between 0 and 86400 (24 hours).', true)
->inject('response')
->inject('dbForProject')
->inject('user')
->inject('queueForStatsUsage')
->inject('transactionState')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, Document $user, StatsUsage $queueForStatsUsage, TransactionState $transactionState, Authorization $authorization): void
{
$isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
@@ -129,9 +132,73 @@ class XList extends Action
$documents = $transactionState->listDocuments($collectionTableId, $transactionId, $queries);
$total = $includeTotal ? $transactionState->countDocuments($collectionTableId, $transactionId, $queries) : 0;
} elseif (! empty($selectQueries)) {
// has selects, allow relationship on documents
$documents = $dbForProject->find($collectionTableId, $queries);
$total = $includeTotal ? $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT) : 0;
if ((int)$ttl > 0) {
$serializedQueries = [];
foreach ($queries as $query) {
$serializedQueries[] = $query instanceof Query ? $query->toArray() : $query;
}
$hostname = $dbForProject->getAdapter()->getHostname();
$roles = $dbForProject->getAuthorization()->getRoles();
$schemaHash = \md5(\json_encode($collection->getAttribute('attributes', [])) . \json_encode($collection->getAttribute('indexes', [])));
$cacheKeyBase = \sprintf(
'%s-cache-%s:%s:%s:collection:%s:%s:user:%s:%s',
$dbForProject->getCacheName(),
$hostname ?? '',
$dbForProject->getNamespace(),
$dbForProject->getTenant(),
$collectionId,
$schemaHash,
\md5(\json_encode($roles)),
\md5(\json_encode($serializedQueries))
);
$documentsCacheKey = $cacheKeyBase . ':documents';
$totalCacheKey = $cacheKeyBase . ':total';
$documentsCacheHit = $totalDocumentsCacheHit = false;
$cachedDocuments = $dbForProject->getCache()->load($documentsCacheKey, $ttl);
if ($cachedDocuments !== null &&
$cachedDocuments !== false &&
\is_array($cachedDocuments)) {
$documents = \array_map(function ($doc) {
return new Document($doc);
}, $cachedDocuments);
$documentsCacheHit = true;
} else {
$documents = $dbForProject->find($collectionTableId, $queries);
// Convert Document objects to arrays for caching
$documentsArray = \array_map(function ($doc) {
return $doc->getArrayCopy();
}, $documents);
$dbForProject->getCache()->save($documentsCacheKey, $documentsArray);
}
if ($includeTotal) {
$cachedTotal = $dbForProject->getCache()->load($totalCacheKey, $ttl);
if ($cachedTotal !== null && $cachedTotal !== false) {
$total = $cachedTotal;
$totalDocumentsCacheHit = true;
} else {
$total = $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT);
$dbForProject->getCache()->save($totalCacheKey, $total);
}
} else {
$total = 0;
}
$response->addHeader('X-Appwrite-Cache', $documentsCacheHit ? 'hit' : 'miss');
} else {
// has selects, allow relationship on documents
$documents = $dbForProject->find($collectionTableId, $queries);
$total = $includeTotal ? $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT) : 0;
}
} else {
// has no selects, disable relationship loading on documents
/* @type Document[] $documents */
@@ -12,6 +12,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
@@ -96,7 +97,8 @@ class Delete extends Action
// Only update status if removing available index
if ($index->getAttribute('status') === 'available') {
$index = $dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting'));
$index->setAttribute('status', 'deleting');
$index = $dbForProject->updateDocument('indexes', $index->getId(), new Document(['status' => 'deleting']));
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
@@ -14,6 +14,7 @@ use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
class XList extends DocumentXList
@@ -56,8 +57,10 @@ class XList extends DocumentXList
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID to read uncommitted changes within the transaction.', true, ['dbForProject'])
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->param('ttl', 0, new Range(min: 0, max: 86400), 'TTL (seconds) for cached responses when caching is enabled for select queries. Must be between 0 and 86400 (24 hours).', true)
->inject('response')
->inject('dbForProject')
->inject('user')
->inject('queueForStatsUsage')
->inject('transactionState')
->inject('authorization')
@@ -322,13 +322,19 @@ class Databases extends Action
$dbForProject->updateDocument(
'attributes',
$attribute->getId(),
$attribute->setAttribute('status', 'stuck')
new Document([
'error' => $attribute->getAttribute('error'),
'status' => 'stuck',
])
);
if (!$relatedAttribute->isEmpty()) {
$dbForProject->updateDocument(
'attributes',
$relatedAttribute->getId(),
$relatedAttribute->setAttribute('status', 'stuck')
new Document([
'error' => $relatedAttribute->getAttribute('error'),
'status' => 'stuck',
])
);
}
@@ -382,7 +388,11 @@ class Databases extends Action
if ($exists) { // Delete the duplicate if created, else update in db
$this->deleteIndex($database, $collection, $index, $project, $dbForPlatform, $dbForProject, $queueForRealtime);
} else {
$dbForProject->updateDocument('indexes', $index->getId(), $index);
$dbForProject->updateDocument('indexes', $index->getId(), new Document([
'attributes' => $index->getAttribute('attributes'),
'lengths' => $index->getAttribute('lengths'),
'orders' => $index->getAttribute('orders'),
]));
}
}
}
@@ -227,7 +227,9 @@ class Create extends Action
foreach ($activeDeployments as $activeDeployment) {
$activeDeployment->setAttribute('activate', false);
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), $activeDeployment);
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), new Document([
'activate' => false,
]));
}
}
@@ -255,14 +257,17 @@ class Create extends Action
'type' => $type
]));
$function = $function
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), $function);
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document([
'latestDeploymentId' => $deployment->getId(),
'latestDeploymentInternalId' => $deployment->getSequence(),
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceSize', $fileSize)->setAttribute('sourceMetadata', $metadata));
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
'sourceSize' => $fileSize,
'sourceMetadata' => $metadata,
]));
}
// Start the build
@@ -295,14 +300,17 @@ class Create extends Action
'type' => $type
]));
$function = $function
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), $function);
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document([
'latestDeploymentId' => $deployment->getId(),
'latestDeploymentInternalId' => $deployment->getSequence(),
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceChunksUploaded', $chunksUploaded)->setAttribute('sourceMetadata', $metadata));
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
'sourceChunksUploaded' => $chunksUploaded,
'sourceMetadata' => $metadata,
]));
}
}
@@ -107,11 +107,12 @@ class Delete extends Action
$function = $dbForProject->updateDocument(
'functions',
$function->getId(),
$function
->setAttribute('latestDeploymentCreatedAt', $latestDeployment->isEmpty() ? '' : $latestDeployment->getCreatedAt())
->setAttribute('latestDeploymentInternalId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getSequence())
->setAttribute('latestDeploymentId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getId())
->setAttribute('latestDeploymentStatus', $latestDeployment->isEmpty() ? '' : $latestDeployment->getAttribute('status', ''))
new Document([
'latestDeploymentCreatedAt' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getCreatedAt(),
'latestDeploymentInternalId' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getSequence(),
'latestDeploymentId' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getId(),
'latestDeploymentStatus' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getAttribute('status', ''),
])
);
}
@@ -10,6 +10,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
@@ -119,7 +120,12 @@ class Create extends Action
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), $function);
$dbForProject->updateDocument('functions', $function->getId(), new Document([
'latestDeploymentId' => $function->getAttribute('latestDeploymentId'),
'latestDeploymentInternalId' => $function->getAttribute('latestDeploymentInternalId'),
'latestDeploymentCreatedAt' => $function->getAttribute('latestDeploymentCreatedAt'),
'latestDeploymentStatus' => $function->getAttribute('latestDeploymentStatus'),
]));
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
@@ -91,7 +91,7 @@ class Update extends Action
$endTime = new \DateTime('now');
$duration = $endTime->getTimestamp() - $startTime->getTimestamp();
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttributes([
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
'buildEndedAt' => DateTime::now(),
'buildDuration' => $duration,
'status' => 'canceled'
@@ -99,7 +99,9 @@ class Update extends Action
if ($deployment->getSequence() === $function->getAttribute('latestDeploymentInternalId', '')) {
$function = $function->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), $function);
$dbForProject->updateDocument('functions', $function->getId(), new Document([
'latestDeploymentStatus' => $function->getAttribute('latestDeploymentStatus'),
]));
}
try {
@@ -174,7 +174,12 @@ class Create extends Base
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), $function);
$dbForProject->updateDocument('functions', $function->getId(), new Document([
'latestDeploymentId' => $function->getAttribute('latestDeploymentId'),
'latestDeploymentInternalId' => $function->getAttribute('latestDeploymentInternalId'),
'latestDeploymentCreatedAt' => $function->getAttribute('latestDeploymentCreatedAt'),
'latestDeploymentStatus' => $function->getAttribute('latestDeploymentStatus'),
]));
$this->updateEmptyManualRule($project, $function, $deployment, $dbForPlatform, $authorization);
@@ -180,7 +180,8 @@ class Create extends Base
$version = $function->getAttribute('version', 'v2');
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
$spec = Config::getParam('specifications')[$function->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$spec = Config::getParam('specifications')[$function->getAttribute('runtimeSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null;
@@ -420,6 +421,11 @@ class Create extends Base
try {
$version = $function->getAttribute('version', 'v2');
$command = $runtime['startCommand'];
if (!empty($deployment->getAttribute('startCommand', ''))) {
$command = 'cd /usr/local/server/src/function/ && ' . $deployment->getAttribute('startCommand', '');
}
$source = $deployment->getAttribute('buildPath', '');
$extension = str_ends_with($source, '.tar') ? 'tar' : 'tar.gz';
$command = $version === 'v2' ? '' : "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$command\"";
@@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
@@ -90,6 +91,15 @@ class Delete extends Base
}
$status = $execution->getAttribute('status');
// Treat timed-out executions as failed so they can be deleted.
if ($status === 'waiting' || $status === 'processing') {
$timeout = $function->getAttribute('timeout', 900);
$elapsed = \time() - \strtotime($execution->getCreatedAt());
if ($elapsed >= $timeout) {
$status = 'failed';
}
}
if (!in_array($status, ['completed', 'failed', 'scheduled'])) {
throw new Exception(Exception::EXECUTION_IN_PROGRESS);
}
@@ -110,7 +120,10 @@ class Delete extends Base
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('active', false);
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([
'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'),
'active' => $schedule->getAttribute('active'),
])));
}
}
@@ -82,6 +82,16 @@ class Get extends Base
throw new Exception(Exception::EXECUTION_NOT_FOUND);
}
// Override status in response if the execution is stuck in waiting/processing beyond the function timeout.
$status = $execution->getAttribute('status', '');
if ($status === 'waiting' || $status === 'processing') {
$timeout = $function->getAttribute('timeout', 900);
$elapsed = \time() - \strtotime($execution->getCreatedAt());
if ($elapsed >= $timeout) {
$execution->setAttribute('status', 'failed');
}
}
$response->dynamic($execution, Response::MODEL_EXECUTION);
}
}
@@ -11,6 +11,7 @@ use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Database\Validator\Queries\Executions;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
@@ -110,6 +111,35 @@ class XList extends Base
$cursor->setValue($cursorDocument);
}
// Calculate the cutoff datetime before which a waiting/processing execution is considered timed out.
$timeout = $function->getAttribute('timeout', 900);
$thresholdDate = new \DateTime("-{$timeout} seconds");
$threshold = DateTime::format($thresholdDate);
// Capture what statuses the caller explicitly requested, before we mutate the query.
$requestedStatuses = [];
foreach ($queries as $query) {
if ($query->getMethod() === Query::TYPE_EQUAL && $query->getAttribute() === 'status') {
$requestedStatuses = [...$requestedStatuses, ...$query->getValues()];
}
}
// If the caller is filtering by 'failed', expand the DB query to also return
// waiting/processing executions created before the timeout threshold, so timed-out
// executions that were never marked failed in the DB are included in the results.
foreach ($queries as $index => $query) {
if ($query->getMethod() === Query::TYPE_EQUAL && $query->getAttribute() === 'status' && \in_array('failed', $query->getValues())) {
$queries[$index] = Query::or([
$query,
Query::and([
Query::equal('status', ['waiting', 'processing']),
Query::createdBefore($threshold),
]),
]);
break;
}
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
@@ -119,6 +149,20 @@ class XList extends Base
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
// Override status in response for timed-out executions, but only when the caller
// did not explicitly request a non-failed status (e.g. waiting/processing).
if (empty(\array_diff($requestedStatuses, ['failed']))) {
foreach ($results as $execution) {
$status = $execution->getAttribute('status', '');
if ($status === 'waiting' || $status === 'processing') {
$elapsed = \time() - \strtotime($execution->getCreatedAt());
if ($elapsed >= $timeout) {
$execution->setAttribute('status', 'failed');
}
}
}
}
$response->dynamic(new Document([
'executions' => $results,
'total' => $total,
@@ -93,16 +93,23 @@ class Create extends Base
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function.', true)
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true)
->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true)
->param('specification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Runtime specification for the function and builds.', true, ['plan'])
), 'Build specification for the function deployments.', true, ['plan'])
->param('runtimeSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Runtime specification for the function executions.', true, ['plan'])
->param('templateRepository', '', new Text(128, 0), 'Repository name of the template.', true, deprecated: true)
->param('templateOwner', '', new Text(128, 0), 'The name of the owner of the template.', true, deprecated: true)
->param('templateRootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.', true, deprecated: true)
->param('templateVersion', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true, deprecated: true)
->param('deploymentRetention', 0, new Range(0, APP_COMPUTE_DEPLOYMENT_MAX_RETENTION), 'Days to keep non-active deployments before deletion. Value 0 means all deployments will be kept.', true)
->inject('response')
->inject('dbForProject')
->inject('timelimit')
@@ -138,11 +145,13 @@ class Create extends Base
string $providerBranch,
bool $providerSilentMode,
string $providerRootDirectory,
string $specification,
string $buildSpecification,
string $runtimeSpecification,
string $templateRepository,
string $templateOwner,
string $templateRootDirectory,
string $templateVersion,
int $deploymentRetention,
Response $response,
Database $dbForProject,
callable $timelimit,
@@ -215,6 +224,7 @@ class Create extends Base
'logging' => $logging,
'name' => $name,
'runtime' => $runtime,
'deploymentRetention' => $deploymentRetention,
'deploymentInternalId' => '',
'deploymentId' => '',
'events' => $events,
@@ -225,7 +235,6 @@ class Create extends Base
'entrypoint' => $entrypoint,
'commands' => $commands,
'scopes' => $scopes,
'deploymentRetention' => 0,
'startCommand' => '',
'search' => implode(' ', [$functionId, $name, $runtime]),
'version' => 'v5',
@@ -237,9 +246,8 @@ class Create extends Base
'providerBranch' => $providerBranch,
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification,
'buildSpecification' => $specification,
'runtimeSpecification' => $specification,
'buildSpecification' => $buildSpecification,
'runtimeSpecification' => $runtimeSpecification,
]));
} catch (DuplicateException) {
throw new Exception(Exception::FUNCTION_ALREADY_EXISTS);
@@ -283,7 +291,12 @@ class Create extends Base
$function->setAttribute('repositoryInternalId', $repository->getSequence());
}
$function = $dbForProject->updateDocument('functions', $function->getId(), $function);
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document([
'scheduleId' => $function->getAttribute('scheduleId'),
'scheduleInternalId' => $function->getAttribute('scheduleInternalId'),
'repositoryId' => $function->getAttribute('repositoryId'),
'repositoryInternalId' => $function->getAttribute('repositoryInternalId'),
]));
// Backwards compatibility with 1.6 behaviour
$requestFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
@@ -321,12 +334,12 @@ class Create extends Base
referenceType: 'branch'
);
$function = $function
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), $function);
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document([
'latestDeploymentId' => $deployment->getId(),
'latestDeploymentInternalId' => $deployment->getSequence(),
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
} elseif (!$template->isEmpty()) {
// Deploy non-VCS from template
$deploymentId = ID::unique();
@@ -347,12 +360,12 @@ class Create extends Base
'activate' => true,
]));
$function = $function
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), $function);
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document([
'latestDeploymentId' => $deployment->getId(),
'latestDeploymentInternalId' => $deployment->getSequence(),
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
@@ -13,6 +13,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
@@ -90,7 +91,10 @@ class Delete extends Base
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('active', false);
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([
'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'),
'active' => $schedule->getAttribute('active'),
])));
}
$queueForDeletes
@@ -103,7 +103,11 @@ class Update extends Base
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId')));
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([
'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'),
'schedule' => $schedule->getAttribute('schedule'),
'active' => $schedule->getAttribute('active'),
])));
$queries = [
Query::equal('trigger', ['manual']),
@@ -119,7 +123,10 @@ class Update extends Base
->setAttribute('deploymentId', $deployment->getId())
->setAttribute('deploymentInternalId', $deployment->getSequence());
$authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), $rule));
$authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
'deploymentId' => $rule->getAttribute('deploymentId'),
'deploymentInternalId' => $rule->getAttribute('deploymentInternalId'),
])));
}, $queries));
$queueForEvents
@@ -87,12 +87,19 @@ class Update extends Base
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function', true)
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true)
->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true)
->param('specification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Runtime specification for the function and builds.', true, ['plan'])
), 'Build specification for the function deployments.', true, ['plan'])
->param('runtimeSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Runtime specification for the function executions.', true, ['plan'])
->param('deploymentRetention', 0, new Range(0, APP_COMPUTE_DEPLOYMENT_MAX_RETENTION), 'Days to keep non-active deployments before deletion. Value 0 means all deployments will be kept.', true)
->inject('request')
->inject('response')
->inject('dbForProject')
@@ -124,7 +131,9 @@ class Update extends Base
string $providerBranch,
bool $providerSilentMode,
string $providerRootDirectory,
string $specification,
string $buildSpecification,
string $runtimeSpecification,
int $deploymentRetention,
Request $request,
Response $response,
Database $dbForProject,
@@ -209,7 +218,7 @@ class Update extends Base
'resourceId' => $function->getId(),
'resourceInternalId' => $function->getSequence(),
'resourceType' => 'function',
'providerPullRequestIds' => []
'providerPullRequestIds' => [],
]));
$repositoryId = $repository->getId();
@@ -229,13 +238,22 @@ class Update extends Base
}
// Enforce Cold Start if spec limits change.
if ($function->getAttribute('specification') !== $specification && !empty($function->getAttribute('deploymentId'))) {
try {
$executor->deleteRuntime($project->getId(), $function->getAttribute('deploymentId'));
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
if ($th->getCode() !== 404) {
throw $th;
if (!empty($function->getAttribute('deploymentId'))) {
$specsChanged = false;
if ($function->getAttribute('runtimeSpecification', '') !== $runtimeSpecification) {
$specsChanged = true;
} elseif ($function->getAttribute('buildSpecification', '') !== $buildSpecification) {
$specsChanged = true;
}
if ($specsChanged) {
try {
$executor->deleteRuntime($project->getId(), $function->getAttribute('deploymentId'));
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
if ($th->getCode() !== 404) {
throw $th;
}
}
}
}
@@ -253,8 +271,7 @@ class Update extends Base
'entrypoint' => $entrypoint,
'commands' => $commands,
'scopes' => $scopes,
'deploymentRetention' => 0,
'startCommand' => '',
'deploymentRetention' => $deploymentRetention,
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getSequence(),
'providerRepositoryId' => $providerRepositoryId,
@@ -263,9 +280,8 @@ class Update extends Base
'providerBranch' => $providerBranch,
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification,
'buildSpecification' => $specification,
'runtimeSpecification' => $specification,
'buildSpecification' => $buildSpecification,
'runtimeSpecification' => $runtimeSpecification,
'search' => implode(' ', [$functionId, $name, $runtime]),
])));
@@ -105,7 +105,8 @@ class Create extends Base
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
$function->setAttribute('live', false);
$dbForProject->updateDocument('functions', $function->getId(), new Document(['live' => false]));
// Inform scheduler to pull the latest changes
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
@@ -113,7 +114,11 @@ class Create extends Base
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId')));
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([
'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'),
'schedule' => $schedule->getAttribute('schedule'),
'active' => $schedule->getAttribute('active'),
])));
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -11,6 +11,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
@@ -86,7 +87,8 @@ class Delete extends Base
$dbForProject->deleteDocument('variables', $variable->getId());
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
$function->setAttribute('live', false);
$dbForProject->updateDocument('functions', $function->getId(), new Document(['live' => false]));
// Inform scheduler to pull the latest changes
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
@@ -94,7 +96,11 @@ class Delete extends Base
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId')));
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([
'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'),
'schedule' => $schedule->getAttribute('schedule'),
'active' => $schedule->getAttribute('active'),
])));
$response->noContent();
}
@@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
@@ -99,12 +100,18 @@ class Update extends Base
->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function']));
try {
$dbForProject->updateDocument('variables', $variable->getId(), $variable);
$dbForProject->updateDocument('variables', $variable->getId(), new Document([
'key' => $key,
'value' => $value ?? $variable->getAttribute('value'),
'secret' => $secret ?? $variable->getAttribute('secret'),
'search' => implode(' ', [$variableId, $function->getId(), $key, 'function']),
]));
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
$function->setAttribute('live', false);
$dbForProject->updateDocument('functions', $function->getId(), new Document(['live' => false]));
// Inform scheduler to pull the latest changes
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
@@ -112,7 +119,11 @@ class Update extends Base
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId')));
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([
'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'),
'schedule' => $schedule->getAttribute('schedule'),
'active' => $schedule->getAttribute('active'),
])));
$response->dynamic($variable, Response::MODEL_VARIABLE);
}
@@ -255,7 +255,7 @@ class Builds extends Action
$version = $this->getVersion($resource);
$runtime = $this->getRuntime($resource, $version);
$spec = Config::getParam('specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
if ($resource->getCollection() === 'functions' && \is_null($runtime)) {
throw new \Exception('Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported');
@@ -279,7 +279,10 @@ class Builds extends Action
$deployment->setAttribute('buildStartedAt', $startTime);
$deployment->setAttribute('status', 'processing');
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
'buildStartedAt' => $startTime,
'status' => 'processing',
]));
if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) {
$resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')]));
@@ -366,7 +369,11 @@ class Builds extends Action
->setAttribute('sourcePath', $source)
->setAttribute('sourceSize', $directorySize)
->setAttribute('totalSize', $directorySize);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
'sourcePath' => $deployment->getAttribute('sourcePath'),
'sourceSize' => $deployment->getAttribute('sourceSize'),
'totalSize' => $deployment->getAttribute('totalSize'),
]));
$queueForRealtime
->setPayload($deployment->getArrayCopy())
@@ -480,7 +487,13 @@ class Builds extends Action
$deployment->setAttribute('providerCommitAuthor', APP_VCS_GITHUB_USERNAME);
$deployment->setAttribute('providerCommitMessage', "Create '" . $resource->getAttribute('name', '') . "' function");
$deployment->setAttribute('providerCommitUrl', "https://github.com/$cloneOwner/$cloneRepository/commit/$providerCommitHash");
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
'providerCommitHash' => $deployment->getAttribute('providerCommitHash'),
'providerCommitAuthorUrl' => $deployment->getAttribute('providerCommitAuthorUrl'),
'providerCommitAuthor' => $deployment->getAttribute('providerCommitAuthor'),
'providerCommitMessage' => $deployment->getAttribute('providerCommitMessage'),
'providerCommitUrl' => $deployment->getAttribute('providerCommitUrl'),
]));
$queueForRealtime
->setPayload($deployment->getArrayCopy())
@@ -528,7 +541,11 @@ class Builds extends Action
->setAttribute('sourcePath', $source)
->setAttribute('sourceSize', $directorySize)
->setAttribute('totalSize', $directorySize);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
'sourcePath' => $deployment->getAttribute('sourcePath'),
'sourceSize' => $deployment->getAttribute('sourceSize'),
'totalSize' => $deployment->getAttribute('totalSize'),
]));
$queueForRealtime
->setPayload($deployment->getArrayCopy())
@@ -543,7 +560,9 @@ class Builds extends Action
/** Request the executor to build the code... */
$deployment->setAttribute('status', 'building');
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
'status' => 'building',
]));
if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) {
$resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')]));
@@ -819,7 +838,9 @@ class Builds extends Action
if ($affected) {
$deployment = $deployment->setAttribute('buildLogs', $currentLogs);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
'buildLogs' => $currentLogs,
]));
$queueForRealtime
->setPayload($deployment->getArrayCopy())
@@ -905,7 +926,14 @@ class Builds extends Action
}
}
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
'buildPath' => $deployment->getAttribute('buildPath'),
'buildSize' => $deployment->getAttribute('buildSize'),
'totalSize' => $deployment->getAttribute('totalSize'),
'buildLogs' => $deployment->getAttribute('buildLogs'),
'adapter' => $deployment->getAttribute('adapter'),
'fallbackFile' => $deployment->getAttribute('fallbackFile'),
]));
$queueForRealtime
->setPayload($deployment->getArrayCopy())
->trigger();
@@ -916,12 +944,15 @@ class Builds extends Action
$logs = $deployment->getAttribute('buildLogs', '');
$date = \date('H:i:s');
$logs .= "[$date] [appwrite] Deployment finished. \n";
$logs .= "\033[90m[$date] \033[90m[\033[0mappwrite\033[90m]\033[32m Deployment finished. \033[0m\n";
$deployment->setAttribute('buildLogs', $logs);
/** Update the status */
$deployment->setAttribute('status', 'ready');
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment);
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
'buildLogs' => $deployment->getAttribute('buildLogs'),
'status' => 'ready',
]));
Console::log('Status marked as ready');
@@ -1109,7 +1140,11 @@ class Builds extends Action
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $resource->getAttribute('schedule'))
->setAttribute('active', !empty($resource->getAttribute('schedule')) && !empty($resource->getAttribute('deploymentId')));
$dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule);
$dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([
'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'),
'schedule' => $schedule->getAttribute('schedule'),
'active' => $schedule->getAttribute('active'),
]));
}
/** Screenshot site */
@@ -1160,7 +1195,12 @@ class Builds extends Action
$deployment->setAttribute('status', 'failed');
$deployment->setAttribute('buildLogs', $message);
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment);
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
'buildEndedAt' => $deployment->getAttribute('buildEndedAt'),
'buildDuration' => $deployment->getAttribute('buildDuration'),
'status' => 'failed',
'buildLogs' => $message,
]));
if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) {
$resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')]));
@@ -1189,7 +1229,7 @@ class Builds extends Action
protected function sendUsage(Document $resource, Document $deployment, Document $project, StatsUsage $queue): void
{
$spec = Config::getParam('specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
switch ($deployment->getAttribute('status')) {
case 'ready':
@@ -1467,7 +1507,9 @@ class Builds extends Action
$logs .= "[$date] [appwrite] Git action failed. Deployment will continue. \n";
$deployment->setAttribute('buildLogs', $logs);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
'buildLogs' => $deployment->getAttribute('buildLogs'),
]));
$queueForRealtime
->setPayload($deployment->getArrayCopy())
@@ -1483,10 +1525,12 @@ class Builds extends Action
$logs = $deployment->getAttribute('buildLogs', '');
$date = \date('H:i:s');
$logs .= "[$date] [appwrite] Build has been canceled. \n";
$logs .= "\033[90m[$date] \033[90m[\033[0mappwrite\033[90m]\033[33m Build has been canceled. \033[0m\n";
$deployment->setAttribute('buildLogs', $logs);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
'buildLogs' => $deployment->getAttribute('buildLogs'),
]));
$queueForRealtime
->setPayload($deployment->getArrayCopy())
@@ -9,6 +9,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
@@ -73,7 +74,7 @@ class Update extends Action
->setAttribute('name', $name)
->setAttribute('expire', $expire);
$dbForPlatform->updateDocument('devKeys', $key->getId(), $key);
$dbForPlatform->updateDocument('devKeys', $key->getId(), new Document(['name' => $name, 'expire' => $expire]));
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
@@ -11,6 +11,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator;
@@ -77,9 +78,9 @@ class Update extends Action
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$project->setAttribute('labels', (array) \array_values(\array_unique($labels)));
$labels = (array) \array_values(\array_unique($labels));
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project);
$project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document(['labels' => $labels]));
$response->dynamic($project, Response::MODEL_PROJECT);
}
@@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
@@ -72,34 +73,31 @@ class Update extends Action
$permissions = $this->getPermissions($teamId, $projectId);
$project
->setAttribute('teamId', $teamId)
->setAttribute('teamInternalId', $team->getSequence())
->setAttribute('$permissions', $permissions);
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project);
$project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'teamId' => $teamId,
'teamInternalId' => $team->getSequence(),
'$permissions' => $permissions,
]));
$installations = $dbForPlatform->find('installations', [
Query::equal('projectInternalId', [$project->getSequence()]),
]);
foreach ($installations as $installation) {
$installation->setAttribute('$permissions', $permissions);
$dbForPlatform->updateDocument('installations', $installation->getId(), $installation);
$dbForPlatform->updateDocument('installations', $installation->getId(), new Document(['$permissions' => $permissions]));
}
$repositories = $dbForPlatform->find('repositories', [
Query::equal('projectInternalId', [$project->getSequence()]),
]);
foreach ($repositories as $repository) {
$repository->setAttribute('$permissions', $permissions);
$dbForPlatform->updateDocument('repositories', $repository->getId(), $repository);
$dbForPlatform->updateDocument('repositories', $repository->getId(), new Document(['$permissions' => $permissions]));
}
$vcsComments = $dbForPlatform->find('vcsComments', [
Query::equal('projectInternalId', [$project->getSequence()]),
]);
foreach ($vcsComments as $vcsComment) {
$vcsComment->setAttribute('$permissions', $permissions);
$dbForPlatform->updateDocument('vcsComments', $vcsComment->getId(), $vcsComment);
$dbForPlatform->updateDocument('vcsComments', $vcsComment->getId(), new Document(['$permissions' => $permissions]));
}
$response->dynamic($project, Response::MODEL_PROJECT);
@@ -237,7 +237,7 @@ class Create extends Action
foreach ($activeDeployments as $activeDeployment) {
$activeDeployment->setAttribute('activate', false);
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), $activeDeployment);
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), new Document(['activate' => false]));
}
}
@@ -272,7 +272,12 @@ class Create extends Action
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), $site);
$dbForProject->updateDocument('sites', $site->getId(), new Document([
'latestDeploymentId' => $deployment->getId(),
'latestDeploymentInternalId' => $deployment->getSequence(),
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
$sitesDomain = $platform['sitesDomain'];
$domain = ID::unique() . "." . $sitesDomain;
@@ -302,7 +307,10 @@ class Create extends Action
]))
);
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceSize', $fileSize)->setAttribute('sourceMetadata', $metadata));
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
'sourceSize' => $fileSize,
'sourceMetadata' => $metadata,
]));
}
// Start the build
@@ -342,7 +350,12 @@ class Create extends Action
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), $site);
$dbForProject->updateDocument('sites', $site->getId(), new Document([
'latestDeploymentId' => $site->getAttribute('latestDeploymentId'),
'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'),
'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'),
'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'),
]));
$sitesDomain = $platform['sitesDomain'];
$domain = ID::unique() . "." . $sitesDomain;
@@ -368,7 +381,10 @@ class Create extends Action
]))
);
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceChunksUploaded', $chunksUploaded)->setAttribute('sourceMetadata', $metadata));
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
'sourceChunksUploaded' => $chunksUploaded,
'sourceMetadata' => $metadata,
]));
}
}
@@ -107,11 +107,12 @@ class Delete extends Action
$site = $dbForProject->updateDocument(
'sites',
$site->getId(),
$site
->setAttribute('latestDeploymentCreatedAt', $latestDeployment->isEmpty() ? '' : $latestDeployment->getCreatedAt())
->setAttribute('latestDeploymentInternalId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getSequence())
->setAttribute('latestDeploymentId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getId())
->setAttribute('latestDeploymentStatus', $latestDeployment->isEmpty() ? '' : $latestDeployment->getAttribute('status', ''))
new Document([
'latestDeploymentCreatedAt' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getCreatedAt(),
'latestDeploymentInternalId' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getSequence(),
'latestDeploymentId' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getId(),
'latestDeploymentStatus' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getAttribute('status', ''),
])
);
}
@@ -142,7 +142,12 @@ class Create extends Action
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), $site);
$dbForProject->updateDocument('sites', $site->getId(), new Document([
'latestDeploymentId' => $site->getAttribute('latestDeploymentId'),
'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'),
'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'),
'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'),
]));
// Preview deployments for sites
$sitesDomain = $platform['sitesDomain'];
@@ -89,7 +89,7 @@ class Update extends Action
$endTime = new \DateTime('now');
$duration = $endTime->getTimestamp() - $startTime->getTimestamp();
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttributes([
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
'buildEndedAt' => DateTime::now(),
'buildDuration' => $duration,
'status' => 'canceled'
@@ -97,7 +97,9 @@ class Update extends Action
if ($deployment->getSequence() === $site->getAttribute('latestDeploymentInternalId', '')) {
$site = $site->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), $site);
$dbForProject->updateDocument('sites', $site->getId(), new Document([
'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'),
]));
}
try {
@@ -187,7 +187,12 @@ class Create extends Base
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), $site);
$dbForProject->updateDocument('sites', $site->getId(), new Document([
'latestDeploymentId' => $site->getAttribute('latestDeploymentId'),
'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'),
'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'),
'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'),
]));
$sitesDomain = $platform['sitesDomain'];
$domain = ID::unique() . "." . $sitesDomain;
@@ -71,6 +71,16 @@ class Get extends Base
throw new Exception(Exception::LOG_NOT_FOUND);
}
// Override status in response if the log is stuck in waiting/processing beyond the site timeout.
$status = $log->getAttribute('status', '');
if ($status === 'waiting' || $status === 'processing') {
$timeout = $site->getAttribute('timeout', 30);
$elapsed = \time() - \strtotime($log->getCreatedAt());
if ($elapsed >= $timeout) {
$log->setAttribute('status', 'failed');
}
}
$response->dynamic($log, Response::MODEL_EXECUTION); //TODO: Change to model log, but model log already exists - decide what to do
}
}
@@ -11,6 +11,7 @@ use Appwrite\Utopia\Database\Validator\Queries\Executions;
use Appwrite\Utopia\Database\Validator\Queries\Logs;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
@@ -99,6 +100,35 @@ class XList extends Base
$cursor->setValue($cursorDocument);
}
// Calculate the cutoff datetime before which a waiting/processing log is considered timed out.
$timeout = $site->getAttribute('timeout', 30);
$thresholdDate = new \DateTime("-{$timeout} seconds");
$threshold = DateTime::format($thresholdDate);
// Capture what statuses the caller explicitly requested, before we mutate the query.
$requestedStatuses = [];
foreach ($queries as $query) {
if ($query->getMethod() === Query::TYPE_EQUAL && $query->getAttribute() === 'status') {
$requestedStatuses = [...$requestedStatuses, ...$query->getValues()];
}
}
// If the caller is filtering by 'failed', expand the DB query to also return
// waiting/processing logs created before the timeout threshold, so timed-out
// logs that were never marked failed in the DB are included in the results.
foreach ($queries as $index => $query) {
if ($query->getMethod() === Query::TYPE_EQUAL && $query->getAttribute() === 'status' && \in_array('failed', $query->getValues())) {
$queries[$index] = Query::or([
$query,
Query::and([
Query::equal('status', ['waiting', 'processing']),
Query::createdBefore($threshold),
]),
]);
break;
}
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
@@ -108,6 +138,20 @@ class XList extends Base
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
// Override status in response for timed-out logs, but only when the caller
// did not explicitly request a non-failed status (e.g. waiting/processing).
if (empty(\array_diff($requestedStatuses, ['failed']))) {
foreach ($results as $log) {
$status = $log->getAttribute('status', '');
if ($status === 'waiting' || $status === 'processing') {
$elapsed = \time() - \strtotime($log->getCreatedAt());
if ($elapsed >= $timeout) {
$log->setAttribute('status', 'failed');
}
}
}
}
$response->dynamic(new Document([
'executions' => $results,
'total' => $total,
@@ -14,6 +14,7 @@ use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -67,6 +68,7 @@ class Create extends Base
->param('timeout', 30, new Range(1, (int) System::getEnv('_APP_SITES_TIMEOUT', 30)), 'Maximum request time in seconds.', true)
->param('installCommand', '', new Text(8192, 0), 'Install Command.', true)
->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true)
->param('startCommand', '', new Text(8192, 0), 'Custom start command. Leave empty to use default.', true)
->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true)
->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.')
->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true)
@@ -76,12 +78,19 @@ class Create extends Base
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true)
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true)
->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true)
->param('specification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Framework specification for the site and builds.', true, ['plan'])
), 'Build specification for the site deployments.', true, ['plan'])
->param('runtimeSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Runtime specification for the SSR executions.', true, ['plan'])
->param('deploymentRetention', 0, new Range(0, APP_COMPUTE_DEPLOYMENT_MAX_RETENTION), 'Days to keep non-active deployments before deletion. Value 0 means all deployments will be kept.', true)
->inject('response')
->inject('dbForProject')
->inject('project')
@@ -99,6 +108,7 @@ class Create extends Base
int $timeout,
string $installCommand,
string $buildCommand,
string $startCommand,
string $outputDirectory,
string $buildRuntime,
string $adapter,
@@ -108,7 +118,9 @@ class Create extends Base
string $providerBranch,
bool $providerSilentMode,
string $providerRootDirectory,
string $specification,
string $buildSpecification,
string $runtimeSpecification,
int $deploymentRetention,
Response $response,
Database $dbForProject,
Document $project,
@@ -136,20 +148,20 @@ class Create extends Base
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".');
}
$site = $dbForProject->createDocument('sites', new Document([
$site = new Document([
'$id' => $siteId,
'enabled' => $enabled,
'live' => true,
'logging' => $logging,
'name' => $name,
'framework' => $framework,
'deploymentRetention' => $deploymentRetention,
'deploymentInternalId' => '',
'deploymentId' => '',
'timeout' => $timeout,
'installCommand' => $installCommand,
'buildCommand' => $buildCommand,
'deploymentRetention' => 0,
'startCommand' => '',
'startCommand' => $startCommand,
'outputDirectory' => $outputDirectory,
'search' => implode(' ', [$siteId, $name, $framework]),
'fallbackFile' => $fallbackFile,
@@ -161,18 +173,21 @@ class Create extends Base
'providerBranch' => $providerBranch,
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification,
'buildSpecification' => $specification,
'runtimeSpecification' => $specification,
'buildSpecification' => $buildSpecification,
'runtimeSpecification' => $runtimeSpecification,
'buildRuntime' => $buildRuntime,
'adapter' => $adapter,
]));
]);
try {
$site = $dbForProject->createDocument('sites', $site);
} catch (DuplicateException) {
throw new Exception(Exception::SITE_ALREADY_EXISTS);
}
// Git connect logic
if (!empty($providerRepositoryId)) {
$teamId = $project->getAttribute('teamId', '');
$repository = $dbForPlatform->createDocument('repositories', new Document([
$repository = new Document([
'$id' => ID::unique(),
'$permissions' => $this->getPermissions($teamId, $project->getId()),
'installationId' => $installation->getId(),
@@ -184,13 +199,16 @@ class Create extends Base
'resourceInternalId' => $site->getSequence(),
'resourceType' => 'site',
'providerPullRequestIds' => []
]));
]);
$repository = $dbForPlatform->createDocument('repositories', $repository);
$site->setAttribute('repositoryId', $repository->getId());
$site->setAttribute('repositoryInternalId', $repository->getSequence());
}
$site = $dbForProject->updateDocument('sites', $site->getId(), $site);
$site = $dbForProject->updateDocument('sites', $site->getId(), new Document([
'repositoryId' => $repository->getId(),
'repositoryInternalId' => $repository->getSequence(),
]));
}
$queueForEvents->setParam('siteId', $site->getId());
@@ -111,7 +111,10 @@ class Update extends Base
->setAttribute('deploymentId', $deployment->getId())
->setAttribute('deploymentInternalId', $deployment->getSequence());
$authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), $rule));
$authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
'deploymentId' => $rule->getAttribute('deploymentId'),
'deploymentInternalId' => $rule->getAttribute('deploymentInternalId'),
])));
}, $queries));
$queueForEvents
@@ -71,6 +71,7 @@ class Update extends Base
->param('timeout', 30, new Range(1, (int) System::getEnv('_APP_SITES_TIMEOUT', 30)), 'Maximum request time in seconds.', true)
->param('installCommand', '', new Text(8192, 0), 'Install Command.', true)
->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true)
->param('startCommand', '', new Text(8192, 0), 'Custom start command. Leave empty to use default.', true)
->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true)
->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.', true)
->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true)
@@ -80,12 +81,19 @@ class Update extends Base
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true)
->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true)
->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true)
->param('specification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
->param('buildSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Framework specification for the site and builds.', true, ['plan'])
), 'Build specification for the site deployments.', true, ['plan'])
->param('runtimeSpecification', fn (array $plan) => $this->getDefaultSpecification($plan), fn (array $plan) => new Specification(
$plan,
Config::getParam('specifications', []),
System::getEnv('_APP_COMPUTE_CPUS', 0),
System::getEnv('_APP_COMPUTE_MEMORY', 0)
), 'Runtime specification for the SSR executions.', true, ['plan'])
->param('deploymentRetention', 0, new Range(0, APP_COMPUTE_DEPLOYMENT_MAX_RETENTION), 'Days to keep non-active deployments before deletion. Value 0 means all deployments will be kept.', true)
->inject('request')
->inject('response')
->inject('dbForProject')
@@ -107,6 +115,7 @@ class Update extends Base
int $timeout,
string $installCommand,
string $buildCommand,
string $startCommand,
string $outputDirectory,
string $buildRuntime,
string $adapter,
@@ -116,7 +125,9 @@ class Update extends Base
string $providerBranch,
bool $providerSilentMode,
string $providerRootDirectory,
string $specification,
string $buildSpecification,
string $runtimeSpecification,
int $deploymentRetention,
Request $request,
Response $response,
Database $dbForProject,
@@ -190,11 +201,9 @@ class Update extends Base
$repositoryInternalId = '';
}
// Git connect logic
if (!$isConnected && !empty($providerRepositoryId)) {
$teamId = $project->getAttribute('teamId', '');
$repository = $dbForPlatform->createDocument('repositories', new Document([
$repository = new Document([
'$id' => ID::unique(),
'$permissions' => $this->getPermissions($teamId, $project->getId()),
'installationId' => $installation->getId(),
@@ -206,8 +215,8 @@ class Update extends Base
'resourceInternalId' => $site->getSequence(),
'resourceType' => 'site',
'providerPullRequestIds' => []
]));
]);
$repository = $dbForPlatform->createDocument('repositories', $repository);
$repositoryId = $repository->getId();
$repositoryInternalId = $repository->getSequence();
}
@@ -218,6 +227,7 @@ class Update extends Base
$site->getAttribute('name') !== $name ||
$site->getAttribute('buildCommand') !== $buildCommand ||
$site->getAttribute('installCommand') !== $installCommand ||
$site->getAttribute('startCommand') !== $startCommand ||
$site->getAttribute('outputDirectory') !== $outputDirectory ||
$site->getAttribute('providerRootDirectory') !== $providerRootDirectory ||
$site->getAttribute('framework') !== $framework
@@ -225,14 +235,22 @@ class Update extends Base
$live = false;
}
// Enforce Cold Start if spec limits change.
if ($site->getAttribute('specification') !== $specification && !empty($site->getAttribute('deploymentId'))) {
try {
$executor->deleteRuntime($project->getId(), $site->getAttribute('deploymentId'));
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
if ($th->getCode() !== 404) {
throw $th;
if (!empty($site->getAttribute('deploymentId'))) {
$specsChanged = false;
if ($site->getAttribute('runtimeSpecification', '') !== $runtimeSpecification) {
$specsChanged = true;
} elseif ($site->getAttribute('buildSpecification', '') !== $buildSpecification) {
$specsChanged = true;
}
if ($specsChanged) {
try {
$executor->deleteRuntime($project->getId(), $site->getAttribute('deploymentId'));
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
if ($th->getCode() !== 404) {
throw $th;
}
}
}
}
@@ -244,10 +262,10 @@ class Update extends Base
'logging' => $logging,
'live' => $live,
'timeout' => $timeout,
'deploymentRetention' => $deploymentRetention,
'installCommand' => $installCommand,
'buildCommand' => $buildCommand,
'deploymentRetention' => 0,
'startCommand' => '',
'startCommand' => $startCommand,
'outputDirectory' => $outputDirectory,
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getSequence(),
@@ -257,9 +275,8 @@ class Update extends Base
'providerBranch' => $providerBranch,
'providerRootDirectory' => $providerRootDirectory,
'providerSilentMode' => $providerSilentMode,
'specification' => $specification,
'buildSpecification' => $specification,
'runtimeSpecification' => $specification,
'buildSpecification' => $buildSpecification,
'runtimeSpecification' => $runtimeSpecification,
'search' => implode(' ', [$siteId, $name, $framework]),
'buildRuntime' => $buildRuntime,
'adapter' => $adapter,
@@ -92,7 +92,9 @@ class Create extends Base
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
$dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false));
$dbForProject->updateDocument('sites', $site->getId(), new Document([
'live' => false,
]));
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -10,6 +10,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -76,7 +77,9 @@ class Delete extends Base
$dbForProject->deleteDocument('variables', $variable->getId());
$dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false));
$dbForProject->updateDocument('sites', $site->getId(), new Document([
'live' => false,
]));
$response->noContent();
}
@@ -9,6 +9,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
@@ -93,12 +94,19 @@ class Update extends Base
->setAttribute('search', implode(' ', [$variableId, $site->getId(), $key, 'site']));
try {
$dbForProject->updateDocument('variables', $variable->getId(), $variable);
$dbForProject->updateDocument('variables', $variable->getId(), new Document([
'key' => $variable->getAttribute('key'),
'value' => $variable->getAttribute('value'),
'secret' => $variable->getAttribute('secret'),
'search' => $variable->getAttribute('search'),
]));
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
$dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false));
$dbForProject->updateDocument('sites', $site->getId(), new Document([
'live' => false,
]));
$response->dynamic($variable, Response::MODEL_VARIABLE);
}
@@ -266,17 +266,22 @@ class Create extends Action
$authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
}
} elseif ($membership->getAttribute('confirm') === false) {
$membership->setAttribute('secret', $proofForToken->hash($secret));
$membership->setAttribute('invited', DateTime::now());
$secretHash = $proofForToken->hash($secret);
$invitedTime = DateTime::now();
if ($isPrivilegedUser || $isAppUser) {
$membership->setAttribute('joined', DateTime::now());
$membership->setAttribute('confirm', true);
$membership = $authorization->skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), new Document([
'secret' => $secretHash,
'invited' => $invitedTime,
'joined' => DateTime::now(),
'confirm' => true
])));
} else {
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), new Document([
'secret' => $secretHash,
'invited' => $invitedTime
]));
}
$membership = ($isPrivilegedUser || $isAppUser) ?
$authorization->skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) :
$dbForProject->updateDocument('memberships', $membership->getId(), $membership);
} else {
throw new Exception(Exception::MEMBERSHIP_ALREADY_CONFIRMED);
}
@@ -131,7 +131,10 @@ class Delete extends Action
if (!$membership->isEmpty()) {
$team->setAttribute('userId', $membership->getAttribute('userId'));
$team->setAttribute('userInternalId', $membership->getAttribute('userInternalId'));
$dbForProject->updateDocument('teams', $team->getId(), $team);
$dbForProject->updateDocument('teams', $team->getId(), new Document([
'userId' => $membership->getAttribute('userId'),
'userInternalId' => $membership->getAttribute('userInternalId'),
]));
}
}
@@ -189,7 +189,7 @@ class Update extends Action
;
}
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership);
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), new Document(['joined' => $membership->getAttribute('joined'), 'confirm' => true]));
$dbForProject->purgeCachedDocument('users', $user->getId());
@@ -116,7 +116,7 @@ class Update extends Action
* Update the roles
*/
$membership->setAttribute('roles', $roles);
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership);
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), new Document(['roles' => $roles]));
/**
* Replace membership on profile
@@ -10,6 +10,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
@@ -67,7 +68,7 @@ class Update extends Action
->setAttribute('name', $name)
->setAttribute('search', implode(' ', [$teamId, $name]));
$team = $dbForProject->updateDocument('teams', $team->getId(), $team);
$team = $dbForProject->updateDocument('teams', $team->getId(), new Document(['name' => $name, 'search' => implode(' ', [$teamId, $name])]));
$queueForEvents->setParam('teamId', $team->getId());
@@ -0,0 +1,140 @@
<?php
namespace Appwrite\Platform\Modules\VCS\Http\GitHub\Authorize\External;
use Appwrite\Event\Build;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\Platform\Modules\VCS\Http\GitHub\Deployment;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\RepositoryNotFound;
class Update extends Action
{
use HTTP;
use Deployment;
public static function getName()
{
return 'updateExternalDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/vcs/github/installations/:installationId/repositories/:repositoryId')
->desc('Update external deployment (authorize)')
->groups(['api', 'vcs'])
->label('scope', 'vcs.write')
->label('sdk', new Method(
namespace: 'vcs',
group: 'repositories',
name: 'updateExternalDeployments',
description: '/docs/references/vcs/update-external-deployments.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
]
))
->param('installationId', '', new Text(256), 'Installation Id')
->param('repositoryId', '', new Text(256), 'VCS Repository Id')
->param('providerPullRequestId', '', new Text(256), 'GitHub Pull Request Id')
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->inject('getProjectDB')
->inject('queueForBuilds')
->inject('platform')
->callback($this->action(...));
}
public function action(
string $installationId,
string $repositoryId,
string $providerPullRequestId,
GitHub $github,
Response $response,
Document $project,
Database $dbForPlatform,
Authorization $authorization,
callable $getProjectDB,
Build $queueForBuilds,
array $platform
) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$repository = $authorization->skip(fn () => $dbForPlatform->findOne('repositories', [
Query::equal('$id', [$repositoryId]),
Query::equal('projectInternalId', [$project->getSequence()])
]));
if ($repository->isEmpty()) {
throw new Exception(Exception::REPOSITORY_NOT_FOUND);
}
if (\in_array($providerPullRequestId, $repository->getAttribute('providerPullRequestIds', []))) {
throw new Exception(Exception::PROVIDER_CONTRIBUTION_CONFLICT);
}
$providerPullRequestIds = \array_unique(\array_merge($repository->getAttribute('providerPullRequestIds', []), [$providerPullRequestId]));
$repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), new Document(['providerPullRequestIds' => $providerPullRequestIds])));
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$providerInstallationId = $installation->getAttribute('providerInstallationId');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$repositories = [$repository];
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$providerRepositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($providerRepositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$owner = $github->getOwnerName($providerInstallationId);
$pullRequestResponse = $github->getPullRequest($owner, $providerRepositoryName, $providerPullRequestId);
$providerRepositoryUrl = $pullRequestResponse['head']['repo']['html_url'] ?? '';
$providerRepositoryOwner = $pullRequestResponse['head']['repo']['owner']['login'] ?? '';
$providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? '';
$providerBranchUrl = "$providerRepositoryUrl/tree/$providerBranch";
$providerCommitHash = $pullRequestResponse['head']['sha'] ?? '';
$commitDetails = $github->getCommit($providerRepositoryOwner, $providerRepositoryName, $providerCommitHash);
$providerCommitMessage = $commitDetails["commitMessage"] ?? '';
$providerCommitUrl = $commitDetails["commitUrl"] ?? '';
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
$providerCommitAuthorUrl = $commitDetails["commitAuthorUrl"] ?? '';
$this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform);
$response->noContent();
}
}
@@ -4,13 +4,12 @@ namespace Appwrite\Platform\Modules\VCS\Http\GitHub\Callback;
use Appwrite\Auth\OAuth2\Github as OAuth2Github;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Permission as AppwritePermission;
use Appwrite\Utopia\Response;
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\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -21,6 +20,7 @@ use Utopia\VCS\Adapter\Git\GitHub;
class Get extends Action
{
use HTTP;
use AppwritePermission;
public static function getName()
{
@@ -132,13 +132,7 @@ class Get extends Action
$installation = 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')),
],
'$permissions' => $this->getPermissions($teamId, $projectId),
'providerInstallationId' => $providerInstallationId,
'projectId' => $projectId,
'projectInternalId' => $projectInternalId,
@@ -158,7 +152,13 @@ class Get extends Action
->setAttribute('personalRefreshToken', $refreshToken)
->setAttribute('personalAccessToken', $accessToken)
->setAttribute('personalAccessTokenExpiry', $accessTokenExpiry);
$installation = $dbForPlatform->updateDocument('installations', $installation->getId(), $installation);
$installation = $dbForPlatform->updateDocument('installations', $installation->getId(), new Document([
'organization' => $installation->getAttribute('organization'),
'personal' => $installation->getAttribute('personal'),
'personalRefreshToken' => $installation->getAttribute('personalRefreshToken'),
'personalAccessToken' => $installation->getAttribute('personalAccessToken'),
'personalAccessTokenExpiry' => $installation->getAttribute('personalAccessTokenExpiry'),
]));
}
} else {
$error = 'Installation of the Appwrite GitHub App on organization accounts is restricted to organization owners. As a member of the organization, you do not have the necessary permissions to install this GitHub App. Please contact the organization owner to create the installation from the Appwrite console.';
@@ -0,0 +1,528 @@
<?php
namespace Appwrite\Platform\Modules\VCS\Http\GitHub;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Filter\BranchDomain as BranchDomainFilter;
use Appwrite\Vcs\Comment;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\Span\Span;
use Utopia\System\System;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\RepositoryNotFound;
trait Deployment
{
protected function createGitDeployments(
GitHub $github,
string $providerInstallationId,
array $repositories,
string $providerBranch,
string $providerBranchUrl,
string $providerRepositoryName,
string $providerRepositoryUrl,
string $providerRepositoryOwner,
string $providerCommitHash,
string $providerCommitAuthor,
string $providerCommitAuthorUrl,
string $providerCommitMessage,
string $providerCommitUrl,
string $providerPullRequestId,
bool $external,
Database $dbForPlatform,
Authorization $authorization,
Build $queueForBuilds,
callable $getProjectDB,
array $platform,
) {
$errors = [];
foreach ($repositories as $repository) {
try {
$repositoryId = $repository->getId();
$projectId = $repository->getAttribute('projectId');
$resourceId = $repository->getAttribute('resourceId');
$resourceType = $repository->getAttribute('resourceType');
$logBase = "vcs.github.event.repo.{$repositoryId}";
Span::add("{$logBase}.projectId", $projectId);
Span::add("{$logBase}.resourceId", $resourceId);
Span::add("{$logBase}.resourceType", $resourceType);
if ($resourceType !== "function" && $resourceType !== "site") {
continue;
}
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND, 'Repository references non-existent project');
}
try {
$dsn = new DSN($project->getAttribute('database'));
$databaseName = $dsn->getHost();
} catch (\InvalidArgumentException) {
$databaseName = $project->getAttribute('database');
}
$databases = Config::getParam('pools-database', []);
$index = in_array($databaseName, $databases);
if ($index === false) {
Console::error("Database: '{$databaseName}' is not part of region: " . System::getEnv('_APP_REGION'));
continue;
}
$dbForProject = $getProjectDB($project);
$resourceCollection = $resourceType === "function" ? 'functions' : 'sites';
$resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
$resourceInternalId = $resource->getSequence();
$deploymentId = ID::unique();
$repositoryId = $repository->getId();
$repositoryInternalId = $repository->getSequence();
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
$installationId = $repository->getAttribute('installationId');
$installationInternalId = $repository->getAttribute('installationInternalId');
$productionBranch = $resource->getAttribute('providerBranch');
$activate = false;
if ($providerBranch == $productionBranch && $external === false) {
$activate = true;
}
$owner = $github->getOwnerName($providerInstallationId) ?? '';
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$isAuthorized = !$external;
if (!$isAuthorized && !empty($providerPullRequestId)) {
if (\in_array($providerPullRequestId, $repository->getAttribute('providerPullRequestIds', []))) {
$isAuthorized = true;
}
}
Span::add("{$logBase}.authorized", $isAuthorized);
$commentStatus = 'waiting';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = $platform['consoleHostname'] ?? '';
$authorizeUrl = $protocol . '://' . $hostname . "/console/git/authorize-contributor?projectId={$projectId}&installationId={$installationId}&repositoryId={$repositoryId}&providerPullRequestId={$providerPullRequestId}";
$action = $isAuthorized ? ['type' => 'logs'] : ['type' => 'authorize', 'url' => $authorizeUrl];
$latestCommentId = '';
if (!empty($providerPullRequestId) && $resource->getAttribute('providerSilentMode', false) === false) {
$latestComment = $authorization->skip(fn () => $dbForPlatform->findOne('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerPullRequestId', [$providerPullRequestId]),
Query::orderDesc('$createdAt'),
]));
if (!$latestComment->isEmpty()) {
$latestCommentId = $latestComment->getAttribute('providerCommentId', '');
$retries = 0;
$lockAcquired = false;
while ($retries < 9) {
$retries++;
try {
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
'$id' => $latestCommentId
]));
$lockAcquired = true;
break;
} catch (\Throwable $err) {
if ($retries >= 9) {
Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage());
}
\sleep(1);
}
}
if ($lockAcquired) {
// Wrap in try/finally to ensure lock file gets deleted
try {
$comment = new Comment($platform);
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
} else {
$comment = new Comment($platform);
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
$latestCommentId = \strval($github->createComment($owner, $repositoryName, $providerPullRequestId, $comment->generateComment()));
if (!empty($latestCommentId)) {
$teamId = $project->getAttribute('teamId', '');
$latestComment = $authorization->skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'installationInternalId' => $installationInternalId,
'installationId' => $installationId,
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'providerRepositoryId' => $providerRepositoryId,
'providerBranch' => $providerBranch,
'providerPullRequestId' => $providerPullRequestId,
'providerCommentId' => $latestCommentId
])));
}
}
} elseif (!empty($providerBranch)) {
$latestComments = $authorization->skip(fn () => $dbForPlatform->find('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerBranch', [$providerBranch]),
Query::orderDesc('$createdAt'),
]));
foreach ($latestComments as $comment) {
$latestCommentId = $comment->getAttribute('providerCommentId', '');
$retries = 0;
$lockAcquired = false;
while ($retries < 9) {
$retries++;
try {
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
'$id' => $latestCommentId
]));
$lockAcquired = true;
break;
} catch (\Throwable $err) {
if ($retries >= 9) {
Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage());
}
\sleep(1);
}
}
if ($lockAcquired) {
// Wrap in try/finally to ensure lock file gets deleted
try {
$comment = new Comment($platform);
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
}
}
if (!$isAuthorized) {
$resourceName = $resource->getAttribute('name');
$projectName = $project->getAttribute('name');
$name = "{$resourceName} ({$projectName})";
$message = 'Authorization required for external contributor.';
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$owner = $github->getOwnerName($providerInstallationId);
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'pending', $message, $authorizeUrl, $name);
continue;
}
$commands = [];
if (!empty($resource->getAttribute('installCommand', ''))) {
$commands[] = $resource->getAttribute('installCommand', '');
}
if (!empty($resource->getAttribute('buildCommand', ''))) {
$commands[] = $resource->getAttribute('buildCommand', '');
}
if (!empty($resource->getAttribute('commands', ''))) {
$commands[] = $resource->getAttribute('commands', '');
}
$deployment = $authorization->skip(fn () => $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceId' => $resourceId,
'resourceInternalId' => $resourceInternalId,
'resourceType' => $resourceCollection,
'entrypoint' => $resource->getAttribute('entrypoint', ''),
'buildCommands' => \implode(' && ', $commands),
'startCommand' => $resource->getAttribute('startCommand', ''),
'buildOutput' => $resource->getAttribute('outputDirectory', ''),
'adapter' => $resource->getAttribute('adapter', ''),
'fallbackFile' => $resource->getAttribute('fallbackFile', ''),
'type' => 'vcs',
'installationId' => $installationId,
'installationInternalId' => $installationInternalId,
'providerRepositoryId' => $providerRepositoryId,
'repositoryId' => $repositoryId,
'repositoryInternalId' => $repositoryInternalId,
'providerBranchUrl' => $providerBranchUrl,
'providerRepositoryName' => $providerRepositoryName,
'providerRepositoryOwner' => $providerRepositoryOwner,
'providerRepositoryUrl' => $providerRepositoryUrl,
'providerCommitHash' => $providerCommitHash,
'providerCommitAuthorUrl' => $providerCommitAuthorUrl,
'providerCommitAuthor' => $providerCommitAuthor,
'providerCommitMessage' => mb_strimwidth($providerCommitMessage, 0, 255, '...'),
'providerCommitUrl' => $providerCommitUrl,
'providerCommentId' => \strval($latestCommentId),
'providerBranch' => $providerBranch,
'activate' => $activate,
])));
$resource = $resource
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$authorization->skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document([
'latestDeploymentId' => $resource->getAttribute('latestDeploymentId'),
'latestDeploymentInternalId' => $resource->getAttribute('latestDeploymentInternalId'),
'latestDeploymentCreatedAt' => $resource->getAttribute('latestDeploymentCreatedAt'),
'latestDeploymentStatus' => $resource->getAttribute('latestDeploymentStatus'),
])));
if ($resource->getCollection() === 'sites') {
$projectId = $project->getId();
// Deployment preview
$sitesDomain = $platform['sitesDomain'];
$domain = ID::unique() . "." . $sitesDomain;
$ruleId = md5($domain);
$previewRuleId = $ruleId;
$authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getSequence(),
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $resourceId,
'deploymentResourceInternalId' => $resourceInternalId,
'deploymentVcsProviderBranch' => $providerBranch,
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
// VCS branch preview
if (!empty($providerBranch)) {
$domain = (new BranchDomainFilter())->apply([
'branch' => $providerBranch,
'resourceId' => $resource->getId(),
'projectId' => $project->getId(),
'sitesDomain' => $sitesDomain,
]);
$ruleId = md5($domain);
try {
$authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getSequence(),
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $resourceId,
'deploymentResourceInternalId' => $resourceInternalId,
'deploymentVcsProviderBranch' => $providerBranch,
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
} catch (Duplicate $err) {
// Ignore, rule already exists; will be updated by builds worker
}
}
// VCS commit preview
if (!empty($providerCommitHash)) {
$domain = "commit-" . substr($providerCommitHash, 0, 16) . ".{$sitesDomain}";
$ruleId = md5($domain);
try {
$authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getSequence(),
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $resourceId,
'deploymentResourceInternalId' => $resourceInternalId,
'deploymentVcsProviderBranch' => $providerBranch,
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
} catch (Duplicate $err) {
// Ignore, rule already exists; will be updated by builds worker
}
}
}
if ($resource->getCollection() === 'sites' && !empty($latestCommentId) && !empty($previewRuleId)) {
$retries = 0;
$lockAcquired = false;
while ($retries < 9) {
$retries++;
try {
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
'$id' => $latestCommentId
]));
$lockAcquired = true;
break;
} catch (\Throwable $err) {
if ($retries >= 9) {
Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage());
}
\sleep(1);
}
}
if ($lockAcquired) {
// Wrap in try/finally to ensure lock file gets deleted
try {
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
if (!empty($previewUrl)) {
$comment = new Comment($platform);
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, $previewUrl);
$github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
}
} finally {
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
}
if (!empty($providerCommitHash) && $resource->getAttribute('providerSilentMode', false) === false) {
$resourceName = $resource->getAttribute('name');
$projectName = $project->getAttribute('name');
$region = $project->getAttribute('region', 'default');
$name = "{$resourceName} ({$projectName})";
$message = 'Starting...';
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$owner = $github->getOwnerName($providerInstallationId);
$providerTargetUrl = $protocol . '://' . $hostname . "/console/project-$region-$projectId/$resourceCollection/$resourceType-$resourceId";
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'pending', $message, $providerTargetUrl, $name);
}
$queueName = $this->getBuildQueueName($project, $dbForPlatform, $authorization);
$queueForBuilds
->setQueue($queueName)
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($resource)
->setDeployment($deployment)
->setProject($project); // set the project because it won't be set for git deployments
$queueForBuilds->trigger(); // must trigger here so that we create a build for each function/site
Span::add("{$logBase}.build.triggered", 'true');
//TODO: Add event?
} catch (\Throwable $e) {
Span::add("{$logBase}.error", $e->getMessage());
$errors[] = $e->getMessage();
}
}
$queueForBuilds->reset(); // prevent shutdown hook from triggering again
if (!empty($errors)) {
throw new Exception(Exception::GENERAL_UNKNOWN, \implode("\n", $errors));
}
}
protected function getBuildQueueName(Document $project, Database $dbForPlatform, Authorization $authorization): string
{
return System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME);
}
}
@@ -0,0 +1,245 @@
<?php
namespace Appwrite\Platform\Modules\VCS\Http\GitHub\Events;
use Appwrite\Event\Build;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\Platform\Modules\VCS\Http\GitHub\Deployment;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Scope\HTTP;
use Utopia\Span\Span;
use Utopia\System\System;
use Utopia\VCS\Adapter\Git\GitHub;
class Create extends Action
{
use HTTP;
use Deployment;
public static function getName()
{
return 'createVCSGitHubEvent';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/vcs/github/events')
->desc('Create event')
->groups(['api', 'vcs'])
->label('scope', 'public')
->inject('gitHub')
->inject('request')
->inject('response')
->inject('dbForPlatform')
->inject('authorization')
->inject('getProjectDB')
->inject('queueForBuilds')
->inject('platform')
->callback($this->action(...));
}
public function action(
GitHub $github,
Request $request,
Response $response,
Database $dbForPlatform,
Authorization $authorization,
callable $getProjectDB,
Build $queueForBuilds,
array $platform
) {
$this->preprocessEvent($request);
$event = $request->getHeader('x-github-event', '');
Span::add('vcs.github.event.name', $event);
$payload = $request->getRawPayload();
$signature = $request->getHeader('x-hub-signature-256', '');
$secretKey = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', '');
$valid = empty($signature) ? true : $github->validateWebhookEvent($payload, $signature, $secretKey);
Span::add('vcs.github.event.signature.valid', $valid);
if (!$valid) {
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, "Invalid webhook payload signature. Please make sure the webhook secret has same value in your GitHub app and in the _APP_VCS_GITHUB_WEBHOOK_SECRET environment variable");
}
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$parsedPayload = $github->getEvent($event, $payload);
match ($event) {
$github::EVENT_INSTALLATION => $this->handleInstallationEvent($parsedPayload, $dbForPlatform, $authorization),
$github::EVENT_PUSH => $this->handlePushEvent($parsedPayload, $githubAppId, $privateKey, $github, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform),
$github::EVENT_PULL_REQUEST => $this->handlePullRequestEvent($parsedPayload, $privateKey, $githubAppId, $github, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform),
default => null,
};
return $response->json($parsedPayload);
}
protected function preprocessEvent(Request $request)
{
return;
}
private function handleInstallationEvent(
array $parsedPayload,
Database $dbForPlatform,
Authorization $authorization,
) {
if ($parsedPayload["action"] !== "deleted") {
return;
}
// TODO: Use worker for this job instead (update function/site as well)
$providerInstallationId = $parsedPayload["installationId"];
$installations = $dbForPlatform->find('installations', [
Query::equal('providerInstallationId', [$providerInstallationId]),
Query::limit(1000)
]);
foreach ($installations as $installation) {
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('installationInternalId', [$installation->getSequence()]),
Query::limit(1000)
]));
foreach ($repositories as $repository) {
$authorization->skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId()));
}
$authorization->skip(fn () => $dbForPlatform->deleteDocument('installations', $installation->getId()));
}
}
private function handlePushEvent(
array $parsedPayload,
string $githubAppId,
string $privateKey,
GitHub $github,
Database $dbForPlatform,
Authorization $authorization,
Build $queueForBuilds,
callable $getProjectDB,
array $platform,
) {
$providerBranchCreated = $parsedPayload["branchCreated"] ?? false;
$providerBranchDeleted = $parsedPayload["branchDeleted"] ?? false;
$providerBranch = $parsedPayload["branch"] ?? '';
$providerBranchUrl = $parsedPayload["branchUrl"] ?? '';
$providerRepositoryId = $parsedPayload["repositoryId"] ?? '';
$providerRepositoryName = $parsedPayload["repositoryName"] ?? '';
$providerInstallationId = $parsedPayload["installationId"] ?? '';
$providerRepositoryUrl = $parsedPayload["repositoryUrl"] ?? '';
$providerCommitHash = $parsedPayload["commitHash"] ?? '';
$providerRepositoryOwner = $parsedPayload["owner"] ?? '';
$providerCommitAuthorName = $parsedPayload["headCommitAuthorName"] ?? '';
$providerCommitAuthorEmail = $parsedPayload["headCommitAuthorEmail"] ?? '';
$providerCommitAuthorUrl = $parsedPayload["authorUrl"] ?? '';
$providerCommitMessage = $parsedPayload["headCommitMessage"] ?? '';
$providerCommitUrl = $parsedPayload["headCommitUrl"] ?? '';
Span::add('vcs.github.event.repo.id', $providerRepositoryId);
Span::add('vcs.github.event.repo.name', $providerRepositoryName);
Span::add('vcs.github.event.branch', $providerBranch);
Span::add('vcs.github.event.installation.id', $providerInstallationId);
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
// Find associated repositories
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::limit(100),
]));
// Create new deployment only on push (not committed by us) and not when branch is created or deleted
if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) {
$this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform);
}
}
private function handlePullRequestEvent(
array $parsedPayload,
string $privateKey,
string $githubAppId,
GitHub $github,
Database $dbForPlatform,
Authorization $authorization,
Build $queueForBuilds,
callable $getProjectDB,
array $platform,
) {
$action = $parsedPayload["action"] ?? '';
if ($action == "opened" || $action == "reopened" || $action == "synchronize") {
$providerBranch = $parsedPayload["branch"] ?? '';
$providerBranchUrl = $parsedPayload["branchUrl"] ?? '';
$providerRepositoryId = $parsedPayload["repositoryId"] ?? '';
$providerRepositoryName = $parsedPayload["repositoryName"] ?? '';
$providerInstallationId = $parsedPayload["installationId"] ?? '';
$providerRepositoryUrl = $parsedPayload["repositoryUrl"] ?? '';
$providerPullRequestId = $parsedPayload["pullRequestNumber"] ?? '';
$providerCommitHash = $parsedPayload["commitHash"] ?? '';
$providerRepositoryOwner = $parsedPayload["owner"] ?? '';
$external = $parsedPayload["external"] ?? true;
$providerCommitUrl = $parsedPayload["headCommitUrl"] ?? '';
$providerCommitAuthorUrl = $parsedPayload["authorUrl"] ?? '';
Span::add('vcs.github.event.repo.id', $providerRepositoryId);
Span::add('vcs.github.event.repo.name', $providerRepositoryName);
Span::add('vcs.github.event.branch', $providerBranch);
Span::add('vcs.github.event.installation.id', $providerInstallationId);
// Ignore sync for non-external. We handle it in push webhook
if (!$external && $parsedPayload["action"] == "synchronize") {
return;
}
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$commitDetails = $github->getCommit($providerRepositoryOwner, $providerRepositoryName, $providerCommitHash);
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
$providerCommitMessage = $commitDetails["commitMessage"] ?? '';
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
$this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform);
} elseif ($action == "closed") {
// Allowed external contributions cleanup
$providerRepositoryId = $parsedPayload["repositoryId"] ?? '';
$providerPullRequestId = $parsedPayload["pullRequestNumber"] ?? '';
$external = $parsedPayload["external"] ?? true;
if ($external) {
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
foreach ($repositories as $repository) {
$providerPullRequestIds = $repository->getAttribute('providerPullRequestIds', []);
if (\in_array($providerPullRequestId, $providerPullRequestIds)) {
$providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]);
$repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds);
$repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), new Document(['providerPullRequestIds' => $providerPullRequestIds])));
}
}
}
}
}
}
@@ -114,7 +114,11 @@ class Create extends Action
->setAttribute('personalRefreshToken', $refreshToken)
->setAttribute('personalAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry('')));
$dbForPlatform->updateDocument('installations', $installation->getId(), $installation);
$dbForPlatform->updateDocument('installations', $installation->getId(), new Document([
'personalAccessToken' => $installation->getAttribute('personalAccessToken'),
'personalRefreshToken' => $installation->getAttribute('personalRefreshToken'),
'personalAccessTokenExpiry' => $installation->getAttribute('personalAccessTokenExpiry'),
]));
}
try {
@@ -152,6 +156,8 @@ class Create extends Action
$repository['pushedAt'] = $repository['pushed_at'] ?? '';
$repository['organization'] = $installation->getAttribute('organization', '');
$repository['provider'] = $installation->getAttribute('provider', '');
$repository['providerInstallationId'] = $installation->getAttribute('providerInstallationId', '');
$repository['authorized'] = true;
$response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY);
}
@@ -85,11 +85,23 @@ class Get extends Action
$repository = $github->getRepository($owner, $repositoryName);
$authorized = false;
try {
$installationRepository = $github->getInstallationRepository($repositoryName);
if (!empty($installationRepository)) {
$authorized = true;
}
} catch (RepositoryNotFound $e) {
$authorized = false;
}
$repository['id'] = \strval($repository['id']) ?? '';
$repository['pushedAt'] = $repository['pushed_at'] ?? '';
$repository['organization'] = $installation->getAttribute('organization', '');
$repository['provider'] = $installation->getAttribute('provider', '');
$repository['defaultBranch'] = $repository['default_branch'] ?? '';
$repository['authorized'] = $authorized;
$repository['providerInstallationId'] = $providerInstallationId;
$response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY);
}
@@ -148,6 +148,8 @@ class XList extends Action
$repo['pushedAt'] = $repo['pushed_at'] ?? null;
$repo['provider'] = $installation->getAttribute('provider', '') ?? '';
$repo['organization'] = $installation->getAttribute('organization', '') ?? '';
$repo['providerInstallationId'] = $installation->getAttribute('providerInstallationId', '');
$repo['authorized'] = true;
return $repo;
}, $repos);
@@ -2,8 +2,10 @@
namespace Appwrite\Platform\Modules\VCS\Services;
use Appwrite\Platform\Modules\VCS\Http\GitHub\Authorize\External\Update as UpdateExternalDeployment;
use Appwrite\Platform\Modules\VCS\Http\GitHub\Authorize\Get as GetGitHubAuthorize;
use Appwrite\Platform\Modules\VCS\Http\GitHub\Callback\Get as GetGitHubCallback;
use Appwrite\Platform\Modules\VCS\Http\GitHub\Events\Create as CreateGitHubEvent;
use Appwrite\Platform\Modules\VCS\Http\Installations\Delete as DeleteInstallation;
use Appwrite\Platform\Modules\VCS\Http\Installations\Get as GetInstallation;
use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Branches\XList as ListRepositoryBranches;
@@ -24,6 +26,7 @@ class Http extends Service
// GitHub Authorization & Callback
$this->addAction(GetGitHubAuthorize::getName(), new GetGitHubAuthorize());
$this->addAction(GetGitHubCallback::getName(), new GetGitHubCallback());
$this->addAction(UpdateExternalDeployment::getName(), new UpdateExternalDeployment());
// Installations
$this->addAction(GetInstallation::getName(), new GetInstallation());
@@ -37,5 +40,8 @@ class Http extends Service
$this->addAction(ListRepositoryBranches::getName(), new ListRepositoryBranches());
$this->addAction(GetRepositoryContents::getName(), new GetRepositoryContents());
$this->addAction(CreateRepositoryDetections::getName(), new CreateRepositoryDetections());
// Events
$this->addAction(CreateGitHubEvent::getName(), new CreateGitHubEvent());
}
}
+2
View File
@@ -16,6 +16,7 @@ use Appwrite\Platform\Tasks\SDKs;
use Appwrite\Platform\Tasks\Specs;
use Appwrite\Platform\Tasks\SSL;
use Appwrite\Platform\Tasks\StatsResources;
use Appwrite\Platform\Tasks\TimeTravel;
use Appwrite\Platform\Tasks\Upgrade;
use Appwrite\Platform\Tasks\Vars;
use Appwrite\Platform\Tasks\Version;
@@ -44,6 +45,7 @@ class Tasks extends Service
->addAction(Vars::getName(), new Vars())
->addAction(Version::getName(), new Version())
->addAction(StatsResources::getName(), new StatsResources())
->addAction(TimeTravel::getName(), new TimeTravel())
;
}
}
+1 -3
View File
@@ -159,9 +159,7 @@ class Interval extends Action
}
foreach ($staleExecutions as $execution) {
$execution->setAttribute('status', 'failed');
$execution->setAttribute('errors', 'Execution timed out');
$dbForProject->updateDocument('executions', $execution->getId(), $execution);
$dbForProject->updateDocument('executions', $execution->getId(), new Document(['status' => 'failed', 'errors' => 'Execution timed out']));
}
$processed++;
+15 -5
View File
@@ -13,6 +13,7 @@ use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Platform\Action;
use Utopia\System\System;
use Utopia\Validator\WhiteList;
class Maintenance extends Action
{
@@ -25,6 +26,7 @@ class Maintenance extends Action
{
$this
->desc('Schedules maintenance tasks and publishes them to our queues')
->param('type', 'loop', new WhiteList(['loop', 'trigger']), 'How to run task. "loop" is meant for container entrypoint, and "trigger" for manual execution.')
->inject('dbForPlatform')
->inject('console')
->inject('queueForCertificates')
@@ -32,7 +34,7 @@ class Maintenance extends Action
->callback($this->action(...));
}
public function action(Database $dbForPlatform, Document $console, Certificate $queueForCertificates, Delete $queueForDeletes): void
public function action(string $type, Database $dbForPlatform, Document $console, Certificate $queueForCertificates, Delete $queueForDeletes): void
{
Console::title('Maintenance V1');
Console::success(APP_NAME . ' maintenance process v1 has started');
@@ -57,9 +59,7 @@ class Maintenance extends Action
$delay = $next->getTimestamp() - $now->getTimestamp();
}
Console::info('Setting loop start time to ' . $next->format("Y-m-d H:i:s.v") . '. Delaying for ' . $delay . ' seconds.');
Console::loop(function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $queueForCertificates) {
$action = function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $queueForCertificates) {
$time = DatabaseDateTime::now();
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
@@ -96,7 +96,17 @@ class Maintenance extends Action
$this->notifyDeleteCache($cacheRetention, $queueForDeletes);
$this->notifyDeleteSchedules($schedulesDeletionRetention, $queueForDeletes);
$this->notifyDeleteCSVExports($queueForDeletes);
}, $interval, $delay);
};
if ($type === 'loop') {
Console::info('Setting loop start time to ' . $next->format("Y-m-d H:i:s.v") . '. Delaying for ' . $delay . ' seconds.');
Console::loop(function () use ($action) {
$action();
}, $interval, $delay);
} elseif ($type === 'trigger') {
$action();
}
}
private function notifyDeleteConnections(Delete $queueForDeletes): void
+371 -229
View File
@@ -25,6 +25,7 @@ use Appwrite\SDK\Language\Swift;
use Appwrite\SDK\Language\Web;
use Appwrite\SDK\SDK;
use Appwrite\Spec\Swagger2;
use CzProject\GitPhp\Git;
use Utopia\Agents\Adapters\OpenAI;
use Utopia\Agents\DiffCheck\DiffCheck;
use Utopia\Agents\DiffCheck\Options as DiffCheckOptions;
@@ -41,29 +42,6 @@ use Utopia\Validator\WhiteList;
class SDKs extends Action
{
protected array $supportedSDKS = [
'web',
'cli',
'php',
'nodejs',
'deno',
'python',
'ruby',
'flutter',
'react-native',
'dart',
'go',
'swift',
'apple',
'dotnet',
'android',
'graphql',
'rest',
'markdown',
'agent-skills',
'cursor-plugin'
];
public static function getName(): string
{
return 'sdks';
@@ -74,6 +52,19 @@ class SDKs extends Action
return Specs::getPlatforms();
}
protected function getSdkConfigPath(): string
{
return __DIR__ . '/../../../../app/config/sdks.php';
}
protected function getSupportedSDKs(): array
{
return \array_unique(\array_merge(...\array_values(\array_map(
fn ($platform) => \array_column($platform['sdks'], 'key'),
Config::getParam('sdks')
))));
}
public function __construct()
{
$this
@@ -100,8 +91,9 @@ class SDKs extends Action
if (! $sdks) {
$selectedPlatform ??= Console::confirm('Choose Platform ("' . implode('", "', static::getPlatforms()) . '" or "*" for all):');
$selectedSDK ??= \strtolower(Console::confirm('Choose SDK ("*" for all):'));
if ($selectedSDK !== '*' && ! \in_array($selectedSDK, $this->supportedSDKS)) {
throw new \Exception('Unknown SDK "' . $selectedSDK . '" given. Options are: ' . implode(', ', $this->supportedSDKS));
$supportedSDKs = $this->getSupportedSDKs();
if ($selectedSDK !== '*' && ! \in_array($selectedSDK, $supportedSDKs)) {
throw new \Exception('Unknown SDK "' . $selectedSDK . '" given. Options are: ' . implode(', ', $supportedSDKs));
}
} else {
$sdks = explode(',', $sdks);
@@ -117,9 +109,6 @@ class SDKs extends Action
$prUrls = [];
if ($git) {
$message ??= Console::confirm('Please enter your commit message:');
}
} elseif ($examplesOnly) {
$git = false;
$prUrls = [];
@@ -337,7 +326,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
}
// Check if release already exists
$checkReleaseCommand = 'gh release view "' . $releaseVersion . '" --repo "' . $repoName . '" --json url --jq ".url" 2>/dev/null';
$checkReleaseCommand = 'gh release view ' . \escapeshellarg($releaseVersion) . ' --repo ' . \escapeshellarg($repoName) . ' --json url --jq ".url" 2>/dev/null';
$existingReleaseUrl = trim(\shell_exec($checkReleaseCommand) ?? '');
if (! empty($existingReleaseUrl)) {
@@ -368,7 +357,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
}
$previousVersion = '';
$tagListCommand = 'gh release list --repo "' . $repoName . '" --limit 1 --json tagName --jq ".[0].tagName" 2>&1';
$tagListCommand = 'gh release list --repo ' . \escapeshellarg($repoName) . ' --limit 1 --json tagName --jq ".[0].tagName" 2>&1';
$previousVersion = trim(\shell_exec($tagListCommand) ?? '');
$formattedNotes = "## What's Changed\n\n";
@@ -396,11 +385,11 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
$tempNotesFile = \tempnam(\sys_get_temp_dir(), 'release_notes_');
\file_put_contents($tempNotesFile, $formattedNotes);
$releaseCommand = 'gh release create "' . $releaseVersion . '" \
--repo "' . $repoName . '" \
--title "' . $releaseTitle . '" \
--notes-file "' . $tempNotesFile . '" \
--target "' . $releaseTarget . '" \
$releaseCommand = 'gh release create ' . \escapeshellarg($releaseVersion) . ' \
--repo ' . \escapeshellarg($repoName) . ' \
--title ' . \escapeshellarg($releaseTitle) . ' \
--notes-file ' . \escapeshellarg($tempNotesFile) . ' \
--target ' . \escapeshellarg($releaseTarget) . ' \
2>&1';
$releaseOutput = [];
@@ -486,12 +475,15 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
$useAi = ($ai !== 'no');
$apiKey = $useAi ? System::getEnv('_APP_ASSISTANT_OPENAI_API_KEY', '') : '';
$aiChangelog = ''; // Track AI-generated changelog for PR description
Console::info('Checking for _APP_ASSISTANT_OPENAI_API_KEY... [' . (! empty($apiKey) ? 'FOUND' : 'NOT FOUND') . ']');
if (! empty($apiKey) && ! $examplesOnly) {
Console::info("Using AI to determine version bump and changelog for {$language['name']} SDK...");
Console::info("Analyzing SDK changes with AI...");
$aiResult = $this->generateVersionAndChangelog($language, $result);
if ($aiResult !== null) {
if (!empty($aiResult['skip'])) {
Console::warning("Skipping {$language['name']} SDK generation");
continue;
} elseif ($aiResult !== null) {
$newVersion = $aiResult['version'];
$newChangelog = $aiResult['changelog'];
$aiChangelog = $newChangelog; // Store for PR description
@@ -499,23 +491,25 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// Update the version in the config
$this->updateSdkVersion($key, $language['key'], $newVersion);
// Update the changelog file
// Update the source changelog file
$this->updateChangelogFile($language['changelog'], $newVersion, $newChangelog);
// Re-read updated changelog so regeneration includes the new entry
$updatedChangelog = \file_get_contents($language['changelog']);
$sdk->setChangelog($updatedChangelog);
// Reload the language config with updated values
$language['version'] = $newVersion;
// Regenerate SDK with new version
// Regenerate SDK with new version and updated changelog
$sdk->setVersion($newVersion);
try {
$sdk->generate($result);
} catch (\Throwable $exception) {
Console::error($exception->getMessage());
}
Console::success("AI determined version: {$newVersion} ({$aiResult['versionBump']} bump)");
} else {
Console::warning('AI version generation failed, using existing version');
Console::warning('AI analysis failed, using existing version');
}
}
@@ -523,147 +517,26 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
$gitBranch = $language['gitBranch'];
$repoBranch = $language['repoBranch'] ?? 'main';
if ($git && ! empty($gitUrl)) {
\exec('rm -rf ' . $target . ' && \
mkdir -p ' . $target . ' && \
cd ' . $target . ' && \
git init && \
git config core.ignorecase false && \
git config pull.rebase false && \
git remote add origin ' . $gitUrl . ' && \
git fetch origin && \
(git checkout -f ' . $repoBranch . ' 2>/dev/null || git checkout -b ' . $repoBranch . ') && \
git pull origin ' . $repoBranch . ' && \
(git checkout -f ' . $gitBranch . ' 2>/dev/null || git checkout -b ' . $gitBranch . ') && \
(git fetch origin ' . $gitBranch . ' 2>/dev/null || git push -u origin ' . $gitBranch . ') && \
git reset --hard origin/' . $gitBranch . ' 2>/dev/null || true && \
(test -d .github && cp -r .github /tmp/.github-backup-$$ || true) && \
git rm -rf --cached . && \
git clean -fdx -e .git -e .github && \
cp -r ' . $result . '/. ' . $target . '/ && \
(test -d /tmp/.github-backup-$$ && cp -rn /tmp/.github-backup-$$/.github . && rm -rf /tmp/.github-backup-$$ || true) && \
git add -A && \
git commit -m "' . $message . '" && \
git push -u origin ' . $gitBranch . '
');
Console::success("Pushed {$language['name']} SDK to {$gitUrl}");
if ($git) {
$prTitle = "feat: {$language['name']} SDK update for version {$language['version']}";
$prBody = "This PR contains updates to the {$language['name']} SDK for version {$language['version']} . ";
$repoName = $language['gitUserName'] . '/' . $language['gitRepoName'];
Console::info("Creating pull request for {$language['name']} SDK...");
$prCommand = 'cd ' . $target . ' && \
gh pr create \
--repo "' . $repoName . '" \
--title "' . $prTitle . '" \
--body "' . $prBody . '" \
--base "' . $repoBranch . '" \
--head "' . $gitBranch . '" \
2>&1';
$prOutput = [];
$prReturnCode = 0;
\exec($prCommand, $prOutput, $prReturnCode);
if ($prReturnCode === 0) {
Console::success("Successfully created pull request for {$language['name']} SDK");
if (! empty($prOutput)) {
$prUrls[$language['name']] = end($prOutput);
}
} else {
$errorMessage = implode("\n", $prOutput);
if (strpos($errorMessage, 'already exists') !== false) {
Console::warning("Pull request already exists for {$language['name']} SDK, updating title and body...");
$prNumberCommand = 'cd ' . $target . ' && \
gh pr list \
--repo "' . $repoName . '" \
--head "' . $gitBranch . '" \
--json number \
--jq ".[0].number" \
2>&1';
$prNumberOutput = [];
$prNumberReturnCode = 0;
\exec($prNumberCommand, $prNumberOutput, $prNumberReturnCode);
if ($prNumberReturnCode === 0 && ! empty($prNumberOutput[0])) {
$prNumber = trim($prNumberOutput[0]);
// Use API directly to update PR to avoid deprecated projectCards field
$updateCommand = 'cd ' . $target . ' && \
gh api \
--method PATCH \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/' . $repoName . '/pulls/' . $prNumber . ' \
-f title="' . $prTitle . '" \
-f body="' . $prBody . '" \
2>&1';
$updateOutput = [];
$updateReturnCode = 0;
\exec($updateCommand, $updateOutput, $updateReturnCode);
if ($updateReturnCode === 0) {
Console::success("Successfully updated pull request for {$language['name']} SDK");
$prUrlCommand = 'cd ' . $target . ' && \
gh pr list \
--repo "' . $repoName . '" \
--head "' . $gitBranch . '" \
--json url \
--jq ".[0].url" \
2>&1';
$prUrlOutput = [];
$prUrlReturnCode = 0;
\exec($prUrlCommand, $prUrlOutput, $prUrlReturnCode);
if ($prUrlReturnCode === 0 && ! empty($prUrlOutput)) {
$prUrls[$language['name']] = trim($prUrlOutput[0]);
}
} else {
$updateErrorMessage = implode("\n", $updateOutput);
Console::error("Failed to update pull request for {$language['name']} SDK: " . $updateErrorMessage);
}
} else {
Console::error("Failed to get PR number for {$language['name']} SDK");
}
} else {
Console::error("Failed to create pull request for {$language['name']} SDK: " . $errorMessage);
}
}
if ($git && !empty($gitUrl)) {
// Generate commit message: use provided message, AI changelog, or fallback
if (! empty($message)) {
$commitMessage = $message;
} elseif (! empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') {
$commitMessage = "feat: update {$language['name']} SDK to {$language['version']}\n\n{$aiChangelog}";
} else {
$commitMessage = "chore: update {$language['name']} SDK to {$language['version']}";
}
\exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target);
Console::success("Remove temp directory '{$target}' for {$language['name']} SDK");
}
$pushSuccess = $this->pushToGit($language, $target, $result, $gitUrl, $gitBranch, $repoBranch, $commitMessage);
$docDirectories = $language['docDirectories'] ?? [''];
if ($version === 'latest') {
continue;
}
foreach ($docDirectories as $languageTitle => $path) {
$languagePath = strtolower($languageTitle !== 0 ? '/' . $languageTitle : '');
$examplesSource = $result . '/docs/examples' . $languagePath;
if (! \is_dir($examplesSource)) {
Console::warning("No code examples found for {$language['name']} SDK at: {$examplesSource}. Skipping copy.");
continue;
if ($pushSuccess) {
$this->createPullRequest($language, $target, $gitBranch, $repoBranch, $aiChangelog, $prUrls);
}
\exec(
'mkdir -p ' . $resultExamples . $languagePath . ' && \
cp -r ' . $examplesSource . ' ' . $resultExamples
);
Console::success("Copied code examples for {$language['name']} SDK to: {$resultExamples}");
$this->cleanupTarget($target, $language['name']);
}
$this->copyExamples($language, $version, $result, $resultExamples);
}
}
@@ -677,6 +550,176 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
}
}
private function pushToGit(array $language, string $target, string $result, string $gitUrl, string $gitBranch, string $repoBranch, string $commitMessage): bool
{
Console::info("Preparing {$language['name']} SDK repository...");
try {
// Init fresh repo
\exec('rm -rf ' . \escapeshellarg($target));
\mkdir($target, 0755, true);
$gitClient = new Git();
$repo = $gitClient->init($target);
$repo->execute('config', 'core.ignorecase', 'false');
$repo->execute('config', 'pull.rebase', 'false');
$repo->execute('config', 'advice.defaultBranchName', 'false');
$repo->addRemote('origin', $gitUrl);
// Fetch and checkout base branch (or create if new repo)
try {
$repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $repoBranch);
try {
$repo->execute('checkout', '-f', $repoBranch);
} catch (\Throwable) {
$repo->execute('checkout', '-b', $repoBranch);
}
} catch (\Throwable) {
$repo->execute('checkout', '-b', $repoBranch);
}
try {
$repo->execute('pull', 'origin', $repoBranch, '--quiet', '--no-tags');
} catch (\Throwable) {
}
// Checkout dev branch (or create if it doesn't exist)
try {
$repo->execute('checkout', '-f', $gitBranch);
} catch (\Throwable) {
$repo->execute('checkout', '-b', $gitBranch);
}
// Fetch dev branch, or push to create it on remote
try {
$repo->execute('fetch', 'origin', $gitBranch, '--quiet', '--no-tags', '--depth', '1');
} catch (\Throwable) {
try {
$repo->execute('push', '-u', 'origin', $gitBranch, '--quiet');
} catch (\Throwable) {
}
}
// Sync with remote dev branch
try {
$repo->execute('reset', '--hard', "origin/{$gitBranch}");
} catch (\Throwable) {
}
// Backup .github before cleaning working tree
$githubDir = $target . '/.github';
$githubBackup = \sys_get_temp_dir() . '/.github-backup-' . \getmypid();
$hasGithubDir = \is_dir($githubDir);
if ($hasGithubDir) {
\exec('cp -r ' . \escapeshellarg($githubDir) . ' ' . \escapeshellarg($githubBackup));
}
// Clean working tree
try {
$repo->execute('rm', '-rf', '--cached', '.');
} catch (\Throwable) {
}
try {
$repo->execute('clean', '-fdx', '-e', '.git', '-e', '.github');
} catch (\Throwable) {
}
// Copy generated SDK and restore .github
\exec('cp -r ' . \escapeshellarg($result . '/.') . ' ' . \escapeshellarg($target . '/'));
if ($hasGithubDir && \is_dir($githubBackup)) {
\exec('cp -rn ' . \escapeshellarg($githubBackup . '/.github') . ' ' . \escapeshellarg($target . '/') . ' 2>/dev/null');
\exec('rm -rf ' . \escapeshellarg($githubBackup));
}
// Stage, commit, push
$repo->addAllChanges();
$repo->commit($commitMessage);
$repo->execute('push', '-u', 'origin', $gitBranch, '--quiet');
} catch (\Throwable $e) {
Console::warning("Git operations failed for {$language['name']} SDK: " . $e->getMessage());
return false;
}
Console::success("Pushed {$language['name']} SDK to {$gitUrl}");
return true;
}
private function createPullRequest(array $language, string $target, string $gitBranch, string $repoBranch, string $aiChangelog, array &$prUrls): void
{
$prTitle = "feat: {$language['name']} SDK update for version {$language['version']}";
$prBody = "This PR contains updates to the {$language['name']} SDK for version {$language['version']}.";
if (!empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') {
$prBody .= "\n\n## Changes\n\n{$aiChangelog}";
}
$repoName = $language['gitUserName'] . '/' . $language['gitRepoName'];
Console::info("Creating pull request for {$language['name']} SDK...");
$prCommand = 'cd ' . $target . ' && \
gh pr create \
--repo ' . \escapeshellarg($repoName) . ' \
--title ' . \escapeshellarg($prTitle) . ' \
--body ' . \escapeshellarg($prBody) . ' \
--base ' . \escapeshellarg($repoBranch) . ' \
--head ' . \escapeshellarg($gitBranch) . ' \
2>&1';
$prOutput = [];
$prReturnCode = 0;
\exec($prCommand, $prOutput, $prReturnCode);
if ($prReturnCode === 0) {
Console::success("Successfully created pull request for {$language['name']} SDK");
foreach ($prOutput as $line) {
if (\str_starts_with(trim($line), 'https://')) {
$prUrls[$language['name']] = trim($line);
break;
}
}
} else {
$errorMessage = implode("\n", $prOutput);
if (strpos($errorMessage, 'already exists') === false) {
Console::error("Failed to create pull request for {$language['name']} SDK: " . $errorMessage);
} else {
$this->updateExistingPr($target, $repoName, $gitBranch, $prTitle, $prBody, $language['name'], $prUrls);
}
}
}
private function cleanupTarget(string $target, string $languageName): void
{
\exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target);
Console::success("Remove temp directory '{$target}' for {$languageName} SDK");
}
private function copyExamples(array $language, string $version, string $result, string $resultExamples): void
{
$docDirectories = $language['docDirectories'] ?? [''];
if ($version === 'latest') {
return;
}
foreach ($docDirectories as $languageTitle => $path) {
$languagePath = strtolower($languageTitle !== 0 ? '/' . $languageTitle : '');
$examplesSource = $result . '/docs/examples' . $languagePath;
if (! \is_dir($examplesSource)) {
Console::warning("No code examples found for {$language['name']} SDK at: {$examplesSource}. Skipping copy.");
continue;
}
\exec(
'mkdir -p ' . $resultExamples . $languagePath . ' && \
cp -r ' . $examplesSource . ' ' . $resultExamples
);
Console::success("Copied code examples for {$language['name']} SDK to: {$resultExamples}");
}
}
/**
* Extract release notes from changelog for a specific version
*/
@@ -764,37 +807,54 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
required: $object->getNames()
);
$isBeta = !empty($language['beta']);
$betaNote = $isBeta
? "\n Note: This SDK is in beta (version < 1.0.0). Do NOT bump to 1.0.0. Use `minor` for both breaking changes and new features, `patch` for bug fixes only."
: '';
$prompt = <<<PROMPT
Analyze the following git diff for the {$language['name']} SDK and determine:
You are a technical writer generating a changelog for the {$language['name']} SDK release.
Required output:
1. The appropriate version bump (`major`, `minor`, or `patch`) using semantic versioning.
2. The new version number (current version: {$language['version']}).
3. A clear, user-facing changelog.
Analyze the git diff below and return a JSON response with the version bump type, new version number, and changelog.
Semantic versioning rules:
- `major`: breaking, non-backward-compatible changes.
- `minor`: backward-compatible new features.
- `patch`: backward-compatible fixes or small improvements.
## Versioning
Changelog rules:
- Include only user-facing SDK changes.
- Exclude internal/project-infra changes (for example `.github/workflows/**`, `.github/ISSUE_TEMPLATE/**`, CI/release automation/template cleanup).
- Never add "Internal housekeeping" style entries.
- If only excluded changes exist, return exactly: `* No user-facing SDK changes.`
Current version: {$language['version']}
Diff context:
- Stats: {{diff_stats}}
- Base repository: {{base}}
- Generated SDK path: {{target}}
Git diff (truncated to 500 lines):
```diff
{{diff}}
```
Provide your analysis in the requested JSON format.
PROMPT;
Determine the semantic version bump:
- `major`: Breaking changes (removed/renamed public APIs, changed method signatures, dropped support)
- `minor`: New features that are backward-compatible (new methods, new optional parameters, new classes)
- `patch`: Bug fixes, documentation updates, refactors with no API surface change
{$betaNote}
When multiple change types are present, use the highest severity bump.
## Changelog guidelines
Write from the SDK consumer's perspective. Each entry should be a single line, max 15 words, in past tense.
Prefixes by category:
- **Breaking:** renamed/removed/changed APIs "Breaking: Renamed `oldMethod()` to `newMethod()`"
- **Added:** new features/options/endpoints "Added `streamResponse` option to client configuration"
- **Fixed:** bug fixes/corrections "Fixed incorrect timeout handling in retry logic"
- **Updated:** dependency bumps, doc improvements "Updated authentication examples for OAuth 2.0 flow"
Rules:
- Only include changes visible to SDK users (public API, behavior, docs, examples, CLI)
- Ignore: CI/CD pipelines (.github/), internal tooling, code formatting, test infrastructure
- Consolidate related changes into one entry (e.g., "Added `timeout`, `retries`, and `baseUrl` options" not three separate lines)
- Wrap all method names, parameter names, class names, and code identifiers in backticks (e.g., `listDocuments`, `ttl`)
- If the diff contains zero user-facing changes, return a single entry: "No user-facing SDK changes"
- Do not speculate only document what the diff explicitly shows
## Diff context
- Stats: {{diff_stats}}
- Base repository: {{base}}
- Generated SDK path: {{target}}
```diff
{{diff}}
```
PROMPT;
$options = (new DiffCheckOptions())
->setSchema($schema)
@@ -805,11 +865,13 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
->setExcludePaths([
'.github/workflows/**',
'.github/ISSUE_TEMPLATE/**',
'.git/**',
])
->setMaxDiffLines(500)
->setUserId('sdk-analyst');
Console::info("Running DiffCheck for {$language['name']} SDK...");
$result = (new DiffCheck())->run(
runner: $adapter,
base: DiffCheckRepository::remote($gitUrl, $repoBranch),
@@ -819,7 +881,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
);
if (!$result['hasChanges']) {
Console::warning("No changes detected for {$language['name']} SDK");
Console::info("No changes detected - SDK is up to date");
return null;
}
@@ -827,21 +889,15 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
if (empty(trim($responseContent))) {
Console::warning('AI returned empty response');
return null;
}
Console::log('AI raw response:');
Console::log($responseContent);
Console::log('--- End of AI response ---');
$parsed = json_decode($responseContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
Console::warning('Failed to parse AI response as JSON: ' . json_last_error_msg());
Console::log('Raw response that failed to parse:');
Console::log('Raw response:');
Console::log($responseContent);
return null;
}
@@ -850,7 +906,20 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
return null;
}
Console::info("AI analysis complete - Version bump: {$parsed['versionBump']}, New version: {$parsed['version']}");
// Guard: beta SDKs must not be bumped to >= 1.0.0
if ($isBeta && ($parsed['versionBump'] === 'major' || \version_compare($parsed['version'], '1.0.0', '>='))) {
Console::warning("Beta SDK {$language['name']} cannot have a major bump or version >= 1.0.0 (AI suggested {$parsed['version']}), skipping");
return ['skip' => true];
}
Console::success("✓ Analysis complete");
Console::log(" Version: {$language['version']}{$parsed['version']} ({$parsed['versionBump']} bump)");
Console::log(" Changelog:");
foreach (explode("\n", $parsed['changelog']) as $line) {
if (trim($line)) {
Console::log(" {$line}");
}
}
return [
'version' => $parsed['version'],
@@ -859,7 +928,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
];
} catch (\Throwable $e) {
Console::error('Error generating version and changelog: ' . $e->getMessage());
return null;
}
}
@@ -874,38 +942,53 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
*/
private function updateSdkVersion(string $platform, string $sdkKey, string $newVersion): bool
{
$configPath = __DIR__ . '/../../../../app/config/sdks.php';
$configPath = $this->getSdkConfigPath();
if (! file_exists($configPath)) {
Console::error("Config file not found: {$configPath}");
return false;
}
$content = file_get_contents($configPath);
// Find and replace the version for this specific SDK
// Pattern matches the version line in the SDK array
$pattern = '/(\[\s*[\'"]key[\'"]\s*=>\s*[\'"]' . preg_quote($sdkKey, '/') . '[\'"]\s*,[\s\S]*?[\'"]version[\'"]\s*=>\s*[\'"])([^\'"]+)([\'"])/m';
// First, try to find inline version in SDK array (pattern 1)
// Pattern matches: ['key' => 'nodejs', ... 'version' => '22.1.2']
$inlinePattern = '/(\[\s*[\'"]key[\'"]\s*=>\s*[\'"]' . preg_quote($sdkKey, '/') . '[\'"]\s*,[\s\S]*?[\'"]version[\'"]\s*=>\s*[\'"])([^\'"]+)([\'"])/m';
if (preg_match($pattern, $content, $matches)) {
if (preg_match($inlinePattern, $content, $matches)) {
$oldVersion = $matches[2];
$newContent = preg_replace($pattern, '${1}' . $newVersion . '${3}', $content);
$newContent = preg_replace($inlinePattern, '${1}' . $newVersion . '${3}', $content);
if (file_put_contents($configPath, $newContent) !== false) {
Console::success("Updated {$sdkKey} version from {$oldVersion} to {$newVersion} in config");
return true;
} else {
Console::error('Failed to write config file');
return false;
}
} else {
Console::warning("Could not find version entry for {$sdkKey} in config");
return false;
}
// Second, try to find version in array format (pattern 2)
// Pattern matches: 'nodejs' => '22.1.2', or "nodejs" => "22.1.2",
// Also handles extra whitespace: 'nodejs' => '22.1.2',
$arrayPattern = '/([\'"]' . preg_quote($sdkKey, '/') . '[\'"]\s*=>\s*[\'"])([^\'"]+)([\'"],)/m';
if (preg_match($arrayPattern, $content, $matches)) {
$oldVersion = $matches[2];
$newContent = preg_replace($arrayPattern, '${1}' . $newVersion . '${3}', $content);
if (file_put_contents($configPath, $newContent) !== false) {
Console::success("Updated {$sdkKey} version from {$oldVersion} to {$newVersion} in config");
return true;
} else {
Console::error('Failed to write config file');
return false;
}
}
Console::warning("Could not find version entry for {$sdkKey} in config");
return false;
}
/**
@@ -957,12 +1040,71 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
if (file_put_contents($changelogPath, $newContent) !== false) {
Console::success("Updated changelog at {$changelogPath} with version {$version}");
return true;
} else {
Console::error('Failed to write changelog file');
return false;
}
}
private function updateExistingPr(string $target, string $repoName, string $gitBranch, string $prTitle, string $prBody, string $sdkName, array &$prUrls): void
{
Console::warning("Pull request already exists for {$sdkName} SDK, updating title and body...");
$prNumberCommand = 'cd ' . $target . ' && \
gh pr list \
--repo ' . \escapeshellarg($repoName) . ' \
--head ' . \escapeshellarg($gitBranch) . ' \
--json number \
--jq ".[0].number" \
2>&1';
$prNumberOutput = [];
$prNumberReturnCode = 0;
\exec($prNumberCommand, $prNumberOutput, $prNumberReturnCode);
if ($prNumberReturnCode !== 0 || empty($prNumberOutput[0])) {
Console::error("Failed to get PR number for {$sdkName} SDK");
return;
}
$prNumber = trim($prNumberOutput[0]);
$apiPath = "/repos/{$repoName}/pulls/{$prNumber}";
$updateCommand = 'cd ' . $target . ' && \
gh api \
--method PATCH \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
' . \escapeshellarg($apiPath) . ' \
-f title=' . \escapeshellarg($prTitle) . ' \
-f body=' . \escapeshellarg($prBody) . ' \
2>&1';
$updateOutput = [];
$updateReturnCode = 0;
\exec($updateCommand, $updateOutput, $updateReturnCode);
if ($updateReturnCode !== 0) {
Console::error("Failed to update pull request for {$sdkName} SDK: " . implode("\n", $updateOutput));
return;
}
Console::success("Successfully updated pull request for {$sdkName} SDK");
$prUrlCommand = 'cd ' . $target . ' && \
gh pr list \
--repo ' . \escapeshellarg($repoName) . ' \
--head ' . \escapeshellarg($gitBranch) . ' \
--json url \
--jq ".[0].url" \
2>&1';
$prUrlOutput = [];
$prUrlReturnCode = 0;
\exec($prUrlCommand, $prUrlOutput, $prUrlReturnCode);
if ($prUrlReturnCode === 0 && ! empty($prUrlOutput)) {
$prUrls[$sdkName] = trim($prUrlOutput[0]);
}
}
}
+5 -2
View File
@@ -59,8 +59,11 @@ abstract class ScheduleBase extends Action
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
$dbForPlatform->updateDocument('projects', $project->getId(), $project);
$now = DateTime::now();
$dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'accessedAt' => $now
]));
$project->setAttribute('accessedAt', $now);
}
}
}
@@ -0,0 +1,72 @@
<?php
namespace Appwrite\Platform\Tasks;
use Utopia\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\System\System;
use Utopia\Validator\WhiteList;
class TimeTravel extends Action
{
public static function getName(): string
{
return 'time-travel';
}
public function __construct()
{
$this
->desc('Create a time-travel to change $createdAt')
->param('projectId', '', new UID(), 'Project ID.')
->param('resourceType', '', new WhiteList(['deployment']), 'Type of resource.')
->param('resourceId', '', new UID(), 'ID of resource.')
->param('createdAt', '', new DatetimeValidator(), 'New value for $createdAt')
->inject('getProjectDB')
->inject('dbForPlatform')
->callback($this->action(...));
}
public function action(string $projectId, string $resourceType, string $resourceId, string $createdAt, callable $getProjectDB, Database $dbForPlatform): void
{
$isDevelopment = System::getEnv('_APP_ENV', 'development') === 'development';
if (!$isDevelopment) {
Console::error('This task is only available in development mode.');
return;
}
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
Console::error('Project not found.');
return;
}
$collection = match ($resourceType) {
'deployment' => 'deployments',
default => throw new \Exception('Resource type not implemented')
};
/** @var Database $dbForProject */
$dbForProject = $getProjectDB($project);
$resource = $dbForProject->getDocument($collection, $resourceId);
if ($resource->isEmpty()) {
Console::error('Resource not found.');
return;
}
$update = new Document([
'$createdAt' => $createdAt,
]);
$dbForProject->withPreserveDates(fn () => $dbForProject->updateDocument($collection, $resourceId, $update));
Console::success('Time-travel successful. Updated $createdAt for ' . $resourceType . ' ' . $resourceId . ' to ' . $createdAt);
}
}

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