Compare commits

..
Author SHA1 Message Date
harsh mahajan 437b3bfa10 fix: lint 2026-05-14 18:08:59 +05:30
harsh mahajan 0b07305775 fix: remove redundant comment, rename commitSkip to validator, use getDescription in span 2026-05-14 18:05:04 +05:30
harsh mahajan b5f0ebb37b refactor: replace DeploymentSkipPatterns validator with VCS_DEPLOYMENT_SKIP_PATTERNS constant 2026-05-14 17:59:34 +05:30
harsh mahajan 3cc600d29a fix: revert validators to 0.2.* for auto updates 2026-05-14 17:56:09 +05:30
Harsh MahajanandGitHub 736196b58b Merge branch '1.9.x' into feat/skip-deployment-commit-patterns 2026-05-14 17:31:06 +05:30
harsh mahajan 25772f4585 chore: composer update all dependencies 2026-05-14 17:28:52 +05:30
harsh mahajan e3755679c5 refactor: extend Contains validator, keep only [skip ci] pattern 2026-05-14 17:26:32 +05:30
Chirag AggarwalandGitHub 450c80ca5b Merge pull request #12312 from appwrite/fix/text-response-specs
Fix text response schemas in API specs
2026-05-14 16:56:32 +05:30
Chirag Aggarwal e1547023f6 Fix OpenAPI text redirect response schemas 2026-05-14 16:48:58 +05:30
Chirag Aggarwal 6d0a76cf51 Fix text response schemas in API specs 2026-05-14 16:35:08 +05:30
Chirag AggarwalandGitHub a3fa12e5b3 Merge pull request #12305 from appwrite/chore/refactor-delete-queue-publisher
refactor: Migrate queueForDeletes to DeletePublisher and DeleteMessage
2026-05-14 16:31:03 +05:30
Chirag AggarwalandGitHub 659bc0dab7 Merge pull request #12307 from appwrite/codex/delete-orphaned-rules-1-9
[codex] Delete orphaned proxy rules on create
2026-05-14 16:28:33 +05:30
premtsd-codeandGitHub 927a5cddc4 Merge pull request #11439 from appwrite/feat-platform-db-access
Add platform migration support
2026-05-14 11:13:53 +01:00
Chirag Aggarwal 6b51a56b2b Tidy deleteOrphanedRule conditionals 2026-05-14 15:42:52 +05:30
Chirag Aggarwal 8f481d6f2c refactor: keep delete publisher in shared container 2026-05-14 15:37:04 +05:30
Chirag Aggarwal 421a9974af Delete orphaned proxy rules on create 2026-05-14 15:33:29 +05:30
Chirag Aggarwal 65d1e58e34 refactor: migrate delete queue publisher 2026-05-14 14:40:11 +05:30
premtsd-codeandGitHub beb119883a Merge branch '1.9.x' into feat-platform-db-access 2026-05-14 08:37:33 +01:00
Prem Palanisamy 1da8d38daf Pin utopia-php/migration to 1.* (resolves 1.12.0) 2026-05-14 08:36:46 +01:00
Harsh MahajanandGitHub 05fb3a5d8d Merge pull request #12304 from appwrite/fix-specs-platform-order
Fix: project specs navigation order
2026-05-14 12:31:41 +05:30
harsh mahajan 5b7d6f1ca6 Fix project specs navigation order 2026-05-14 12:13:13 +05:30
Jake BarnbyandGitHub a4d1043684 Merge pull request #12302 from appwrite/fix-transaction-deletes
fix: match transactions collection name in deletes worker
2026-05-14 17:48:18 +12:00
Chirag AggarwalandGitHub 10de0d4245 Merge pull request #12284 from appwrite/codex/fail-open-abuse-timelimit
Fail open on abuse timelimit failures
2026-05-14 11:02:16 +05:30
Prem Palanisamy 0497137d80 Merge remote-tracking branch 'origin/1.9.x' into feat-platform-db-access
# Conflicts:
#	composer.lock
2026-05-14 06:20:03 +01:00
Prem Palanisamy f05b55add7 test: use project API key for platform migration E2E; bump migration lock 2026-05-14 06:15:55 +01:00
Chirag Aggarwal 1ced50db1f Merge remote-tracking branch 'origin/1.9.x' into codex/resolve-12284-conflict
# Conflicts:
#	app/controllers/shared/api.php
2026-05-14 10:43:05 +05:30
Damodar LohaniandGitHub 64b0cdaab7 Merge pull request #12303 from appwrite/chore/utopia-audit-2.3
chore: bump utopia-php/audit to 2.3.x
2026-05-14 10:19:46 +05:45
Damodar LohaniandGitHub bce5fe8e09 Merge branch '1.9.x' into chore/utopia-audit-2.3 2026-05-14 10:07:53 +05:45
Damodar LohaniandClaude Opus 4.7 6116d113b8 chore: refresh utopia-php/audit to 2.3.2
Picks up the opt-in async cleanup setter (utopia-php/audit#117)
needed for the deletes worker on the cloud side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 04:20:27 +00:00
Chirag AggarwalandGitHub 1fb1cec1b6 Merge pull request #12299 from appwrite/chore/refactor-function-queue-publisher
refactor: Migrate queueForFunctions to FunctionPublisher and FunctionMessage
2026-05-14 08:56:12 +05:30
Damodar LohaniandClaude Opus 4.7 fe5de129da chore: bump utopia-php/audit to 2.3.x
Brings in cursor pagination (cursorAfter/cursorBefore) and count(max)
support for the activities/audit endpoints. Pulls utopia-php/query 0.1.x
as a new transitive dependency. utopia-php/audit 2.3.x requires PHP 8.4,
satisfied by the appwrite/base:1.4.1 runtime image used on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:16:48 +00:00
Jake BarnbyandClaude Opus 4.7 10eae0ab87 fix: match transactions collection name in deletes worker
The DELETE_TYPE_TRANSACTION constant was 'transaction' (singular), so
the worker's inner switch on $document->getCollection() (which returns
'transactions') never matched and every transaction deletion fell to
the default branch, logging "No lazy delete operation available for
document of type: transactions". Renamed to DELETE_TYPE_TRANSACTIONS
to align with every other constant used in that switch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 13:58:40 +12:00
Prem Palanisamy e9c16a8fd3 chore(deps): bump utopia-php/migration to feat-platform-db-access 2026-05-13 16:56:09 +01:00
Prem Palanisamy bddde5f298 Merge remote-tracking branch 'origin/1.9.x' into feat-platform-db-access
# Conflicts:
#	app/controllers/api/migrations.php
#	composer.json
#	composer.lock
#	src/Appwrite/Platform/Workers/Migrations.php
2026-05-13 16:36:39 +01:00
Chirag Aggarwal 85e2cf7d8b (refactor): Migrate queueForFunctions to FunctionPublisher and FunctionMessage 2026-05-13 18:28:05 +05:30
Luke B. SilverandGitHub f6d2314e11 Merge pull request #12298 from appwrite/chore/normalize-span-keys
chore: normalize Span keys and centralize span lifecycle
2026-05-13 12:01:46 +01:00
loks0nandClaude Opus 4.7 24e108fdbf fix: use canonical project.id/user.id so trace sampler matches realtime and VCS spans
The sampler reads project.id from the span; realtime and VCS were storing it
under namespaced keys (realtime.project.id, vcs.github.event.repo.{id}.project.id),
causing all those spans to be dropped when _APP_TRACE_PROJECT_ID was set.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 11:03:28 +01:00
loks0nandClaude Opus 4.7 9a016d7727 docs: tighten span tracing section in AGENTS.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 10:32:41 +01:00
Chirag Aggarwal d9987dd645 Fix rate limit headers when abuse check is skipped 2026-05-13 15:00:51 +05:30
loks0nandClaude Opus 4.7 a0c4b7695e chore: normalize Span keys and centralize span lifecycle
Span attribute keys are now snake_case with dots only for child
relationships. Worker span lifecycle moved to app/worker.php; selective
trace filtering moved to the exporter sampler in app/init/span.php so
handlers only call Span::add.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 10:29:42 +01:00
Chirag AggarwalandGitHub 86df9ca14a Merge pull request #12297 from appwrite/chore/bump-utopia-cache-2
Bump Utopia cache to 2.1
2026-05-13 14:20:14 +05:30
Chirag Aggarwal 476e9de2a8 Bump Utopia cache to 2.1 2026-05-13 12:49:17 +05:30
Jake BarnbyandGitHub be2bb984c5 Merge pull request #12194 from appwrite/feat-insights-module
feat(insights): add insights module with CTA framework
2026-05-13 17:34:56 +12:00
Jake Barnby b058eb31f4 (refactor): inline CTAs as JSON on insights collection 2026-05-13 17:10:33 +12:00
Chirag AggarwalandGitHub 1c09d93a07 Merge pull request #12295 from appwrite/codex/update-sdk-generator-cover-image
Update SDK generator cover image config
2026-05-13 10:31:35 +05:30
Chirag Aggarwal 31fbc64ad2 Update SDK generator cover image config 2026-05-13 10:19:54 +05:30
Luke B. SilverandGitHub 35941ddc34 Merge pull request #12220 from appwrite/feat/utopia-http-resources-context
Migrate to utopia-php/http resources/context API
2026-05-12 23:01:29 +01:00
Matej BačoandGitHub 7421b29c45 Merge pull request #12292 from appwrite/fix-locale-tests
Fix: avatars tests
2026-05-12 16:53:31 +02:00
Matej Bačo af0c41dcf5 Fix avatars tests 2026-05-12 16:08:58 +02:00
Jake BarnbyandClaude Opus 4.6 f4133609db (fix): remove invalid index prefix lengths exceeding column size
The prefix length 700 exceeded the resourceId/parentResourceId column
size of 255 (Database::LENGTH_KEY), crashing the container at startup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-13 02:05:55 +12:00
Jake BarnbyandClaude Opus 4.6 b85ca1536a (fix): remove redundant null coalesce on always-defined variable
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-13 01:53:33 +12:00
Jake BarnbyandClaude Opus 4.6 304b0dab35 (fix): address greptile P1 review comments
- Add skipFilters to Reports/Get.php (was the only endpoint still
  triggering the full N+1 subquery cascade)
- Scale CTA batch limit dynamically (insightCount * MAX_CTA_COUNT)
  instead of fixed APP_LIMIT_SUBQUERY to prevent silent truncation
- Revert deleteReport to callback-based pagination so CTAs are not
  orphaned when a report has more than APP_LIMIT_SUBQUERY insights
- Add explicit prefix lengths (700) to _key_project_resource and
  _key_project_parent_resource indexes to stay under InnoDB 3072-byte limit
- Validate CTA service/method against ADVISOR_CTA_SERVICES and
  ADVISOR_CTA_METHODS enums in the CTAs validator

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-13 01:42:59 +12:00
Jake BarnbyandClaude Opus 4.6 fdb8e06f12 (test): fix misleading advisor e2e test names and assertions
- Rename testReportsAreReadOnly → testReportsCreateAndUpdateNotExposed
  (reports ARE deletable, they're not read-only)
- Extract testDeleteReportMissing with proper error type assertion
- Rename testInsightsAreReadOnly → testInsightsCreateUpdateDeleteNotExposed
- Fix insight create test to use correct URL (/reports/:id/insights)
  instead of nonexistent /manager/ prefix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-13 01:15:43 +12:00
Jake BarnbyandClaude Opus 4.6 9da4a3260d (refactor): improve advisor module perf, security, and maintainability
- Fix N+1 in Reports/XList (51→4 queries) via skipFilters + batch fetch
- Add skipFilters to Reports/Delete and cursor fetch (avoid loading all
  nested insights/CTAs just for ownership check)
- Fix N+1 in deleteReport worker (flat CTA deletion instead of per-insight)
- Add advisor entity cleanup on project deletion (reports, insights, CTAs)
- Remove resourceInternalId, parentResourceInternalId, $permissions from
  Insight response model (internal IDs leak DB internals, permissions unused)
- Remove dead subQueryInsightCTAs filter registration
- Remove stale enum-value comments from platform schema
- Fix _key_dismissedAt index to include projectInternalId
- Fix scope category from 'Other' to 'Advisor'
- Switch action base class from Utopia\Platform\Action to Appwrite\Platform\Action

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-13 01:13:37 +12:00
Jake BarnbyandGitHub 8c8a56ed43 Merge pull request #12273 from appwrite/refactor-target-fetch
fix(messaging): remove redundant user fetch
2026-05-13 01:00:26 +12:00
Matej BačoandGitHub 99fad4dca2 Merge pull request #12290 from appwrite/fix-sdk-generator-for-response-enums
Fix: Project SDK enum names
2026-05-12 14:55:48 +02:00
Matej Bačo b655f24172 Fix warning 2026-05-12 14:49:59 +02:00
Matej Bačo 5a748c2783 PR review fixes 2026-05-12 14:46:33 +02:00
Jake BarnbyandGitHub 87e93c9ff5 Merge pull request #12289 from appwrite/fix/tablesdb-runner-family
chore: pin TablesDB e2e to 8cpu runner
2026-05-13 00:42:36 +12:00
Matej Bačo 0ca52a1bdb Request enum params sdk method name 2026-05-12 14:39:13 +02:00
Jake Barnby a0ecffe961 chore: pin TablesDB e2e to 8cpu runner
After family=m7 was removed from the default e2e_service runner in #12274,
TablesDB inherits the bare 4cpu runner with no instance-family pinning. Under
paratest_processes: 3 the databases worker can't create attributes fast enough
for the polling loop, producing "Expected 'available', Actual 'processing'"
flakes across unrelated PRs. Match the Databases service override to give
TablesDB its own 8cpu runner.
2026-05-13 00:24:21 +12:00
Matej Bačo 17e9c313f0 Fix response format enum sdk names 2026-05-12 14:17:31 +02:00
Jake Barnby f9ff1166b9 (perf): batch-fetch CTAs across an insight set to collapse N+1 2026-05-12 23:24:26 +12:00
Chirag Aggarwal 1ca8282944 Remove unused request logError resource 2026-05-12 16:30:31 +05:30
Jake BarnbyandGitHub 4cd35e4d93 Merge branch '1.9.x' into feat-insights-module 2026-05-12 22:59:34 +12:00
Matej BačoandGitHub 5e1439f64e Merge pull request #12276 from appwrite/feat-rework-project-response-model
Feat: Rework project response model
2026-05-12 12:57:16 +02:00
loks0nandClaude Opus 4.7 e97dfb041a Bump utopia-php/http to 2.0-rc1 and utopia-php/platform to 1.0-rc1
These RC releases ship the new resources/context API that this PR
adopts. Drop the dev-branch alias on utopia-php/http now that 2.0.0-rc1
is tagged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:55:41 +01:00
Chirag Aggarwal e132784532 Fix cache auth role lookup after rate limit refactor 2026-05-12 16:24:50 +05:30
loks0nandClaude Opus 4.7 1cef07a2d4 Fix GraphQL coroutine context propagation
Promises\Swoole was already propagating the request container into child
coroutines, but using its own pre-existing key (__utopia_http_request_container)
rather than the new utopia-php/http key (__utopia__). With the keys
mismatched, the propagation was dead code and resolvers spawned from
webonyx coroutines saw an empty context, failing every GraphQL query
with 'Dependency utopia:graphql not found'.

Align the key so child coroutines actually inherit the request's
container (wrapped as a child Container, so request-scoped overrides
in the resolver can't bleed back into the outer request).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:47:52 +01:00
loks0nandClaude Opus 4.7 93e96a044d Fix bus resolver to look up context container per dispatch
The resources/context migration replaced the resolver closure with a
first-class callable from `$swoole->context()->get(...)`. That
captures the result of `$swoole->context()` once -- on the first
dispatch -- and binds the bus to the first request's per-request
container forever after.

Wrap the lookup in a fresh closure so each dispatch re-resolves
`context()` against the current Swoole coroutine. Restores the
behavior of the pre-migration `getContainer()` form.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 11:47:52 +01:00
loks0nandClaude Opus 4.7 040ce6e335 Simplify Swoole worker bootstrap
EVENT_START and EVENT_TASK were each constructing `new Http($swoole, 'UTC')`
purely as a vehicle to reach DI -- never configuring routing, never calling
->run(). Now that the Swoole adapter exposes resources() directly, the
Http instance is no longer needed: the resources container we passed into
the Server constructor is already in scope as $container.

Drop the throwaway Http construction, pass $container straight through, and
update createDatabase() to take Container instead of Http to match. Also
rename $swooleAdapter to $swoole for consistency with the rest of the file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:47:51 +01:00
Chirag Aggarwal a51fd49ed1 Refactor abuse rate limiting init hook 2026-05-12 16:17:23 +05:30
loks0nandClaude Opus 4.7 4bdcdb6f96 Migrate to utopia-php/http resources/context API
Adopts the new split DI containers in utopia-php/http: `resources()` for
boot-time wiring (shared across requests) and `context()` for per-request
state. Replaces the removed `getResource()`/`setResource()`/`getContainer()`
helpers throughout the HTTP entry point, controllers, GraphQL layer, and
installer.

Bumps the dependency chain accordingly: utopia-php/http to the dev branch
(aliased to 0.34.25 to satisfy platform's exact pin), servers 0.4.*,
queue 0.18.*, and pulled-along cli/platform/database upgrades.

Also tightens app/init/resources/request.php by collapsing single-return
factories to arrow functions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:41:42 +01:00
Chirag Aggarwal 7914fde9f1 Use error_log for abuse timelimit failures 2026-05-12 16:09:20 +05:30
Matej Bačo 930c23d7e3 Harden backwards compatibility attributes 2026-05-12 12:27:06 +02:00
Jake Barnby 45f3a64617 (perf): skip subquery filters when loading report for ownership-only check 2026-05-12 22:09:50 +12:00
Matej Bačo 548077f01e Fix flaky test 2026-05-12 11:27:24 +02:00
Matej BačoandGitHub bbd24e5029 Merge pull request #12285 from appwrite/fix-schedule-doc-missing
Fix: scheduler doc missing for functions
2026-05-12 11:26:13 +02:00
Matej Bačo 8bef181ad8 Fix scheduler doc missing for functions 2026-05-12 10:44:16 +02:00
Matej Bačo ff92fd229b Revert "Fix function scheduler document missing"
This reverts commit 5ef5ead98f.
2026-05-12 10:43:11 +02:00
Matej Bačo 5ef5ead98f Fix function scheduler document missing 2026-05-12 10:42:56 +02:00
63df500b6f refactor(constants): remove Advisor alias prefixes for insight/report enums
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/55c56d91-4db4-4501-b5cb-42159dbc9778

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-12 08:09:18 +00:00
Chirag Aggarwal d523c7425a Log abuse timelimit fail-open errors 2026-05-12 13:31:33 +05:30
Chirag Aggarwal 728298d2f5 Address abuse timelimit review comments 2026-05-12 13:23:39 +05:30
Chirag Aggarwal ac79a91aa9 Fail open on abuse timelimit failures 2026-05-12 13:12:13 +05:30
Atharva DeosthaleandGitHub 668b61e620 Merge pull request #12279 from appwrite/fix-codex-plugin
fix codex plugin
2026-05-12 12:29:06 +05:30
Jake BarnbyandGitHub ea28f12fb4 Merge pull request #12280 from appwrite/fix-migration-stuck-on-early-failure
fix(migrations): write _APP_MIGRATION_HOST in the installer-generated .env
2026-05-12 17:08:45 +12:00
Damodar LohaniandGitHub 45298f8e23 Merge pull request #12282 from appwrite/CLO-4320-fix-project-delete-event
Fix: remove invalid event label from project delete action
2026-05-12 10:47:33 +05:45
Damodar Lohani 0e87d0b483 fix: remove invalid event label from project delete action
Event::generateEvents() rejects the pattern 'project.delete' because
it parses 'delete' as a resource that must be present in route params.
The pre-migration route did not declare an event label, so functions
and webhooks were never triggered on project deletion. Restore that
behavior by removing the label.
2026-05-12 04:50:58 +00:00
Chirag AggarwalandGitHub fde30794ec Merge pull request #12246 from appwrite/refactor/mails-messaging-publishers
Migrate mails and messaging queues to publishers
2026-05-12 09:58:56 +05:30
Jake Barnby 51c65093b8 (feat): add appId and appInternalId to reports collection for third-party app submissions 2026-05-12 16:09:38 +12:00
premtsd-codeandGitHub 1f5ee53f3a Merge branch '1.9.x' into fix-migration-stuck-on-early-failure 2026-05-11 21:29:40 +01:00
Prem Palanisamy 3721e6b950 fix(migrations): write _APP_MIGRATION_HOST in generated .env (install & upgrade)
The migrations worker (Appwrite→Appwrite migrations + CSV/JSON imports
& exports) reads `_APP_MIGRATION_HOST` to call back into this instance's
API. `_APP_MIGRATION_HOST` was introduced in #11229 (1.8.x / 1.9.x) but
was never added to `app/config/variables.php`, so `appwrite install` /
`appwrite upgrade` never write it into the generated `.env`. With the
var unset, the migrations worker fails — on a fresh self-hosted install
the first export hangs with no error (#11853). (Contributors and CI
don't hit it because the repo's hand-maintained `.env` already has
`_APP_MIGRATION_HOST=appwrite`; only the *installer-generated* `.env` is
missing it.)

Add `_APP_MIGRATION_HOST` to `app/config/variables.php` with default
`appwrite` — the API service name in the standard Docker Compose setup,
which is what the repo's own `.env` and the cloud Helm charts already
use, and what the migration endpoint was hardcoded to before #11229.
`appwrite install` and `appwrite upgrade` now write it into the
generated `.env`, so fresh installs and upgrades have it set and the
migration/import/export flows work.

Scope: this PR fixes the install & upgrade paths only — it deliberately
doesn't change the worker code.

Fixes #11853
2026-05-11 21:26:26 +01:00
Levi van NoortandGitHub 103e219675 Merge pull request #12274 from appwrite/migrate-away-from-blacksmith-based-runners
refactor: enhance execution log checks in SitesCustomServerTest
2026-05-11 21:35:42 +02:00
Matej Bačo da3a3b939a Finalize PR 2026-05-11 20:22:50 +02:00
Levi van Noort 994a4e0fc7 chore: remove 'family=m7' from e2e_service runner configuration 2026-05-11 20:11:08 +02:00
Matej Bačo 36dec7c88f Fix more tests 2026-05-11 19:47:57 +02:00
Matej Bačo 5eb4c6b310 Fix tests 2026-05-11 18:38:30 +02:00
Matej Bačo 152b45087e Fix more tests 2026-05-11 17:01:00 +02:00
Levi van Noort 5fba2889cb chore: update e2e_service runner configuration to use m7 family 2026-05-11 16:58:36 +02:00
Levi van NoortandGitHub 9b4f8d7ac0 Merge branch '1.9.x' into migrate-away-from-blacksmith-based-runners 2026-05-11 16:16:55 +02:00
Atharva Deosthale 9056de0103 fix codex plugin 2026-05-11 19:38:06 +05:30
Matej Bačo 6cbf080156 Fix code quality 2026-05-11 16:04:39 +02:00
Matej Bačo 89ec639f87 Fix startup issues 2026-05-11 15:55:53 +02:00
Matej Bačo 870fe72bf6 Fix response filter 2026-05-11 15:40:45 +02:00
harsh mahajan dda38442ad refactor: simplify deployment skip patterns 2026-05-11 18:56:31 +05:30
Matej Bačo 5cd737b901 Backwards compatibility for GET /v1/project 2026-05-11 15:08:15 +02:00
harsh mahajan cc0207cfe4 fix: align appwrite skip directives 2026-05-11 18:32:14 +05:30
harsh mahajan fa63983fd7 fix: bracket appwrite skip directives 2026-05-11 18:11:48 +05:30
harsh mahajan 9a6d2a6d21 refactor: use fixed commit skip directives 2026-05-11 18:02:51 +05:30
Chirag Aggarwal cf1bb1a1cc build 2026-05-11 17:59:43 +05:30
Jake Barnby c18865423e Merge remote-tracking branch 'origin/1.9.x' into feat-insights-module
# Conflicts:
#	composer.lock
#	src/Appwrite/Platform/Workers/Migrations.php
2026-05-12 00:23:17 +12:00
Jake Barnby fec779de05 (chore): bump utopia-php/migration to 1.10.2 to match resolver signature 2026-05-12 00:15:49 +12:00
Chirag AggarwalandGitHub 330b11a64b Merge branch '1.9.x' into refactor/mails-messaging-publishers 2026-05-11 17:43:48 +05:30
harsh mahajan 73eb14d4bb fix: fail open for non-string commit messages 2026-05-11 17:19:55 +05:30
harsh mahajan 4684688fc7 feat: expose commit skip patterns 2026-05-11 17:17:01 +05:30
Jake BarnbyandClaude Opus 4.7 53d18a0d08 refactor(advisor): rename module to Advisor (service-named, top-level)
The module's namespace and directory now match the top-level service
name (`advisor`) instead of one of its resource names (`insights`):

- src/Appwrite/Platform/Modules/Insights -> .../Modules/Advisor
- src/Appwrite/Insights -> src/Appwrite/Advisor
- tests/unit/Insights -> tests/unit/Advisor
- Route group label flipped from `'insights'` to `'advisor'`
- Section-header comments aligned

Resource names (`insights`, `reports`, `insightCTAs`) and the
`Insight*`/`Report` response models stay — those are the resources the
service exposes, not the service itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:40:06 +12:00
Harsh MahajanandGitHub 68fdf3fbb4 Merge branch '1.9.x' into feat/skip-deployment-commit-patterns 2026-05-11 17:07:55 +05:30
harsh mahajanandClaude Sonnet 4.6 444d4b4e66 refactor: standalone regex matching for commit skip patterns
Replace directive-extraction approach with word-boundary regex matching
so plain-word patterns like "skip appwrite" and "appwrite skip" work
alongside bracket directives. Use \s+ between word tokens (required
space) and \s* only after ":" tokens (git trailer flexibility).
Add tests for "skip appwrite" and "appwrite skip" with case insensitivity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 17:04:58 +05:30
harsh mahajan 4a4f51622d fix: tighten commit skip directive matching 2026-05-11 17:04:58 +05:30
harsh mahajanandClaude Sonnet 4.6 2836f89b34 feat: skip deployment on commit message pattern match
Add providerCommitSkipPatterns array field to functions and sites.
Any commit message containing one of the patterns (case-insensitive
substring) skips the VCS-triggered deployment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 17:04:58 +05:30
Jake BarnbyandGitHub 4e73559eb7 Merge branch '1.9.x' into refactor-target-fetch 2026-05-11 23:31:18 +12:00
Jake BarnbyandGitHub 11acbc6db4 Merge pull request #12245 from appwrite/feat-bump-sdk-23
feat: bump utopia-php/abuse and utopia-php/migration to feat/sdk-23 branches
2026-05-11 23:30:10 +12:00
Levi van Noort d8dbe15cb3 chore: update runner configuration for e2e_service jobs in ci workflow 2026-05-11 13:12:25 +02:00
Jake BarnbyandClaude Opus 4.7 d2e4ed6a11 test(messaging): skip users recipient test when push DSN not configured
CI lacks _APP_MESSAGE_PUSH_TEST_DSN, so new DSN(null) threw a TypeError
before the existing skip guard could run. Match the pattern used by
testSendPushNotification: gate on the env var first, then construct the DSN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:58:29 +12:00
Chirag AggarwalandGitHub 6ae113a969 Merge branch '1.9.x' into refactor/mails-messaging-publishers 2026-05-11 16:24:50 +05:30
Matej Bačo 2da400d5b4 Bump version to introduce response filter (for project get) 2026-05-11 12:53:01 +02:00
Jake BarnbyandClaude Opus 4.7 e8bdb954f2 docs(advisor): add per-endpoint reference docs
Each SDK method now points at its own markdown file under
docs/references/advisor/ instead of an inline heredoc, matching the
house convention used by every other module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:51:58 +12:00
Matej Bačo 9fae4c965f Unpin console version to get latest automatically
Only during local development
2026-05-11 12:50:44 +02:00
Matej Bačo 83887b09b6 Improve project get tests 2026-05-11 12:47:53 +02:00
Jake Barnby e6a93d4a9f chore: bump limit 2026-05-11 22:37:44 +12:00
Matej Bačo 4e36040f47 Add project GET tests 2026-05-11 12:22:17 +02:00
Matej Bačo 5727d25f32 Add enum to project response models 2026-05-11 12:19:51 +02:00
Matej Bačo 1f7f1e7b31 Fix failing to startup issue 2026-05-11 12:16:04 +02:00
Matej Bačo 14493f7ea4 PR review fixes 2026-05-11 12:13:36 +02:00
Matej Bačo fd893a8b10 Migrate project GET endpoint 2026-05-11 12:10:38 +02:00
Matej Bačo beab655eba Code quality details 2026-05-11 12:03:48 +02:00
d8957a5468 fix(messaging): paginate user recipient target lookup
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/8ea97768-3304-4a3c-81e2-d62d6fe3ad80

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-11 10:03:40 +00:00
2a47ccc0f9 test(messaging): add users recipient e2e coverage and fix user target query limit
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/8ea97768-3304-4a3c-81e2-d62d6fe3ad80

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-11 10:03:02 +00:00
Matej Bačo 443ebac71c Finish project response model 2026-05-11 11:56:22 +02:00
Jake BarnbyGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
3773a5a5dc Update src/Appwrite/Platform/Workers/Messaging.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-05-11 21:51:19 +12:00
51f1aee93a Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-11 21:50:38 +12:00
premtsd-codeandGitHub 0b2a9240b3 Merge branch '1.9.x' into feat-bump-sdk-23 2026-05-11 10:28:38 +01:00
Jake BarnbyandClaude Opus 4.7 7041c1d719 fix(migrations): resolve destination DSN per database type
utopia-php/migration 1.10.2 stopped propagating the source DSN to the
destination's `_databases.database` and added an opt-in `getDatabaseDSN`
resolver. Without it, the destination DB row gets a blank `database`
field, so reads on documentsdb/vectorsdb fall back to the project's
main DSN (mongodb in dedicated mode) and fail with "Vector types are
not supported by the current database".

Pass a resolver that builds the proper DSN from the destination
project's pools per database type, mirroring the logic
`Database/Create.php` already uses on first creation. Made
`constructDatabaseDSNFromProjectDatabase` public+static so the worker
can reuse it instead of duplicating the pool-selection logic.

This is a 1.9.x regression introduced by the migration 1.10.1 -> 1.10.2
bump; affects every PR that runs MongoDB-dedicated Migrations tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:25:53 +12:00
Matej Bačo 75d4e931bc Project response model rework beginning 2026-05-11 11:17:41 +02:00
Levi van Noort d949482094 chore: set bigger size on database related e2e 2026-05-11 11:10:41 +02:00
Levi van NoortandGitHub c95901b879 Merge branch '1.9.x' into migrate-away-from-blacksmith-based-runners 2026-05-11 10:50:29 +02:00
Levi van Noort 0fb2e208ab chore: add spot=false label to the runs-on based runners 2026-05-11 10:47:22 +02:00
Matej BačoandGitHub 76aed33a5c Merge pull request #12263 from appwrite/feat-google-oauth-prompt-param
Feat: Google OAuth2 "prompt" param
2026-05-11 10:47:13 +02:00
Prem Palanisamy c26ff1849a chore: bump utopia-php/abuse to 1.3.0 and migration to 1.11.0 (released) 2026-05-11 09:37:07 +01:00
Levi van Noort 702a8a83a0 test: add assertion for action execution logs content in SitesCustomServerTest 2026-05-11 10:30:08 +02:00
Levi van NoortandGitHub 5be05da5ca Merge branch '1.9.x' into migrate-away-from-blacksmith-based-runners 2026-05-11 10:27:46 +02:00
Levi van Noort fe5e5b8891 refactor: enhance execution log checks in SitesCustomServerTest 2026-05-11 10:17:55 +02:00
Jake Barnby add56d7365 chore: select 2026-05-11 19:54:48 +12:00
Jake Barnby 5c09d726ba fix(messaging): remove redundant user fetch 2026-05-11 19:51:26 +12:00
Jake BarnbyandClaude Opus 4.7 db4cb5361d fix(databases): fall back to project DSN when database attr is empty
`getDatabasesDB` used `??` to fall back from a database doc's `database`
attribute to the project DSN, but `??` only triggers on null. Migration
destinations end up with an empty-string `database` (the value is copied
from the source DB but isn't a valid DSN on the destination's pool),
which slipped past the fallback and surfaced as a 500 with
`new DSN('mysql://')` in the catch block. Use elvis so empty strings
fall back too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:12:48 +12:00
Jake BarnbyandClaude Opus 4.7 ad02ad3f81 fix(advisor): fall back to direct insight lookup beyond subquery cap
The report's embedded insights subquery is capped at APP_LIMIT_SUBQUERY,
so insights past position 1000 would 404 even though they exist. Try
the embedded slice first, then fall back to a scoped direct lookup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:23:20 +12:00
Prem Palanisamy a902c25363 refactor(migrations): extract destination DSN resolver to named method
Replaces the inline match closure with resolveDestinationDatabaseDsn(),
mirroring cloud's worker. Adds a docblock explaining why documentsdb /
vectorsdb keep the source DSN.
2026-05-11 06:52:23 +01:00
premtsd-codeandGitHub 04a6eaf5d4 Merge branch '1.9.x' into feat-bump-sdk-23 2026-05-11 05:54:00 +01:00
Prem Palanisamy f0fb7bf877 test: remove orphaned VectorsDB testGetCollectionLogs
Endpoint deleted in 96fe989f6d ("update composer dependencies and remove
obsolete log classes") but the two test methods calling it were left
behind. They have been failing with 404 on every PR since.
2026-05-11 05:53:03 +01:00
Prem Palanisamy 74fbbea2b3 chore: bump utopia-php/abuse cfd290a + migration 447a987 (PHP >=8.2) 2026-05-11 05:41:37 +01:00
735be3e892 fix(advisor): align nested insights list SDK method naming
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/7e091450-e0b2-4c2c-98a7-ba964e5436ca

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-11 03:25:41 +00:00
49bf1198b2 refactor(advisor): polish constants aliases and insight lookup helper
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/517a4586-d3e8-40b3-a3a9-f2d2ca82b0a0

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-11 02:08:29 +00:00
9186ce3f24 chore(advisor): document null index prefix lengths in reports index
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/517a4586-d3e8-40b3-a3a9-f2d2ca82b0a0

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-11 02:05:20 +00:00
bfd6cebbb0 fix(advisor): address review comments on tests, naming, docs, and get insight
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/517a4586-d3e8-40b3-a3a9-f2d2ca82b0a0

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-11 01:59:31 +00:00
Prem Palanisamy 4957f568cd chore: bump utopia-php/migration to 4dc7270 (empty teams guard) 2026-05-11 02:38:01 +01:00
Prem Palanisamy 64c9d8d85f chore: bump utopia-php/migration to a9bdfba (revert deno fallback) 2026-05-11 02:28:07 +01:00
Prem Palanisamy 7b5cb379c4 chore: bump utopia-php/migration to e5dc657 (deno runtime fallback) 2026-05-11 02:21:18 +01:00
Prem Palanisamy fd625fca7b fix(migrations): preserve source DSN for documentsdb/vectorsdb resolver
Migration lib 1.10.2's getDatabaseDSN resolver returns the value
written into destination's _databases.database. The previous resolver
always returned the project's main DSN (mongodb in default CI) for
every database type, including documentsdb / vectorsdb — which are
routed to their own adapters (mongodb / postgresql) per
_APP_DB_ADAPTER_DOCUMENTSDB / _APP_DB_ADAPTER_VECTORSDB.

The wrong DSN routed vectorsdb attribute creates back to mongodb,
producing 'Vector types are not supported by the current database'
on the MixedDatabases / VectorsDB migration tests.

Mirror cloud's resolver: keep the source DSN for documentsdb /
vectorsdb (they target dedicated hosts), use destination project's
main DSN otherwise.
2026-05-10 22:09:39 +01:00
Matej Bačo e45e5a09f4 Reorder tests to make them pass 2026-05-10 13:19:18 +02:00
Matej Bačo e3dc30ad93 PR review fixes 2026-05-10 12:03:05 +02:00
Matej Bačo 0406d9e04d improve copy 2026-05-10 11:13:23 +02:00
Matej Bačo fbfde6cc77 Implement google oauth prompt param. 2026-05-10 11:11:47 +02:00
Matej BačoandGitHub c6f91e18c4 Merge pull request #12254 from appwrite/fix-sateless-git-hints
Fix: Stateless git hints
2026-05-10 10:14:51 +02:00
Matej BačoandGitHub 6db1d2e5c0 Merge pull request #12256 from appwrite/chore-google-oauth-dual-read
Chore: Dual read for google oauth secret
2026-05-10 10:14:30 +02:00
Prem Palanisamy 817172c460 Merge branch '1.9.x' into feat-bump-sdk-23
# Conflicts:
#	composer.lock
2026-05-10 05:54:06 +01:00
Prem Palanisamy cd445ceccf chore: bump utopia-php/migration to 6deabc6 (Sites::create named args) 2026-05-10 05:49:23 +01:00
Chirag AggarwalandGitHub 87a32f65ee Merge pull request #12182 from appwrite/add-codex-plugin 2026-05-10 09:26:49 +05:30
Prem Palanisamy 698fde247f chore: bump utopia-php/migration to 0e88268 (createVariable variableId fix) 2026-05-10 04:46:40 +01:00
Matej Bačo 0e939ea9d7 PR review fixes 2026-05-09 12:58:47 +02:00
Matej Bačo a5ddc465e6 PR review fixes 2026-05-09 12:53:11 +02:00
Matej Bačo 76a41d70b0 Dual read for google oauth secret
Will allow future support for more params
2026-05-09 10:51:46 +02:00
Matej Bačo 43777ee6d9 Add unit tests for github hints 2026-05-09 10:16:19 +02:00
Matej Bačo 6ee2196fae Fix git hint regnerating nonstop 2026-05-09 09:54:54 +02:00
Jake BarnbyandClaude Opus 4.7 8fa9ead279 chore: merge 1.9.x into feat-insights-module
Resolves conflict in app/init/constants.php (kept 4327 cache buster, took 1.9.4 stable version).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:59:01 +12:00
Jake BarnbyandClaude Opus 4.7 83c0a32494 fix(advisor): make reports.target a TEXT column
target stores free-form URLs or resource IDs. URLs in the wild can
exceed the prior 2048-char cap, so switch the column to VAR_TEXT
(65535). The _key_project_target index already declares an explicit
700-char prefix length, so indexing still works on both MariaDB and
MongoDB.

Bump APP_CACHE_BUSTER for the schema change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:42:29 +12:00
Jake BarnbyandClaude Opus 4.7 b830c08257 fix(advisor): use VAR_TEXT for free-form fields
Switch markdown summaries, JSON CTA params, and the embedded
insights/ctas subQuery payloads from sized VAR_STRING to VAR_TEXT.
None of these fields are indexed and they all hold free-form data, so
removing the artificial size cap means we never have to revisit the
limit when an analyzer surfaces a longer report.

- reports.summary, insights.summary: markdown, no upper bound enforced
- reports.insights, insights.ctas: subQuery virtuals carrying full
  child rows
- insightCTAs.params: JSON parameter blob for arbitrary CTA actions

Bump APP_CACHE_BUSTER for the schema change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:30:45 +12:00
Jake BarnbyandClaude Opus 4.7 b747c97561 fix(advisor): address review comments on scopes and schema
- Register dedicated reports.write scope and switch deleteReport to it
  so cloud can issue narrowly-scoped delete keys without granting
  insights.write.
- Make insights.parentResourceInternalId optional with null default to
  match its companion parentResourceType/parentResourceId fields and
  unblock insights with no parent (e.g. database-level performance
  insights).
- Tighten Insight.reportId model description: insights always belong to
  a report, ad-hoc insights are not supported.
- Add reports.write to default test API key and admin role so existing
  e2e tests using serverHeaders() can hit the delete endpoint.
- Bump APP_CACHE_BUSTER for the schema change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:18:39 +12:00
Jake BarnbyGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
e27997dcbc Update src/Appwrite/Insights/Validator/CTAs.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-05-09 12:06:09 +12:00
6b64eb85b3 fix(advisor): require null-default VAR_ID schema fields
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/38cebef5-ed45-4283-9097-d48dd0fafb06

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-09 00:00:28 +00:00
Prem Palanisamy 5313460c7c fix(migrations): pass destination project DSN resolver to DestinationAppwrite
Migration lib's `_databases.database` resolver now defaults to empty
when no callable is supplied (utopia-php/migration ff3b444). The
runtime falls back via `$project->getAttribute('database')`, but tests
hit DSN("mysql://") and a 500 because that fallback is also empty for
fresh test projects.

Pass an explicit resolver returning the destination project's `database`
attribute so migrated databases store a usable DSN.
2026-05-08 17:53:09 +01:00
Prem Palanisamy c94ae409e8 chore: refresh composer.lock for utopia-php/fetch ^1.1 and logger 0.8.* 2026-05-08 16:50:04 +01:00
Prem Palanisamy 4844a4bf99 Merge branch '1.9.x' into feat-bump-sdk-23
# Conflicts:
#	composer.lock
2026-05-08 16:34:19 +01:00
Prem Palanisamy 687698001f chore: bump utopia-php/migration to 80e9a04 (SDK 23 nested typed object fixes) 2026-05-08 16:30:02 +01:00
Jake BarnbyandClaude Opus 4.7 5ba92c7ff4 test(proxy): clean up rules before deleting their site
testCreateRedirectRule reassigned $ruleId for the second redirect rule,
so only the second was tracked, and cleanupSite was called before
cleanupRule. Site deletion cascades to its rules, so cleanupRule then
saw a 404 and the strict assertEquals(204) blew up.

Track both rule IDs and tear them down before the site so the asserted
204 actually fires.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 02:09:48 +12:00
Jake BarnbyandClaude Opus 4.7 a23555783f test(advisor): initialise project before getNewKey
testReadWithAdvisorScopes called getNewKey() as its first action, but
getNewKey reads self::$project['$id'] without triggering project
creation. ParaTest runs each test method in a fresh worker, so
self::$project is empty until getProject() is called. The empty id
produced /v1/projects//keys, which doesn't match any route and 404s.

Call getProject() first, like every other consumer of getNewKey.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 01:49:22 +12:00
Harsh MahajanandGitHub acc0b2c184 Merge pull request #12252 from appwrite/fix/repository-branch-pagination-validator
fix: Support branch query validators in SDK generation
2026-05-08 19:18:04 +05:30
harsh mahajan 10e4341db2 Support branch query validators in SDK generation 2026-05-08 19:15:17 +05:30
Jake BarnbyandClaude Opus 4.7 83cf47e3fd fix(advisor): correct index lengths to fit MongoDB 1024 limit
The reports `_key_project_target` index was failing to create on MongoDB
because target's full size (2048) plus targetType (64) exceeded the 1024
character maximum. Set explicit lengths to truncate target to 700 chars
(total 765, under both Mongo 1024 and MariaDB 768 limits).

Also drop redundant explicit lengths from VAR_ID positions in the
insights and insightCTAs indexes — the validator rejects explicit
lengths on VAR_ID attributes, and they default to length 1 anyway.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 01:29:01 +12:00
Chirag AggarwalandGitHub 8a35e87cfd Merge pull request #12251 from appwrite/fix/graphql-preview-test-assertions 2026-05-08 18:13:14 +05:30
Chirag Aggarwal 9295869279 Fix GraphQL preview test assertions 2026-05-08 17:51:42 +05:30
Chirag AggarwalandGitHub 5472bff265 Merge pull request #12219 from abhay-dev2901/fix/scheduler-platform-env 2026-05-08 17:17:43 +05:30
AbhayandGitHub 27de465032 Merge branch '1.9.x' into fix/scheduler-platform-env 2026-05-08 17:14:42 +05:30
Chirag AggarwalandGitHub f2a59de804 Merge branch '1.9.x' into refactor/mails-messaging-publishers 2026-05-08 17:13:49 +05:30
Prem Palanisamy 4563706b76 chore: bump utopia-php/migration to a5195ba (SDK 23 typed models refactor) 2026-05-08 12:38:20 +01:00
Chirag AggarwalandGitHub be9bb5e34d Merge pull request #12249 from appwrite/fix/preview-conditional-transforms 2026-05-08 16:40:56 +05:30
Chirag AggarwalandGitHub 0a582daff0 Merge pull request #12248 from appwrite/codex/bump-logger-0.8.0 2026-05-08 16:38:05 +05:30
Torsten DittmannandGitHub 003647abd7 Merge branch '1.9.x' into fix/preview-conditional-transforms 2026-05-08 15:06:08 +04:00
Torsten Dittmann 9100e06bbe perf(storage): skip no-op imagick transforms and add transform value spans
- Only call crop() when dimensions > 0 or gravity differs from center
- Only call setOpacity() when opacity !== 1.0 (was always called due to !empty(1.0))
- Only call setBorder() when borderWidth > 0
- Only call setBorderRadius() when borderRadius > 0
- Only call setRotation() when rotation !== 0
- Add Span::add() with actual values inside each transform condition
- Leave output() unconditional (format conversion always applies)
2026-05-08 15:04:54 +04:00
Chirag Aggarwal fc83b4d986 Bump logger dependency 2026-05-08 16:25:46 +05:30
Harsh MahajanandGitHub 4954655e8b Merge pull request #12243 from appwrite/feat/repository-branch-search-pagination
feat: Add search and pagination for repository branches
2026-05-08 16:14:06 +05:30
ArnabChatterjee20kandGitHub 319483d41d Merge pull request #12244 from appwrite/fix-method-ids
Enhance URL parameter handling in OpenAPI3 and Swagger2 formats to su…
2026-05-08 15:35:05 +05:30
Aditya Oberai 5f6b1dda1a update composer lock 2026-05-08 09:35:38 +00:00
Aditya Oberai 8df7a628c8 Merge branch '1.9.x' into add-codex-plugin 2026-05-08 09:34:43 +00:00
Chirag Aggarwal 18ec96f124 Address Greptile feedback for queue publishers 2026-05-08 14:41:58 +05:30
Chirag Aggarwal 34075322d7 Migrate mails and messaging queues to publishers 2026-05-08 14:32:11 +05:30
Prem Palanisamy 74b84ba38c feat: bump utopia-php/abuse and utopia-php/migration to feat/sdk-23 branches 2026-05-08 09:52:47 +01:00
Harsh MahajanandGitHub fe4608a6c3 Merge branch '1.9.x' into feat/repository-branch-search-pagination 2026-05-08 13:50:51 +05:30
harsh mahajan d598773163 Adjust repository branch cursor typing for analysis 2026-05-08 13:50:24 +05:30
Levi van NoortandGitHub 79a4eac82f Merge pull request #12241 from appwrite/migrate-away-from-blacksmith-based-runners
refactor: migrate to different ci runners
2026-05-08 10:19:12 +02:00
harsh mahajan 370a823888 Use a query validator for repository branch pagination 2026-05-08 13:43:02 +05:30
harsh mahajan 3fbe77a27c Fix repository branch cursorBefore pagination 2026-05-08 13:31:32 +05:30
harsh mahajan 1a0a19a793 Add search and pagination for repository branches 2026-05-08 13:25:35 +05:30
Levi van Noort bd0e7456a7 refactor: update ci runners to use volume instead of disk 2026-05-08 09:08:42 +02:00
Levi van Noort 6a0d2d5314 chore: add bigger disks to the provisioned setup 2026-05-08 09:01:36 +02:00
721b3c1229 style(advisor): add strict return types to report delete action
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/6496395d-5cbf-42ba-a3e0-d3c9e6ca901c

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-08 06:56:05 +00:00
f63ded6159 chore(advisor): simplify report cleanup delete queries
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/6496395d-5cbf-42ba-a3e0-d3c9e6ca901c

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-08 06:55:28 +00:00
56b9de097f fix(advisor): restore report delete with async nested cleanup
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/6496395d-5cbf-42ba-a3e0-d3c9e6ca901c

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-08 06:54:49 +00:00
Jake BarnbyandGitHub 602e1be296 Merge pull request #12226 from appwrite/spike/isPublishable-migration
Stop user-data errors leaking to Sentry from migrations worker (isPublishable)
2026-05-08 18:22:41 +12:00
Chirag AggarwalandGitHub e10d51c4eb Merge pull request #12240 from appwrite/fix/cache-control-callback 2026-05-08 11:39:30 +05:30
a9902c33df chore(advisor): clarify insights.write scope usage
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/8d7897b5-ac68-487d-954a-be717380bf66

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-08 06:08:17 +00:00
6d0eab2583 refactor(advisor): make insights API read-only in CE
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/8d7897b5-ac68-487d-954a-be717380bf66

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-08 06:07:23 +00:00
Chirag Aggarwal 3d35d140d1 Relax storage preview cache hit max age 2026-05-08 11:27:35 +05:30
Chirag Aggarwal 8ebf4fee46 Use storage cache callback max age 2026-05-08 11:18:55 +05:30
Chirag Aggarwal 9337daf7ac Preserve default storage cache hit max age 2026-05-08 11:15:17 +05:30
0829b26508 refactor(insights): remove redundant payload field
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/a680e208-34b8-4bae-a7fd-51949112233a

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-08 05:44:20 +00:00
Chirag Aggarwal 2207fff44d Wait for storage preview cache hit in e2e 2026-05-08 10:52:31 +05:30
Chirag Aggarwal 8e274012bd Relax storage preview cache max-age assertion 2026-05-08 10:48:12 +05:30
8c57ff161e fix(insights): use VAR_ID for project internal references
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/688345d8-e680-46c7-9002-82f73193461b

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-08 05:17:15 +00:00
0d65ffbb6c chore(insights): simplify report index lengths and orders
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/2e52811c-bf98-4b39-b3f7-64fabcef4cf6

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-08 05:13:38 +00:00
Chirag Aggarwal 75a30793ac Remove preview flag from storage cache config 2026-05-08 10:42:39 +05:30
Chirag Aggarwal 96e5193601 Fix storage preview cache e2e assertion 2026-05-08 10:18:34 +05:30
Chirag Aggarwal 48e2ee9acb Test storage preview cache headers 2026-05-08 10:04:40 +05:30
Chirag Aggarwal 5841d0a944 Inline storage preview cache control 2026-05-08 09:59:36 +05:30
Chirag Aggarwal bf76736971 Split storage cache control context 2026-05-08 09:57:33 +05:30
Chirag Aggarwal 707e5d231a Scope storage cache control to previews 2026-05-08 09:55:33 +05:30
Chirag Aggarwal 6abd88b8f1 Type storage cache control config 2026-05-08 09:44:00 +05:30
Chirag Aggarwal 1208aff569 Fix storage cache hook analyze 2026-05-08 08:52:24 +05:30
Chirag Aggarwal 7b5a5b8d19 Rename storage cache control hook 2026-05-08 08:44:35 +05:30
Chirag Aggarwal 20ab5b026e Add cached response cache-control callback 2026-05-08 08:33:37 +05:30
Jake BarnbyandGitHub db46a60168 Merge pull request #12237 from appwrite/chore/pin-github-actions
chore: pin github actions to sha and bump to latest
2026-05-08 13:37:59 +12:00
premtsd-codeandGitHub f2b96318ca Merge branch '1.9.x' into spike/isPublishable-migration 2026-05-07 21:37:53 +05:30
Luke B. SilverandGitHub 1d004240e6 Merge pull request #12236 from appwrite/refactor/builds-publishing
refactor: migrate queueForBuilds to publisherForBuilds pattern
2026-05-07 17:06:32 +01:00
Matej BačoandGitHub 98ba86b876 Merge pull request #12239 from appwrite/fix-console-email-features
Fix: Email policies for console project
2026-05-07 17:58:55 +02:00
Matej Bačo c3881f9974 Fix console email policy features 2026-05-07 17:13:26 +02:00
Levi van Noort b2080fb8f9 refactor: migrate to different ci runners 2026-05-07 17:07:13 +02:00
Jake BarnbyandGitHub 04171dc0b8 Merge pull request #12238 from appwrite/fix-method-ids
updated
2026-05-08 01:11:55 +12:00
Jake Barnby 708aea2532 chore: pin github actions to sha and bump to latest
Pin every third-party action in .github/workflows/ to a full commit SHA
with a trailing version comment, and bump to the latest stable release.
Defends against tag-rewrite supply-chain attacks while keeping versions
legible.
2026-05-08 01:07:12 +12:00
loks0nandClaude Opus 4.7 f947fde701 Restore platform fallback and null-template round-trip in BuildMessage
- toArray() falls back to Config::getParam('platform') when the
  in-memory platform is empty, matching the old Build event behavior so
  workers always receive a populated platform array.
- toArray() emits null for an absent template instead of an empty
  array, and fromArray() treats empty/null/missing template as null so
  the round-trip preserves the no-template case.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 13:56:46 +01:00
Prem Palanisamy 3676af925c Sanitize bubbled message for non-typed exceptions on migration document 2026-05-07 13:45:57 +01:00
Prem Palanisamy 81e57ea30e Revert gate fallback to always-publish on unknown exception types 2026-05-07 13:36:49 +01:00
Prem Palanisamy 98b4e9b063 Loosen utopia-php/migration constraint back to 1.* (matches 1.9.x baseline) 2026-05-07 13:30:28 +01:00
loks0nandClaude Opus 4.7 7d0843cc7f Migrate queueForBuilds to publisherForBuilds pattern
Replaces the stateful Appwrite\Event\Build queue class with a stateless
BuildPublisher and BuildMessage DTO, matching the publisher pattern used
by audits, certificates, executions, migrations, screenshots, stats, and
usage. Call sites now enqueue messages directly instead of mutating a
shared event object and relying on the API shutdown hook.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 13:23:21 +01:00
Jake BarnbyGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
409cebb26e Apply suggestion from @greptile-apps[bot]
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-05-08 00:12:20 +12:00
Prem Palanisamy e4b51d0abb Drop custom utopia-php/migration VCS repo (now resolved via Packagist) 2026-05-07 13:10:31 +01:00
premtsd-codeandGitHub 76ddaf0317 Merge branch '1.9.x' into spike/isPublishable-migration 2026-05-07 17:17:55 +05:30
ArnabChatterjee20kandGitHub dfd6c2fbb4 Merge pull request #12235 from appwrite/fix-method-ids
updated methods
2026-05-07 16:56:06 +05:30
Matej BačoandGitHub 461e07cfee Merge pull request #12234 from appwrite/chore-improve-project-response-tests
Chore: Improve project GET tests
2026-05-07 12:39:54 +02:00
Matej Bačo a1e51d27eb PR review fixes 2026-05-07 11:57:14 +02:00
Matej Bačo 0b881ec58a Finish test improvements for get project 2026-05-07 11:45:25 +02:00
Matej Bačo c4a941015f Continue adding project get tests 2026-05-07 11:26:55 +02:00
Matej Bačo 379a388f61 auth security get project tests 2026-05-07 11:12:05 +02:00
Matej Bačo 38575d7620 Improve get project tests 2026-05-07 11:06:47 +02:00
Matej Bačo fe27ae0584 Fix failing tests 2026-05-07 10:58:05 +02:00
Matej Bačo 8ad106632e Improve tests further 2026-05-07 10:40:54 +02:00
Matej Bačo 4721523ea8 Improve project tests 2026-05-07 10:30:56 +02:00
Prem Palanisamy e7d3384660 Pin utopia-php/migration to 1.10.* (release with Sentry-leak fix) 2026-05-07 09:22:21 +01:00
premtsd-codeandGitHub 0ccb206a1d Merge branch '1.9.x' into spike/isPublishable-migration 2026-05-07 13:31:07 +05:30
premtsd-codeandGitHub 0b810e500f Merge branch '1.9.x' into spike/isPublishable-migration 2026-05-07 12:42:04 +05:30
Prem Palanisamy 8e9fd32566 spike: simplify gate fallback to always publish on unknown exception types 2026-05-07 07:41:47 +01:00
Prem Palanisamy 85d7c27a38 spike: treat null exception code as publishable in fallback gate 2026-05-07 03:50:08 +01:00
Prem Palanisamy f99f2cc1a9 Merge remote-tracking branch 'origin/1.9.x' into spike/isPublishable-migration
# Conflicts:
#	composer.json
#	composer.lock
2026-05-06 13:10:57 +01:00
Prem Palanisamy b094993f77 spike: convert worker throws to AppwriteException + isPublishable gate 2026-05-06 12:34:50 +01:00
Jake Barnby 6c6782dd28 Merge remote-tracking branch 'origin/1.9.x' into pr-12194-feat-insights-module
# Conflicts:
#	app/config/scopes/project.php
2026-05-06 20:33:40 +12:00
Jake BarnbyandClaude Opus 4.7 4660185a23 test(insights): fix CI failures from nesting refactor
Three breakages on the prior CI run:

1. PHPUnit 12 didn't propagate `@depends` data — chained tests got
   ArgumentCountError because they expected `array $data` but PHPUnit
   passed nothing. Convert all annotations to `#[Depends]` attributes,
   matching the modern style already used by the Migrations and
   VectorsDB suites.

2. `InsightsCustomConsoleTest` extends ProjectConsole, which doesn't
   set up a project API key. The trait's `serverHeaders()` hardcodes
   `x-appwrite-key`, so every Console test 401'd. Drop the Console
   class entirely — the manager Create endpoint is KEY-only by
   design, the Server class already exercises every code path, and a
   Console-side variant adds no real coverage.

3. `testCreateRequiresManagerScope` called `getNewKey()`, which lives
   on `ProjectCustom`. PHPStan flagged the call as undefined when the
   trait was analyzed against the (no-longer-existing) Console class.
   Move the test into `InsightsCustomServerTest.php` directly so it's
   only ever resolved against `ProjectCustom`.

Plus PHP 8.4 + match-exhaustive cleanups PHPStan caught while I was
in there:

- `?array $headers = null` instead of `array $headers = null` on every
  helper (PHP 8.4 deprecates implicit-nullable params).
- `?string $insightId = null` on `sampleInsight()`.
- `parentResourceType` match collapsed to `tablesDB => 'tables';
  databases/documentsDB/vectorsDB => 'collections'`. The earlier
  `$type` match already throws on unknown engine, so the fall-through
  default was unreachable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 20:20:54 +12:00
Jake BarnbyandClaude Opus 4.7 68c354e09b refactor(insights): nest insights API under reports
Insights are children of reports — make the URL hierarchy reflect that.

Endpoints:
- POST   /v1/manager/reports/:reportId/insights        (manager Create)
- GET    /v1/reports/:reportId/insights                (List)
- GET    /v1/reports/:reportId/insights/:insightId     (Get)
- PATCH  /v1/reports/:reportId/insights/:insightId     (Update)
- DELETE /v1/reports/:reportId/insights/:insightId     (Delete)

`reportId` moves from optional body field to required path param. All
endpoints fetch the report first (404 REPORT_NOT_FOUND if missing or
in another project), then verify the insight's `reportInternalId`
matches before doing anything else.

Side effects:
- Event names nested: `reports.[reportId].insights.[insightId].create`
  etc. Top-level `insights.*` event tree removed from events.php.
- Realtime channel parser handles the nested form: a `reports.{rid}`
  event lights up `reports`, `reports.{rid}` channels; a nested
  `reports.{rid}.insights.{iid}` event also lights up
  `reports.{rid}.insights` and `reports.{rid}.insights.{iid}`.
- Audit resource paths nested similarly:
  `report/{request.reportId}/insight/{response.$id}`.
- listInsights query validator drops `reportId` from
  ALLOWED_ATTRIBUTES — it's path-scoped now, not a query filter.

Tests:
- E2E helpers `createInsight`/`getInsight`/`listInsights`/
  `updateInsight`/`deleteInsight` all take `reportId` as the first
  argument.
- New `createFixtureReport()` helper for standalone validation tests
  that need a parent.
- Dropped `testCreateWithoutReport` — reportId is mandatory now.
- `testCreateRejectsUnknownReport` now exercises the path-level 404
  rather than a body-level check.
- `testGet` and `testUpdateMissing` exercise the
  wrong-reportId-but-valid-insightId case (returns
  `report_not_found`).
- `testList` asserts every result carries the path's reportId, plus a
  404 case for a nonexistent parent.
- `testCreateForEachEngine` and the standalone create-rejection tests
  inline-create their own fixture report and clean up after.
- `testListSurvivesEmptyDatabase` renamed to
  `testListSurvivesEmptyReport` and uses a fresh fixture report.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:53:54 +12:00
Jake BarnbyandClaude Opus 4.7 1f4111d6f2 test(insights): drop summary update from testUpdate
User Update only accepts severity + status now — analyzer-controlled
fields like summary flow through the manager Create endpoint. The
previous testUpdate sent `summary: 'Updated summary.'` and asserted
it changed; Utopia silently dropped the unknown param so the
assertion would have failed.

Trim the call to severity-only and verify the analyzer fields
(including summary) are preserved on the response.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:34:04 +12:00
Jake BarnbyandClaude Opus 4.7 0b72dba817 refactor(insights): drop CTA key field
`key` was a leftover from when CTAs were embedded JSON — there's no
remaining reason to require analyzers to invent a within-insight
identifier. The execution layer is gone (no `cta.key` event format),
insights are immutable from the user side (analyzers re-ingest by
delete + recreate, so idempotent matching never happens), and `label`
already covers human-facing identification. The console can group/sort
CTAs by `service`+`method` if needed.

- Schema: drop `key` attribute and the UNIQUE
  `(insightInternalId, key)` index from insightCTAs. Required fields
  are now `label`, `service`, `method` (+ optional `params`).
- Validator no longer requires `key`. Drop the dup-key normalization
  loop in the manager Create endpoint — there's no semantic
  uniqueness to enforce.
- Response model: `InsightCTA` keeps `$id` + standard headers,
  `insightId` backref, and the four functional fields.
- E2E: drop sampleCTA's `$key` parameter, drop the
  testCreateRejectsDuplicateCTAIds test entirely, rename empty-fields
  test to testCreateRejectsCTAWithEmptyLabel and update the missing-
  fields tests to drop `key` from their payloads.
- Unit tests rewritten to drop `key`.
- Comment on the `insights.ctas` virtual attribute updated to
  reference the renamed `insightCTAs` collection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:29:40 +12:00
Jake BarnbyandClaude Opus 4.7 c5dfc42a60 refactor(insights): rename ctas collection to insightCTAs
Disambiguate the platform-level collection name. Field/request-param
remains `ctas` (the embedded array on the insight response).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:12:17 +12:00
Jake BarnbyandClaude Opus 4.7 5404bfec75 refactor(insights): promote CTAs to own collection with backref
Embedding CTAs as a 16384-byte JSON blob on `insights` was the wrong
shape — they're real documents with their own lifecycle. Move them out.

Schema:
- New platform `ctas` collection. Each row carries `projectInternalId`,
  `projectId`, `insightInternalId`, `insightId` (backref), plus the
  CTA fields: `key`, `label`, `service`, `method`, `params`.
- Indexes: `(projectInternalId, insightInternalId)` for the subquery
  lookup and a UNIQUE `(insightInternalId, key)` so the per-insight
  uniqueness invariant lives at the DB layer (not just in PHP).
- The `ctas` field on `insights` becomes a virtual attribute backed by
  a new `subQueryInsightCTAs` filter that joins child docs at read
  time. Consumers still get CTAs embedded on the insight response —
  one round-trip from their perspective.
- The CTA descriptor's within-insight identifier renamed `id` → `key`
  (clashed with the document `$id`). Validator updated.

Endpoints:
- Manager Create now persists CTAs as separate `ctas` documents after
  the parent insight, then re-fetches the insight so the response
  carries the freshly-joined CTA list.
- User Update trimmed to user-controlled state only (`severity`,
  `status`). `title`, `summary`, `payload`, `ctas`, and `analyzedAt`
  are analyzer-controlled — analyzers re-ingest by deleting and
  POSTing again to the manager endpoint.
- Insight Delete cascades to CTAs.
- Report Delete cascades through Insights → CTAs.

Response model:
- InsightCTA gains the standard document headers (`$id`,
  `$createdAt`, `$updatedAt`) and an `insightId` backref. The
  caller-supplied identifier is now `key`.

Tests:
- E2E sampleCTA factory uses `key` everywhere; testCreate asserts the
  freshly-created CTA carries `$id`, `$createdAt`, `insightId`, and
  the right shape.
- Dropped the testUpdate*CTA* tests — user Update no longer accepts
  CTAs. testDismissViaUpdate now depends on testUpdate directly.
- Unit tests rewritten to validate `key` instead of `id`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:07:49 +12:00
Jake BarnbyandClaude Opus 4.7 38efdf18e2 feat(insights): add parent resource pointer
Eldad's review comment: insights about nested resources need a pointer
to the containing parent (the file-in-bucket pattern). Add three
optional fields:

- parentResourceType (plural noun, e.g. `tables`, `collections`)
- parentResourceId
- parentResourceInternalId

so an insight whose `resourceType=indexes` / `resourceId=_idx_status`
can also carry `parentResourceType=tables` / `parentResourceId=orders`
to identify the table that owns the index. All three are nullable for
top-level resources (e.g. a project-wide audit finding).

Schema, response model, manager Create endpoint, and the listInsights
query validator (parent fields are filterable). New compound index
`_key_project_parent_resource(projectInternalId, parentResourceType,
parentResourceId, $sequence)` to support the parent lookup pattern
the console will use ("show all insights for table X").

E2E factory generates a parent by default (engine-aware:
tables for tablesDB, collections for the others). New
testCreateWithoutParentResource exercises the top-level case;
testList gains a parent-resource filter assertion; testUpdate's
preserved-fields check picks up the new attributes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:25:34 +12:00
Jake BarnbyandClaude Opus 4.7 4fc3e9c386 refactor(insights): manager-only Create endpoint + native categories array
Insights are produced by internal Appwrite services (edge, executor,
background analyzers) — never by user clients. Move the ingestion
endpoint accordingly.

- Move Http/Insights/Create.php → Http/Manager/Insights/Create.php.
- Path: /v1/insights → /v1/manager/insights. SDK Method marked
  `hide: true` and namespaced under `manager` so generated SDKs don't
  expose it. Auth narrowed from [ADMIN, KEY] to [KEY] only.
- New scope `insights.manager`. Not granted by any user role
  (app/config/roles.php) — Cloud/edge teams configure their internal
  key issuance to grant it. `insights.write` description trimmed to
  the user-facing surface (update/dismiss/delete) since create is now
  manager-only.
- Reports, ListInsights, GetInsight, UpdateInsight, DeleteInsight
  remain at /v1/insights/*. Existing scopes unchanged.
- Reports `categories` switched from JSON-encoded string to a native
  array<string> column (size 64 per entry, up to 32 entries via the
  endpoint validator). MySQL JSON-array indexes are weak and we never
  query individual entries — read+rewrite only.
- E2E test API key in tests/e2e/Scopes/ProjectCustom.php gains
  insights.read/write/manager + reports.read/write so the manager
  endpoint is reachable from the test harness.
- E2E InsightsBase.createInsight() helper now POSTs /manager/insights.
- New testCreateRequiresManagerScope verifies a key with
  insights.read/write but no insights.manager is rejected with 401.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:20:49 +12:00
Jake BarnbyandClaude Opus 4.7 a1f64c6f71 refactor(insights): split CTA action into service + method
- InsightCTA model now exposes `service` (SDK namespace) and `method`
  (function name) instead of a single `action` string. Drops the
  string-splitting burden on every consumer and lets the console reach
  the right SDK method directly.

- Validator requires both `service` and `method` non-empty; same
  16-entry max still enforced.

- Endpoint normalization (Create + Update) splits the new shape into
  the persisted CTA descriptor.

- Constants split: INSIGHT_CTA_SERVICE_* (databases / tablesDB /
  documentsDB / vectorsDB) and INSIGHT_CTA_METHOD_* (createIndex).

- Insight model + InsightCTA model docs updated with the new field
  semantics and per-engine examples.

- E2E factory `sampleCTA($id, $engine)` emits the correct service and
  engine-appropriate params keys (tableId/columns for tablesDB;
  collectionId/attributes everywhere else). Engine matrix asserts
  `service` and `method` independently.

- Added e2e + unit coverage for the new failure modes (missing
  service, missing method, empty service, empty method).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:35:33 +12:00
Jake BarnbyandClaude Opus 4.7 8f79379b6e test(insights): full e2e + per-engine CTA action mapping
- Restructure InsightsBase trait with explicit helper methods
  (createInsight/getInsight/listInsights/updateInsight/deleteInsight,
  createReport/getReport/listReports/updateReport/deleteReport, plus
  sampleInsight/sampleCTA factories) — same shape ProxyBase uses.

- Add coverage for: report CRUD + duplicate-id rejection, invalid type
  rejection, list filtering by all allowed attributes, cursor
  pagination + missing-cursor, update preserving untouched fields,
  CTA validation edge cases (duplicate ids, empty fields, count > 16),
  dismissal round-trip + status filter, report cascade delete,
  unauthorized access (no server key), empty-result list.

- Engine-specific insight types (tablesDBIndex, documentsDBIndex,
  vectorsDBIndex, plus the legacy databaseIndex) so the CTA's `action`
  can map to the matching public API: databases.createIndex,
  tablesDB.createIndex, documentsDB.createIndex,
  vectorsDB.createIndex. dataProvider drives the engine matrix and
  asserts the right action lands in the persisted CTA. Constants for
  each action name live in app/init/constants.php.

- InsightCTA model docs spell out which action belongs to which engine
  and that the params keys differ between APIs (tableId/columns for
  tablesDB vs collectionId/attributes for the legacy / DocumentsDB /
  VectorsDB APIs).

- Insight model `type` description now lists every engine variant.

- CTAsTest gains coverage for object-shaped params, empty-action and
  empty-label rejection, and the default 16-entry cap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:25:49 +12:00
Jake BarnbyandClaude Opus 4.7 00565ea471 refactor(insights): metadata-only CTAs, platform DB, reports parent
Address review feedback on PR #12194:

- Pivot CTAs to pure descriptors (id/label/action/params). Drop the
  server-side execution layer: Action interface, registry, the
  databases.indexes.create CTA action, the params validator, the
  /v1/insights/:id/ctas/:id/executions endpoint, the InsightCTAExecution
  model, the INSIGHT_CTA_* errors, and the corresponding events. The
  console invokes the existing public API directly with the descriptor's
  action + params.

- Restore Databases\Indexes\Action.php to its pre-CTA shape and inline
  the index-create body back into Create.php (the createIndex helper
  was added solely for CTA reuse).

- Move insights collection from project DB to platform DB and add a
  parent reports collection alongside it. Insights carry projectId /
  projectInternalId for tenant scoping and an optional reportId for
  grouping. List endpoints filter by projectInternalId; Get/Update/
  Delete also enforce project ownership before touching the document.

- New Reports module with full CRUD (Create/Get/XList/Update/Delete),
  Report response model, Reports query validator, REPORT_NOT_FOUND /
  REPORT_ALREADY_EXISTS errors, reports.read / reports.write scopes,
  and reports.* event tree. Delete cascades to child insights.

- Update.php now mutates the loaded document via setAttribute (instead
  of passing a partial new Document), reuses CTAsValidator (instead of
  the looser ArrayList<JSON> + isset check), and rejects duplicate CTA
  ids.

- Create.php enforces unique CTA ids during normalization.

- CTAsValidator gained a configurable maxCount (default 16) so the
  Create path matches the Update path and the DB column size, and
  oversized payloads return a clean 400.

- Validator\Queries\Insights adds status and reportId to
  ALLOWED_ATTRIBUTES so dismissal / report workflows are filterable.

- Realtime channel parser guards $parts[1] for both insights and
  reports event names.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:46:07 +12:00
Prem Palanisamy 803f646239 Update composer.lock for utopia-php/migration da8205e 2026-05-05 17:19:40 +01:00
abhay-dev2901 f36105a7af fix: pass platform env vars to function schedulers 2026-05-05 17:23:50 +05:30
Prem Palanisamy 561b482e54 Restore original sanitizeErrors decode/strip/encode flow 2026-05-04 11:20:00 +01:00
Prem Palanisamy 3bb68ef455 Trim verbose comments in migrations worker outer catch 2026-05-04 11:03:51 +01:00
Prem Palanisamy 07d60bb36d Capture bubbled exception in migration error report
The catch block recorded status='failed' but didn't surface the bubbling
exception's message on the migration document. Setup-time failures (e.g.
"Source project not found for provided projectId") left the user looking
at status='failed' with errors=[].

Capture the throwable in the catch and include it in the consolidated
errors list when finally serializes to the migration document.
2026-05-04 10:23:40 +01:00
Prem Palanisamy 581fdb26dd Update composer.lock for utopia-php/migration branch pin 2026-05-04 08:57:56 +01:00
Prem Palanisamy c849e652b3 Pin utopia-php/migration to fix-migration-sentry-leak branch for testing
Wires the worker against the matching utopia-php/migration branch so
end-to-end testing of the Sentry-routing fix can run against both
sides of the change set.

Run `composer update utopia-php/migration --with-all-dependencies`
after pulling to refresh composer.lock. Revert before merging.
2026-05-04 08:36:38 +01:00
Prem Palanisamy c2e4bc2ae3 Simplify sanitizeErrors now that jsonSerialize doesn't emit trace
With Migration\Exception::jsonSerialize() no longer including the stack
trace, sanitizeErrors no longer needs to decode/strip/re-encode each
entry. Reduce it to a single json_encode pass.
2026-05-04 07:27:27 +01:00
Prem Palanisamy 4703e4ba14 Gate Sentry logging in migrations worker by exception type
The outer catch in the migrations worker now only calls logError when the
caught Throwable is not a MigrationException. User-facing setup errors
(invalid source type, missing project, etc.) are thrown as MigrationException
with appropriate codes and stay in the migration report only.

Removed the foreach loop that re-published collected errors to Sentry; with
the library-side fix in utopia-php/migration, items in $source->getErrors() /
$destination->getErrors() are by construction user errors that don't need
Sentry routing.

Hoisted setAttribute('errors', sanitizeErrors(...)) into finally so the
migration document always reflects the consolidated error list, including
on bug paths.
2026-05-04 05:05:00 +01:00
Jake BarnbyandClaude Opus 4.7 56ef3b4cfa refactor(insights): nest CTA params validator under resource path
Moves Validator/CTAParams/DatabasesCreateIndex to
Validator/CTA/Databases/Index/Create so the validator hierarchy mirrors
the action hierarchy (Insights/CTA/Action/Databases/Indexes/Create).
Also tightens the Update and CTA execute endpoint descriptions to call
out the dismissal-via-status flow and what the execution result carries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:11:30 +12:00
Jake BarnbyandClaude Opus 4.7 719b1885bf refactor(insights): share index-create body between CE action and CTA
Move the body of the public databases create-index endpoint into a new
final `createIndex(...)` helper on the abstract Indexes Action so both
the HTTP-facing CE action and the insights CTA share one source of truth
for index validation, persistence, and queue dispatch.

The CTA's `Appwrite\Insights\CTA\Action` becomes a small interface
(`getName()` + `execute()`); the dispatcher now calls `$action->execute(...)`
directly instead of poking at Utopia Action internals via `getCallback()`
and `getParams()`. The CTA's `Create` extends the CE Indexes `Create`
so it inherits `createIndex()` for free, while keeping a no-op constructor
to skip the HTTP route registration that runs in the parent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:49:44 +12:00
Jake BarnbyandClaude Opus 4.7 1d215b2840 revert(insights): drop insight/project param declarations on CTA actions
The CTA execution dispatcher invokes the action callback positionally
with values that have already been validated (insight fetched and
asserted non-empty in the endpoint, project pulled from the request
context). Re-validating them through param declarations adds noise
without catching anything. Drop the declarations and the validator
classes/tests created for them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:22:28 +12:00
Jake BarnbyandClaude Opus 4.7 0fbf31bd9a refactor(insights): drop Document suffix from Insight/Project validators
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:18:27 +12:00
Jake BarnbyandClaude Opus 4.7 4d560bdff2 feat(insights): validate insight and project documents in CTA actions
Replace the no-op `fn () => true` validators (gated by skipValidation)
on the `insight` and `project` params with dedicated InsightDocument and
ProjectDocument validators. They check that the injected value is a
non-empty Document with the expected attributes, so a misconfigured
dispatcher or unbound injection fails fast with a useful message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:12:41 +12:00
Jake BarnbyandClaude Opus 4.7 69c637c72d style(insights): use aliased import for DatabasesIndexesCreate registry entry
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:01:31 +12:00
Jake BarnbyandClaude Opus 4.7 242c7f75ad refactor(insights): nest databases create-index CTA under resource path
Move the action class from Action/DatabasesCreateIndex.php to
Action/Databases/Indexes/Create.php so the directory mirrors the
underlying resource hierarchy. Action name follows: databases.createIndex
becomes databases.indexes.create, with the constant renamed to
INSIGHT_CTA_ACTION_DATABASES_INDEXES_CREATE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:57:28 +12:00
Jake BarnbyandClaude Opus 4.7 f779c7aa3b refactor(insights): collapse dismissal into a status field on update
Dismissal was a sub-resource (POST /v1/insights/:id/dismissals) but a
dismissal is just a state transition, not a thing the client creates.
Drop the dedicated endpoint and add a `status` enum (`active` |
`dismissed`) to the insights schema, settable via the existing PATCH
update route. The server still derives `dismissedAt` and `dismissedBy`
on transition for audit/sorting, but the client-facing API is just a
single status toggle.

- Schema: add `status` attribute (default `active`)
- Constants: add `INSIGHT_STATUSES`
- Update endpoint: accept `status` param, derive dismissedAt/By on
  active <-> dismissed transitions
- Response model: add `status` rule
- Drop Insights/Dismissal/Create.php, the createInsightDismissal SDK
  method, the `insights.[id].dismissals.create` event, and the
  `insight.dismissal.create` audit
- E2E: replace testCreateDismissal with testDismissViaUpdate covering
  both directions of the toggle

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:36:31 +12:00
Jake BarnbyandClaude Opus 4.7 cd539d972a refactor(insights): capitalise CTA acronym in identifiers
Project-specific override of the default camelCase-acronyms convention:
namespaces, class names, file paths, and SDK method names use `CTA` in
all caps. Touches all insights surfaces — directories, response models,
validators, container resource keys, and SDK method names like
`createInsightCTAExecution`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:27:45 +12:00
Jake BarnbyandClaude Opus 4.7 d188bd7a2e refactor(insights): register CTA registry on the global $register
Persist the insight CTA registry across requests by attaching it to the
boot-time global $register, mirroring the pattern used for `geodb`,
`passwordsDictionary`, `hooks`, etc. The container resource now looks
the registry up from `$register` instead of rebuilding it on every
request.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:22:10 +12:00
Jake Barnby 3c4aceea48 ci(insights): add Insights to e2e test matrix 2026-05-01 14:04:49 +12:00
Jake Barnby e0d5164af2 test(insights): split e2e into base trait and server/console overlays
Extract the existing test methods into InsightsBase trait and
provide thin Custom{Server,Console}Test classes that compose the
base with the appropriate scope/side traits. Updates the dismissal
and CTA execution tests to the new sub-resource paths.
2026-05-01 14:04:36 +12:00
Jake Barnby da5a137b98 refactor(insights): nest CTA execution and dismissal as sub-resources
Move POST /v1/insights/:id/dismiss to /v1/insights/:id/dismissals
and POST /v1/insights/:id/ctas/:ctaId/trigger to
/v1/insights/:id/ctas/:ctaId/executions, with the corresponding
class moves into Http/Insights/Dismissal/Create.php and
Http/CTA/Execution/Create.php. Rename the response model to
InsightCtaExecution and update events.php to surface dismissal
and execution as resource events with create verbs. The reshape
matches the rest of the API where verbs hang off plural sub-resources.
2026-05-01 14:04:31 +12:00
Jake Barnby d15be21923 refactor(insights): use Ctas validator on create endpoint
Replace the inline CTA shape check with a dedicated Utopia validator
so the constraint surfaces as a normal 400 with a useful message
rather than a generic argument-invalid exception.
2026-05-01 13:42:01 +12:00
Jake Barnby f5d6f6e27c refactor(insights): use Utopia Platform Action and Registry
Replace the bespoke CtaAction/Registry with Utopia\Platform\Action
and Utopia\Registry\Registry. Implement DatabasesCreateIndex with
the full createDocument('indexes') + queue path used by the existing
indexes endpoint, validated via a dedicated Utopia validator. Drop
the obsolete unit tests (custom-Action contract) in favor of
validator-focused tests.
2026-05-01 13:40:27 +12:00
Jake Barnby 8beae59454 fix(insights): schema corrections from review
- Add inline comments listing possible values for enum-bounded attributes
- Use VAR_ID for resourceInternalId
- Drop trailing 0 from composite index lengths and use $sequence over $createdAt
- Drop redundant ORDER_ASC values (default direction)
2026-05-01 13:30:23 +12:00
Jake BarnbyandClaude Opus 4.7 68dc974271 test(insights): unit and e2e tests
Unit tests cover the CTA registry register/resolve/has/all behaviour
and the DatabasesCreateIndex action's name, scope, validation surface,
and not-implemented execute path. The e2e suite runs the full CRUD
lifecycle, dismiss, and CTA trigger paths against a real cloud project,
including authentication boundaries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:40:59 +12:00
Jake BarnbyandClaude Opus 4.7 236e594195 feat(insights): add dismiss and CTA trigger endpoints
Dismiss is a convenience that stamps `dismissedAt` and `dismissedBy` so
analyzers can see an insight has been acknowledged without losing the
record. CTA trigger looks up the action in the runtime registry,
validates the params blob, executes the action, and returns the result
as an `InsightCtaResult`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:40:53 +12:00
Jake BarnbyandClaude Opus 4.7 5a8be81484 feat(insights): add CRUD endpoints
Adds the create, get, list, update, and delete endpoints under the
`insights` SDK namespace. Mutating endpoints are admin/key-only because
insights are produced by analyzers; reads are open to sessions and JWTs
so console UIs can surface them. Updates use sparse documents so unset
fields keep their existing value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:40:47 +12:00
Jake BarnbyandClaude Opus 4.7 7f7be46547 feat(insights): add module skeleton and registration
Adds the platform module so the http service is discoverable, registers
it on the Appwrite platform alongside the other modules, exposes the
`insights` and `insights.[insightId]` realtime channels via the messaging
adapter, and ships the queries validator covering the indexed attributes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:40:34 +12:00
Jake BarnbyandClaude Opus 4.7 e1ddcd051c feat(insights): add schema, scopes, events, errors, constants
Wires the platform glue for insights: the `insights` collection on the
project database, the `insights.read` / `insights.write` scopes, the
`insights.[insightId]` event tree (including the nested `ctas.[ctaId].trigger`
event), the typed exceptions, and the runtime CTA registry resource.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:40:28 +12:00
Jake BarnbyandClaude Opus 4.7 1c8cc6fc92 feat(insights): add response models
Adds the Insight, InsightCta, and InsightCtaResult response models and
registers their model identifiers on the Response class so endpoints can
serialise insights consistently across the SDK surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:40:22 +12:00
Jake BarnbyandClaude Opus 4.7 1889ccdd12 feat(insights): add CTA framework
Introduces the call-to-action framework: a server-side registry plus an
Action interface that lets analyzers offer one-click remediations alongside
each insight. Ships a stub `databases.createIndex` action whose execute()
defers to a future cloud-side implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:40:16 +12:00
adityaoberai 7648978cfa Add codex plugin to SDKs 2026-04-29 19:22:46 +05:30
Prem Palanisamy 043e7efb55 Update phpseclib to 3.0.50 to fix CVE-2026-32935 2026-03-23 13:46:58 +00:00
Prem Palanisamy 91cda84772 Update composer.lock after rebase onto 1.9.x 2026-03-23 13:46:58 +00:00
Prem Palanisamy 90641041ef Pass projectInternalId to destination migration 2026-03-23 13:46:58 +00:00
Prem Palanisamy e2f2ce3da8 Change console-key endpoint from GET to POST 2026-03-23 13:46:58 +00:00
Prem Palanisamy 374e1f8782 Add unit tests for scopedProjectId and source fields in Key::decode() 2026-03-23 13:46:58 +00:00
Prem Palanisamy b0a0c1552e Update composer.lock for utopia-php/migration dev branch 2026-03-23 13:46:58 +00:00
Prem Palanisamy 4020335aac Add E2E test for platform migration 2026-03-23 13:46:58 +00:00
Prem Palanisamy 75c26259a1 Add expire field to migration console key response 2026-03-23 13:46:58 +00:00
Prem Palanisamy 4e8feff46e Add platform migration support 2026-03-23 13:46:58 +00:00
Prem Palanisamy 4438e08f64 Add platform DB access via console-key endpoint 2026-03-23 13:46:57 +00:00
232 changed files with 7482 additions and 3221 deletions
+1 -1
View File
@@ -25,6 +25,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: AI Moderator
uses: github/ai-moderator@v1
uses: github/ai-moderator@81159c370785e295c97461ade67d7c33576e9319 # v1.1.4
with:
token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Issue Labeler
uses: github/issue-labeler@v3.4
uses: github/issue-labeler@c1b0f9f52a63158c4adc09425e858e87b32e9685 # v3.4
with:
configuration-path: .github/labeler.yml
enable-versioned-regex: false
+56 -63
View File
@@ -32,7 +32,7 @@ jobs:
actions: read
security-events: write
contents: read
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@v2.3.3"
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@c51854704019a247608d928f370c98740469d4b5" # v2.3.5
security:
name: Checks / Image
@@ -43,13 +43,13 @@ jobs:
security-events: write
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
submodules: 'recursive'
- name: Build the Docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
push: false
@@ -58,7 +58,7 @@ jobs:
target: production
- name: Run Trivy vulnerability scanner on image
uses: aquasecurity/trivy-action@0.35.0
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: 'pr_image:${{ github.sha }}'
format: 'sarif'
@@ -66,7 +66,7 @@ jobs:
severity: 'CRITICAL,HIGH'
- name: Run Trivy vulnerability scanner on source code
uses: aquasecurity/trivy-action@0.35.0
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: 'fs'
scan-ref: '.'
@@ -76,14 +76,14 @@ jobs:
skip-setup-trivy: true
- name: Upload image scan results
uses: github/codeql-action/upload-sarif@v4
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
if: always() && hashFiles('trivy-image-results.sarif') != ''
with:
sarif_file: 'trivy-image-results.sarif'
category: 'trivy-image'
- name: Upload source code scan results
uses: github/codeql-action/upload-sarif@v4
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
if: always() && hashFiles('trivy-fs-results.sarif') != ''
with:
sarif_file: 'trivy-fs-results.sarif'
@@ -94,10 +94,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup PHP
uses: shivammathur/setup-php@v2
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0
with:
php-version: '8.3'
tools: composer:v2
@@ -119,7 +119,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 2
@@ -127,7 +127,7 @@ jobs:
if: github.event_name == 'pull_request'
- name: Setup PHP
uses: shivammathur/setup-php@v2
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0
with:
php-version: '8.3'
tools: composer:v2
@@ -144,10 +144,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup PHP
uses: shivammathur/setup-php@v2
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0
with:
php-version: '8.3'
tools: composer:v2
@@ -157,7 +157,7 @@ jobs:
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
- name: Cache PHPStan result cache
uses: actions/cache@v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .phpstan-cache
key: phpstan-${{ github.sha }}
@@ -172,10 +172,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup PHP
uses: shivammathur/setup-php@v2
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0
with:
php-version: '8.3'
extensions: swoole
@@ -193,10 +193,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24'
@@ -212,7 +212,7 @@ jobs:
steps:
- name: Generate matrix
id: generate
uses: actions/github-script@v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
@@ -253,28 +253,28 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build and push Appwrite
uses: docker/build-push-action@v6
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
push: true
@@ -297,16 +297,16 @@ jobs:
packages: read
steps:
- name: checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -327,7 +327,7 @@ jobs:
run: docker compose exec -T appwrite vars
- name: Run Unit Tests
uses: itznotabug/php-retry@v3
uses: itznotabug/php-retry@d6bef45a8bff490babfb613e33b00d133e4062f0 # v3
with:
max_attempts: 2
retry_wait_seconds: 60
@@ -350,16 +350,16 @@ jobs:
packages: read
steps:
- name: checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -385,7 +385,7 @@ jobs:
done
- name: Run General Tests
uses: itznotabug/php-retry@v3
uses: itznotabug/php-retry@d6bef45a8bff490babfb613e33b00d133e4062f0 # v3
with:
max_attempts: 2
retry_wait_seconds: 60
@@ -406,7 +406,7 @@ jobs:
e2e_service:
name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }}
runs-on: ${{ matrix.runner || 'ubuntu-latest' }}
runs-on: ${{ matrix.runner || format('runs-on={0}/runner=4cpu-linux-x64/volume=120g/spot=false', github.run_id) }}
needs: [build, matrix]
permissions:
contents: read
@@ -427,6 +427,7 @@ jobs:
FunctionsSchedule,
GraphQL,
Health,
Advisor,
Locale,
Projects,
Realtime,
@@ -445,26 +446,18 @@ jobs:
]
include:
- service: Databases
runner: blacksmith-4vcpu-ubuntu-2404
runner: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/volume=120g/spot=false
paratest_processes: 3
timeout_minutes: 30
- service: Sites
runner: blacksmith-4vcpu-ubuntu-2404
- service: Functions
runner: blacksmith-4vcpu-ubuntu-2404
- service: Avatars
runner: blacksmith-4vcpu-ubuntu-2404
- service: Realtime
runner: blacksmith-4vcpu-ubuntu-2404
- service: TablesDB
runner: blacksmith-4vcpu-ubuntu-2404
runner: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/volume=120g/spot=false
paratest_processes: 3
timeout_minutes: 30
- service: Migrations
paratest_processes: 1
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set environment
run: |
@@ -488,13 +481,13 @@ jobs:
fi
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -525,7 +518,7 @@ jobs:
done
- name: Run tests
uses: itznotabug/php-retry@v3
uses: itznotabug/php-retry@d6bef45a8bff490babfb613e33b00d133e4062f0 # v3
with:
max_attempts: 2
retry_wait_seconds: 60
@@ -573,18 +566,18 @@ jobs:
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -607,7 +600,7 @@ jobs:
docker compose up -d --quiet-pull --wait
- name: Run tests
uses: itznotabug/php-retry@v3
uses: itznotabug/php-retry@d6bef45a8bff490babfb613e33b00d133e4062f0 # v3
with:
max_attempts: 2
retry_wait_seconds: 60
@@ -640,16 +633,16 @@ jobs:
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -679,7 +672,7 @@ jobs:
done
- name: Run tests
uses: itznotabug/php-retry@v3
uses: itznotabug/php-retry@d6bef45a8bff490babfb613e33b00d133e4062f0 # v3
with:
max_attempts: 2
retry_wait_seconds: 60
@@ -711,18 +704,18 @@ jobs:
packages: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -735,7 +728,7 @@ jobs:
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}:after
- name: Setup k6
uses: grafana/setup-k6-action@ffe7d7290dfa715e48c2ccc924d068444c94bde2
uses: grafana/setup-k6-action@db07bd9765aac508ef18982e52ab937fe633a065 # v1.2.1
with:
k6-version: ${{ env.K6_VERSION }}
@@ -774,7 +767,7 @@ jobs:
- name: Benchmark before
if: steps.benchmark_before_start.outcome == 'success'
continue-on-error: true
uses: grafana/run-k6-action@a15e2072ede004e8d46141e33d7f7dad8ad08d9d
uses: grafana/run-k6-action@de51a7390bdf0ac85a3bef493691bd71d4c7c158 # v1.4.0
env:
APPWRITE_ENDPOINT: 'http://localhost/v1'
APPWRITE_BENCHMARK_ITERATIONS: '5'
@@ -826,7 +819,7 @@ jobs:
- name: Benchmark after
id: benchmark_after
continue-on-error: true
uses: grafana/run-k6-action@a15e2072ede004e8d46141e33d7f7dad8ad08d9d
uses: grafana/run-k6-action@de51a7390bdf0ac85a3bef493691bd71d4c7c158 # v1.4.0
env:
APPWRITE_ENDPOINT: 'http://localhost/v1'
APPWRITE_BENCHMARK_ITERATIONS: '5'
@@ -846,7 +839,7 @@ jobs:
- name: Comment on PR
if: always()
uses: actions/github-script@v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
BENCHMARK_BASE_REF: ${{ github.event.pull_request.base.ref }}
BENCHMARK_HEAD_REF: ${{ github.event.pull_request.head.ref }}
@@ -856,7 +849,7 @@ jobs:
await comment({ github, context, core });
- name: Save results
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ !cancelled() }}
with:
name: benchmark-results
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Cleanup
run: |
+4 -4
View File
@@ -34,7 +34,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -47,14 +47,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -68,4 +68,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
+6 -6
View File
@@ -10,13 +10,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Build the Docker image
run: DOCKER_BUILDKIT=1 docker build . --target production -t appwrite_image:latest
- name: Run Trivy vulnerability scanner on image
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: 'appwrite_image:latest'
format: 'sarif'
@@ -24,7 +24,7 @@ jobs:
ignore-unfixed: 'false'
severity: 'CRITICAL,HIGH'
- name: Upload Docker Image Scan Results
uses: github/codeql-action/upload-sarif@v4
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
if: always() && hashFiles('trivy-image-results.sarif') != ''
with:
sarif_file: 'trivy-image-results.sarif'
@@ -35,16 +35,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run Trivy vulnerability scanner on filesystem
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: 'fs'
format: 'sarif'
output: 'trivy-fs-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Code Scan Results
uses: github/codeql-action/upload-sarif@v4
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
if: always() && hashFiles('trivy-fs-results.sarif') != ''
with:
sarif_file: 'trivy-fs-results.sarif'
+6 -6
View File
@@ -12,33 +12,33 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 2
submodules: recursive
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: appwrite/cloud
tags: |
type=ref,event=tag
- name: Build & Publish to DockerHub
uses: docker/build-push-action@v6
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64
+6 -6
View File
@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -20,20 +20,20 @@ jobs:
submodules: recursive
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: appwrite/appwrite
tags: |
@@ -42,7 +42,7 @@ jobs:
type=semver,pattern={{major}}
- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set SDK type
id: set-sdk
@@ -49,7 +49,7 @@ jobs:
docker compose exec appwrite sdks --platform=${{ steps.set-sdk.outputs.platform }} --sdk=${{ steps.set-sdk.outputs.sdk_type }} --version=latest --git=no
sudo chown -R $USER:$USER ./app/sdks/${{ steps.set-sdk.outputs.platform }}-${{ steps.set-sdk.outputs.sdk_type }}
- uses: actions/setup-node@v4
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: "This issue has been labeled as a 'question', indicating that it requires additional information from the requestor. It has been inactive for 7 days. If no further activity occurs, this issue will be closed in 14 days."
+8
View File
@@ -115,6 +115,14 @@ Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `
- Never hardcode credentials -- use environment variables.
- Code changes may require container restart. No central log location -- check relevant containers.
## Tracing with Utopia Span
In handlers, only call `Span::add($key, $value)`. **Never** call `Span::init`, `Span::error`, or `Span::finish` -- lifecycle is owned by the entry-point harness (`app/http.php`, `app/worker.php`, `app/realtime.php`, `Bus::dispatch`). For selective export, filter in the sampler in `app/init/span.php`.
Keys are `snake_case` with dots only for child relationships: `project.id` (id of project), `storage.bucket.id`. No dot otherwise: `inbound_bytes`, not `inbound.bytes`. No camelCase, no bare top-level keys (`function.id`, not `functionId`).
Cross-cutting identifiers (`project.id`, `function.id`, `user.id`) live at the top level, not under a subsystem (no `realtime.project.id`). The trace sampler and downstream filters look them up by the canonical key.
## Patch release process
For bumping patch versions (e.g., `1.9.0` -> `1.9.1`), follow the checklist in `.claude/skills/patch-release-checklist/SKILL.md`. It covers the 4 files that must be updated, console image bumps, CHANGES.md updates, and common pitfalls to avoid.
+10 -8
View File
@@ -2,10 +2,10 @@
require_once __DIR__ . '/init.php';
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Platform\Appwrite;
@@ -281,12 +281,14 @@ $container->set('publisherForStatsResources', fn (Publisher $publisher) => new S
$publisher,
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
), ['publisher']);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
$publisher,
new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
), ['publisher']);
$container->set('publisherForDeletes', fn (Publisher $publisher) => new DeletePublisher(
$publisher,
new Queue(System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME))
), ['publisher']);
$container->set('logError', function (Registry $register) {
return function (Throwable $error, string $namespace, string $action) use ($register) {
Console::error('[Error] Timestamp: ' . date('c', time()));
+434
View File
@@ -1956,6 +1956,440 @@ $platformCollections = [
'attributes' => [],
'indexes' => []
],
'reports' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('reports'),
'name' => 'Reports',
'attributes' => [
[
'$id' => ID::custom('projectInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('projectId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('appInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('appId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('type'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('title'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('summary'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
// Resource type the report is about. Plural noun, e.g. databases, sites, urls.
'$id' => ID::custom('targetType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
// Free-form target identifier (URL for lighthouse, resource ID for db).
// Indexed by `_key_project_target` with an explicit prefix length.
'$id' => ID::custom('target'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
// Category strings, e.g. 'performance', 'accessibility'. Native array
// column — we never query on individual entries (MySQL JSON-array
// indexes are weak), this is read+rewrite only.
'$id' => ID::custom('categories'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => false,
'default' => null,
'array' => true,
'filters' => [],
],
[
// Virtual attribute — insights live in the `insights` collection
// back-referenced by `reportInternalId`. The subQuery filter joins
// them at read time.
'$id' => ID::custom('insights'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['subQueryReportInsights'],
],
[
'$id' => ID::custom('analyzedAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_project_app_type'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'appInternalId', 'type'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_target'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'appInternalId', 'targetType', 'target'],
'lengths' => [null, null, null, 700],
'orders' => [],
],
],
],
'insights' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('insights'),
'name' => 'Insights',
'attributes' => [
[
'$id' => ID::custom('projectInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('projectId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('reportInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('reportId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('type'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('severity'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('status'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16,
'signed' => true,
'required' => true,
'default' => 'active',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('resourceType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('resourceId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('resourceInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('parentResourceType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 64,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('parentResourceId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('parentResourceInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('title'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('summary'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('ctas'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('analyzedAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('dismissedAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('dismissedBy'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_project_report'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'reportInternalId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_resource'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'resourceType', 'resourceId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_parent_resource'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'parentResourceType', 'parentResourceId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_type'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'type'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_severity'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'severity'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_status'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'status'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_project_dismissedAt'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'dismissedAt'],
'lengths' => [],
'orders' => [Database::ORDER_ASC, Database::ORDER_DESC],
],
],
],
];
// Organization API keys subquery
+44
View File
@@ -1246,6 +1246,26 @@ return [
'description' => 'The specified database type is not supported for CSV import or export operations.',
'code' => 400,
],
Exception::MIGRATION_SOURCE_PROJECT_ID_REQUIRED => [
'name' => Exception::MIGRATION_SOURCE_PROJECT_ID_REQUIRED,
'description' => 'A source projectId is required for Appwrite migrations. Provide it in the migration credentials.',
'code' => 400,
],
Exception::MIGRATION_SOURCE_PROJECT_NOT_FOUND => [
'name' => Exception::MIGRATION_SOURCE_PROJECT_NOT_FOUND,
'description' => 'The source project for the provided projectId was not found. Verify the projectId and the API key has access to it.',
'code' => 404,
],
Exception::MIGRATION_SOURCE_TYPE_INVALID => [
'name' => Exception::MIGRATION_SOURCE_TYPE_INVALID,
'description' => 'The migration source type is invalid. Use one of the supported source types.',
'code' => 400,
],
Exception::MIGRATION_DESTINATION_TYPE_INVALID => [
'name' => Exception::MIGRATION_DESTINATION_TYPE_INVALID,
'description' => 'The migration destination type is invalid. Use one of the supported destination types.',
'code' => 400,
],
/** Realtime */
Exception::REALTIME_MESSAGE_FORMAT_INVALID => [
@@ -1433,4 +1453,28 @@ return [
'description' => 'The maximum number of mock phones for this project has been reached.',
'code' => 400,
],
/** Advisor */
Exception::INSIGHT_NOT_FOUND => [
'name' => Exception::INSIGHT_NOT_FOUND,
'description' => 'Insight with the requested ID could not be found.',
'code' => 404,
],
Exception::INSIGHT_ALREADY_EXISTS => [
'name' => Exception::INSIGHT_ALREADY_EXISTS,
'description' => 'Insight with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
/** Reports */
Exception::REPORT_NOT_FOUND => [
'name' => Exception::REPORT_NOT_FOUND,
'description' => 'Report with the requested ID could not be found.',
'code' => 404,
],
Exception::REPORT_ALREADY_EXISTS => [
'name' => Exception::REPORT_ALREADY_EXISTS,
'description' => 'Report with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
];
+29 -1
View File
@@ -426,5 +426,33 @@ return [
'update' => [
'$description' => 'This event triggers when a proxy rule is updated.',
]
]
],
'reports' => [
'$model' => Response::MODEL_REPORT,
'$resource' => true,
'$description' => 'This event triggers on any report event.',
'create' => [
'$description' => 'This event triggers when a report is created.',
],
'update' => [
'$description' => 'This event triggers when a report is updated.',
],
'delete' => [
'$description' => 'This event triggers when a report is deleted.',
],
'insights' => [
'$model' => Response::MODEL_INSIGHT,
'$resource' => true,
'$description' => 'This event triggers on any insight event.',
'create' => [
'$description' => 'This event triggers when an insight is created.',
],
'update' => [
'$description' => 'This event triggers when an insight is updated.',
],
'delete' => [
'$description' => 'This event triggers when an insight is deleted.',
],
],
],
];
+4
View File
@@ -103,6 +103,10 @@ $admins = [
'tokens.write',
'schedules.read',
'schedules.write',
'insights.read',
'insights.write',
'reports.read',
'reports.write',
];
return [
+18
View File
@@ -361,4 +361,22 @@ return [
'description' => 'Access to create, update, and delete resources under VCS service.',
'category' => 'Other',
],
// Advisor
'insights.read' => [
'description' => 'Access to read insights under Advisor service.',
'category' => 'Advisor',
],
'insights.write' => [
'description' => 'Reserved for Advisor insight ingestion outside CE.',
'category' => 'Advisor',
],
'reports.read' => [
'description' => 'Access to read reports under Advisor service.',
'category' => 'Advisor',
],
'reports.write' => [
'description' => 'Access to delete reports under Advisor service.',
'category' => 'Advisor',
],
];
+20
View File
@@ -320,6 +320,26 @@ return [
'repoBranch' => 'main',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/claude-plugin/CHANGELOG.md'),
],
[
'key' => 'codex-plugin',
'name' => 'CodexPlugin',
'version' => '0.1.1',
'url' => 'https://github.com/appwrite/codex-plugin.git',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'spec' => 'static',
'family' => APP_SDK_PLATFORM_STATIC,
'prism' => 'codex-plugin',
'source' => \realpath(__DIR__ . '/../sdks/static-codex-plugin'),
'gitUrl' => 'git@github.com:appwrite/codex-plugin.git',
'gitRepoName' => 'codex-plugin',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'repoBranch' => 'main',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/codex-plugin/CHANGELOG.md'),
],
],
],
+15 -1
View File
@@ -308,5 +308,19 @@ return [
'optional' => true,
'icon' => '/images/services/messaging.png',
'platforms' => ['client', 'server', 'console'],
]
],
'advisor' => [
'key' => 'advisor',
'name' => 'Advisor',
'subtitle' => 'The Advisor service surfaces actionable reports about your project resources, with CTA descriptors for one-click remediation in the console.',
'description' => '/docs/services/advisor.md',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/server/advisor',
'tests' => true,
'optional' => true,
'icon' => '/images/services/insights.png',
'platforms' => ['server', 'console'],
],
];
+9
View File
@@ -1336,6 +1336,15 @@ return [
'category' => 'Migrations',
'description' => '',
'variables' => [
[
'name' => '_APP_MIGRATION_HOST',
'description' => 'Internal hostname the migrations worker uses to reach this instance\'s API (for migrations and CSV/JSON imports & exports). Defaults to \'appwrite\', the API service name in the standard Docker Compose setup. Only change this for non-standard deployments.',
'introduction' => '1.9.0',
'default' => 'appwrite',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_MIGRATIONS_FIREBASE_CLIENT_ID',
'description' => 'Google OAuth client ID. You can find it in your GCP application settings.',
+174 -166
View File
@@ -11,10 +11,13 @@ use Appwrite\Auth\Validator\PersonalData;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Bus\Events\SessionCreated;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Message\Messaging as MessagingMessage;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Redirect;
@@ -332,15 +335,15 @@ Http::post('/v1/account')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -470,9 +473,9 @@ Http::delete('/v1/account')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('authorization')
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes, Authorization $authorization) {
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, DeletePublisher $publisherForDeletes, Authorization $authorization) {
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
@@ -496,9 +499,11 @@ Http::delete('/v1/account')
$dbForProject->deleteDocument('users', $user->getId());
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($user);
$publisherForDeletes->enqueue(new DeleteMessage(
project: $project,
type: DELETE_TYPE_DOCUMENT,
document: $user,
));
$queueForEvents
->setParam('userId', $user->getId())
@@ -580,12 +585,12 @@ Http::delete('/v1/account/sessions')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('store')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, DeletePublisher $publisherForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
$protocol = $request->getProtocol();
$sessions = $user->getAttribute('sessions', []);
@@ -615,10 +620,11 @@ Http::delete('/v1/account/sessions')
$queueForEvents
->setPayload($response->output($session, Response::MODEL_SESSION));
$queueForDeletes
->setType(DELETE_TYPE_SESSION_TARGETS)
->setDocument($session)
->trigger();
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_SESSION_TARGETS,
document: $session,
));
}
}
@@ -712,12 +718,12 @@ Http::delete('/v1/account/sessions/:sessionId')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('store')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, DeletePublisher $publisherForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
$protocol = $request->getProtocol();
$sessionId = ($sessionId === 'current')
@@ -759,10 +765,11 @@ Http::delete('/v1/account/sessions/:sessionId')
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION));
$queueForDeletes
->setType(DELETE_TYPE_SESSION_TARGETS)
->setDocument($session)
->trigger();
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_SESSION_TARGETS,
document: $session,
));
$response->noContent();
return;
@@ -1676,15 +1683,15 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
$failureRedirect(Exception::USER_EMAIL_FREE);
}
@@ -1817,15 +1824,15 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
$failureRedirect(Exception::USER_EMAIL_FREE);
}
@@ -2113,12 +2120,12 @@ Http::post('/v1/account/tokens/magic-url')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
->inject('queueForMails')
->inject('publisherForMails')
->inject('plan')
->inject('proofForPassword')
->inject('platform')
->inject('authorization')
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, array $plan, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@@ -2175,15 +2182,15 @@ Http::post('/v1/account/tokens/magic-url')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -2304,6 +2311,7 @@ Http::post('/v1/account/tokens/magic-url')
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
$smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -2321,13 +2329,6 @@ Http::post('/v1/account/tokens/magic-url')
$replyToName = $smtp['replyToName'];
}
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '');
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -2348,11 +2349,17 @@ Http::post('/v1/account/tokens/magic-url')
$subject = $customTemplate['subject'] ?? $subject;
}
$queueForMails
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $replyToEmail,
'replyToName' => $replyToName,
'senderEmail' => $senderEmail,
'senderName' => $senderName,
];
}
$projectName = $project->getAttribute('name');
@@ -2374,18 +2381,17 @@ Http::post('/v1/account/tokens/magic-url')
'team' => '',
];
$queueForMails
->setSubject($subject)
->setPreview($preview)
->setBody($body)
->appendVariables($emailVariables)
->setRecipient($email);
if ($project->getId() === 'console') {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $email,
subject: $subject,
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $project->getId() === 'console' ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
$token->setAttribute('secret', $tokenSecret);
@@ -2436,12 +2442,12 @@ Http::post('/v1/account/tokens/email')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
->inject('queueForMails')
->inject('publisherForMails')
->inject('plan')
->inject('proofForPassword')
->inject('proofForCode')
->inject('authorization')
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, array $plan, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@@ -2496,15 +2502,15 @@ Http::post('/v1/account/tokens/email')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -2633,6 +2639,7 @@ Http::post('/v1/account/tokens/email')
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
$smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -2650,13 +2657,6 @@ Http::post('/v1/account/tokens/email')
$replyToName = $smtp['replyToName'];
}
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '');
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -2677,11 +2677,17 @@ Http::post('/v1/account/tokens/email')
$subject = $customTemplate['subject'] ?? $subject;
}
$queueForMails
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $replyToEmail,
'replyToName' => $replyToName,
'senderEmail' => $senderEmail,
'senderName' => $senderName,
];
}
$projectName = $project->getAttribute('name');
@@ -2717,20 +2723,18 @@ Http::post('/v1/account/tokens/email')
]);
}
$queueForMails
->setSubject($subject)
->setPreview($preview)
->setBody($body)
->setBodyTemplate($bodyTemplate)
->appendVariables($emailVariables)
->setRecipient($email);
// since this is console project, set email sender name!
if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $email,
subject: $subject,
bodyTemplate: $bodyTemplate,
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
$token->setAttribute('secret', $tokenSecret);
@@ -2880,7 +2884,7 @@ Http::post('/v1/account/tokens/phone')
->inject('platform')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('locale')
->inject('timelimit')
->inject('usage')
@@ -2888,7 +2892,7 @@ Http::post('/v1/account/tokens/phone')
->inject('store')
->inject('proofForCode')
->inject('authorization')
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, Context $usage, array $plan, Store $store, ProofsCode $proofForCode, Authorization $authorization) {
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, MessagingPublisher $publisherForMessaging, Locale $locale, callable $timelimit, Context $usage, array $plan, Store $store, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -3021,11 +3025,13 @@ Http::post('/v1/account/tokens/phone')
],
]);
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_INTERNAL)
->setMessage($messageDoc)
->setRecipients([$phone])
->setProviderType(MESSAGE_TYPE_SMS);
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_INTERNAL,
project: $project,
message: $messageDoc,
recipients: [$phone],
providerType: MESSAGE_TYPE_SMS,
));
$helper = PhoneNumberUtil::getInstance();
try {
@@ -3417,15 +3423,15 @@ Http::patch('/v1/account/email')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -3681,11 +3687,11 @@ Http::post('/v1/account/recovery')
->inject('project')
->inject('platform')
->inject('locale')
->inject('queueForMails')
->inject('publisherForMails')
->inject('queueForEvents')
->inject('proofForToken')
->inject('authorization')
->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Mail $queueForMails, Event $queueForEvents, ProofsToken $proofForToken, Authorization $authorization) {
->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, MailPublisher $publisherForMails, Event $queueForEvents, ProofsToken $proofForToken, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
@@ -3768,6 +3774,7 @@ Http::post('/v1/account/recovery')
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
$smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -3785,13 +3792,6 @@ Http::post('/v1/account/recovery')
$replyToName = $smtp['replyToName'];
}
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '');
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -3812,11 +3812,17 @@ Http::post('/v1/account/recovery')
$subject = $customTemplate['subject'] ?? $subject;
}
$queueForMails
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $replyToEmail,
'replyToName' => $replyToName,
'senderEmail' => $senderEmail,
'senderName' => $senderName,
];
}
$emailVariables = [
@@ -3829,19 +3835,18 @@ Http::post('/v1/account/recovery')
'team' => ''
];
$queueForMails
->setRecipient($profile->getAttribute('email', ''))
->setName($profile->getAttribute('name', ''))
->setBody($body)
->appendVariables($emailVariables)
->setSubject($subject)
->setPreview($preview);
if ($project->getId() === 'console') {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $profile->getAttribute('email', ''),
name: $profile->getAttribute('name', ''),
subject: $subject,
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $project->getId() === 'console' ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
$recovery->setAttribute('secret', $secret);
@@ -4009,10 +4014,10 @@ Http::post('/v1/account/verifications/email')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
->inject('queueForMails')
->inject('publisherForMails')
->inject('proofForToken')
->inject('authorization')
->action(function (string $url, Request $request, Response $response, Document $project, array $platform, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsToken $proofForToken, Authorization $authorization) {
->action(function (string $url, Request $request, Response $response, Document $project, array $platform, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, ProofsToken $proofForToken, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
@@ -4099,6 +4104,7 @@ Http::post('/v1/account/verifications/email')
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
$smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -4116,13 +4122,6 @@ Http::post('/v1/account/verifications/email')
$replyToName = $smtp['replyToName'];
}
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '');
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -4143,11 +4142,17 @@ Http::post('/v1/account/verifications/email')
$subject = $customTemplate['subject'] ?? $subject;
}
$queueForMails
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $replyToEmail,
'replyToName' => $replyToName,
'senderEmail' => $senderEmail,
'senderName' => $senderName,
];
}
$emailVariables = [
@@ -4174,20 +4179,19 @@ Http::post('/v1/account/verifications/email')
]);
}
$queueForMails
->setSubject($subject)
->setPreview($preview)
->setBody($body)
->setBodyTemplate($bodyTemplate)
->appendVariables($emailVariables)
->setRecipient($user->getAttribute('email'))
->setName($user->getAttribute('name') ?? '');
if ($project->getId() === 'console') {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $user->getAttribute('email'),
name: $user->getAttribute('name') ?? '',
subject: $subject,
bodyTemplate: $bodyTemplate,
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $project->getId() === 'console' ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
$verification->setAttribute('secret', $verificationSecret);
@@ -4321,7 +4325,7 @@ Http::post('/v1/account/verifications/phone')
->inject('user')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('project')
->inject('locale')
->inject('timelimit')
@@ -4329,7 +4333,7 @@ Http::post('/v1/account/verifications/phone')
->inject('plan')
->inject('proofForCode')
->inject('authorization')
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, Context $usage, array $plan, ProofsCode $proofForCode, Authorization $authorization) {
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, MessagingPublisher $publisherForMessaging, Document $project, Locale $locale, callable $timelimit, Context $usage, array $plan, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -4398,11 +4402,13 @@ Http::post('/v1/account/verifications/phone')
],
]);
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_INTERNAL)
->setMessage($messageDoc)
->setRecipients([$user->getAttribute('phone')])
->setProviderType(MESSAGE_TYPE_SMS);
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_INTERNAL,
project: $project,
message: $messageDoc,
recipients: [$user->getAttribute('phone')],
providerType: MESSAGE_TYPE_SMS,
));
$helper = PhoneNumberUtil::getInstance();
try {
@@ -4674,13 +4680,13 @@ Http::delete('/v1/account/targets/:targetId/push')
))
->param('targetId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Target ID.', false, ['dbForProject'])
->inject('queueForEvents')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('user')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('authorization')
->action(function (string $targetId, Event $queueForEvents, Delete $queueForDeletes, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) {
->action(function (string $targetId, Event $queueForEvents, DeletePublisher $publisherForDeletes, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) {
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId));
if ($target->isEmpty()) {
@@ -4695,9 +4701,11 @@ Http::delete('/v1/account/targets/:targetId/push')
$dbForProject->purgeCachedDocument('users', $user->getId());
$queueForDeletes
->setType(DELETE_TYPE_TARGET)
->setDocument($target);
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_TARGET,
document: $target,
));
$queueForEvents
->setParam('userId', $user->getId())
+53 -37
View File
@@ -3,9 +3,11 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Messaging;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Message\Messaging as MessagingMessage;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Status as MessageStatus;
use Appwrite\Permission;
@@ -2727,9 +2729,9 @@ Http::delete('/v1/messaging/topics/:topicId')
->param('topicId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Topic ID.', false, ['dbForProject'])
->inject('queueForEvents')
->inject('dbForProject')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('response')
->action(function (string $topicId, Event $queueForEvents, Database $dbForProject, Delete $queueForDeletes, Response $response) {
->action(function (string $topicId, Event $queueForEvents, Database $dbForProject, DeletePublisher $publisherForDeletes, Response $response) {
$topic = $dbForProject->getDocument('topics', $topicId);
if ($topic->isEmpty()) {
@@ -2738,9 +2740,11 @@ Http::delete('/v1/messaging/topics/:topicId')
$dbForProject->deleteDocument('topics', $topicId);
$queueForDeletes
->setType(DELETE_TYPE_TOPIC)
->setDocument($topic);
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_TOPIC,
document: $topic,
));
$queueForEvents
->setParam('topicId', $topic->getId());
@@ -3187,9 +3191,9 @@ Http::post('/v1/messaging/messages/email')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('response')
->action(function (string $messageId, string $subject, string $content, ?array $topics, ?array $users, ?array $targets, ?array $cc, ?array $bcc, ?array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, string $subject, string $content, ?array $topics, ?array $users, ?array $targets, ?array $cc, ?array $bcc, ?array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3274,9 +3278,11 @@ Http::post('/v1/messaging/messages/email')
switch ($status) {
case MessageStatus::PROCESSING:
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForPlatform->createDocument('schedules', new Document([
@@ -3362,9 +3368,9 @@ Http::post('/v1/messaging/messages/sms')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('response')
->action(function (string $messageId, string $content, ?array $topics, ?array $users, ?array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, string $content, ?array $topics, ?array $users, ?array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3418,9 +3424,11 @@ Http::post('/v1/messaging/messages/sms')
switch ($status) {
case MessageStatus::PROCESSING:
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForPlatform->createDocument('schedules', new Document([
@@ -3498,10 +3506,10 @@ Http::post('/v1/messaging/messages/push')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('response')
->inject('platform')
->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response, array $platform) {
->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response, array $platform) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3638,9 +3646,11 @@ Http::post('/v1/messaging/messages/push')
switch ($status) {
case MessageStatus::PROCESSING:
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForPlatform->createDocument('schedules', new Document([
@@ -3983,9 +3993,9 @@ Http::patch('/v1/messaging/messages/email/:messageId')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $content, ?bool $draft, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, ?array $attachments, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $content, ?bool $draft, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, ?array $attachments, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -4141,9 +4151,11 @@ Http::patch('/v1/messaging/messages/email/:messageId')
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
if ($status === MessageStatus::PROCESSING) {
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
}
$queueForEvents
@@ -4205,9 +4217,9 @@ Http::patch('/v1/messaging/messages/sms/:messageId')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $content, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $content, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -4323,9 +4335,11 @@ Http::patch('/v1/messaging/messages/sms/:messageId')
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
if ($status === MessageStatus::PROCESSING) {
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
}
$queueForEvents
@@ -4379,10 +4393,10 @@ Http::patch('/v1/messaging/messages/push/:messageId')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('publisherForMessaging')
->inject('response')
->inject('platform')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response, array $platform) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response, array $platform) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -4584,9 +4598,11 @@ Http::patch('/v1/messaging/messages/push/:messageId')
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
if ($status === MessageStatus::PROCESSING) {
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_EXTERNAL)
->setMessageId($message->getId());
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_EXTERNAL,
project: $project,
messageId: $message->getId(),
));
}
$queueForEvents
-34
View File
@@ -2,9 +2,6 @@
use Appwrite\Auth\Validator\MockNumber;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
@@ -27,37 +24,6 @@ Http::init()
}
});
Http::get('/v1/projects/:projectId')
->desc('Get project')
->groups(['api', 'projects'])
->label('scope', 'projects.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
name: 'get',
description: '/docs/references/projects/get.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$response->dynamic($project, Response::MODEL_PROJECT);
});
// Backwards compatibility
Http::patch('/v1/projects/:projectId/oauth2')
->desc('Update project OAuth2')
+22 -17
View File
@@ -11,8 +11,9 @@ use Appwrite\Auth\Validator\Phone;
use Appwrite\Deletes\Identities as DeleteIdentities;
use Appwrite\Deletes\Targets as DeleteTargets;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\SDK\AuthType;
@@ -131,15 +132,15 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
} catch (\Throwable) {
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -1563,15 +1564,15 @@ Http::patch('/v1/users/:userId/email')
} catch (\Throwable) {
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -2592,8 +2593,8 @@ Http::delete('/v1/users/:userId')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForDeletes')
->action(function (string $userId, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes) {
->inject('publisherForDeletes')
->action(function (string $userId, Response $response, Database $dbForProject, Event $queueForEvents, DeletePublisher $publisherForDeletes) {
$user = $dbForProject->getDocument('users', $userId);
@@ -2608,9 +2609,11 @@ Http::delete('/v1/users/:userId')
DeleteIdentities::delete($dbForProject, Query::equal('userInternalId', [$user->getSequence()]));
DeleteTargets::delete($dbForProject, Query::equal('userInternalId', [$user->getSequence()]));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($clone);
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_DOCUMENT,
document: $clone,
));
$queueForEvents
->setParam('userId', $user->getId())
@@ -2643,10 +2646,10 @@ Http::delete('/v1/users/:userId/targets/:targetId')
->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject'])
->param('targetId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Target ID.', false, ['dbForProject'])
->inject('queueForEvents')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('response')
->inject('dbForProject')
->action(function (string $userId, string $targetId, Event $queueForEvents, Delete $queueForDeletes, Response $response, Database $dbForProject) {
->action(function (string $userId, string $targetId, Event $queueForEvents, DeletePublisher $publisherForDeletes, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
@@ -2666,9 +2669,11 @@ Http::delete('/v1/users/:userId/targets/:targetId')
$dbForProject->deleteDocument('targets', $target->getId());
$dbForProject->purgeCachedDocument('users', $user->getId());
$queueForDeletes
->setType(DELETE_TYPE_TARGET)
->setDocument($target);
$publisherForDeletes->enqueue(new DeleteMessage(
project: $queueForEvents->getProject(),
type: DELETE_TYPE_TARGET,
document: $target,
));
$queueForEvents
->setParam('userId', $user->getId())
+32 -23
View File
@@ -7,9 +7,10 @@ use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Cors;
use Appwrite\Platform\Appwrite;
@@ -29,6 +30,7 @@ use Appwrite\Utopia\Request\Filters\V22 as RequestV22;
use Appwrite\Utopia\Request\Filters\V23 as RequestV23;
use Appwrite\Utopia\Request\Filters\V24 as RequestV24;
use Appwrite\Utopia\Request\Filters\V25 as RequestV25;
use Appwrite\Utopia\Request\Filters\V26 as RequestV26;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
@@ -40,6 +42,7 @@ use Appwrite\Utopia\Response\Filters\V22 as ResponseV22;
use Appwrite\Utopia\Response\Filters\V23 as ResponseV23;
use Appwrite\Utopia\Response\Filters\V24 as ResponseV24;
use Appwrite\Utopia\Response\Filters\V25 as ResponseV25;
use Appwrite\Utopia\Response\Filters\V26 as ResponseV26;
use Appwrite\Utopia\View;
use Executor\Exception\Timeout as ExecutorTimeout;
use Executor\Executor;
@@ -72,7 +75,7 @@ use Utopia\Validator\Text;
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, Bus $bus, 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, DeletePublisher $publisherForDeletes, int $executionsRetentionCount)
{
$host = $request->getHostname();
if (!empty($previewHostname)) {
@@ -788,12 +791,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
? RESOURCE_TYPE_FUNCTIONS
: RESOURCE_TYPE_SITES;
$queueForDeletes
->setProject($project)
->setResourceType($resourceType)
->setResource($resource->getSequence())
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
->trigger();
$publisherForDeletes->enqueue(new DeleteMessage(
project: $project,
type: DELETE_TYPE_EXECUTIONS_LIMIT,
resource: (string) $resource->getSequence(),
resourceType: $resourceType,
));
}
return true;
@@ -854,9 +857,9 @@ Http::init()
->inject('apiKey')
->inject('cors')
->inject('authorization')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->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, Event $queueForEvents, Bus $bus, 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, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
@@ -864,7 +867,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, $bus, $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, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -914,6 +917,9 @@ Http::init()
if (version_compare($requestFormat, '1.9.4', '<')) {
$request->addFilter(new RequestV25());
}
if (version_compare($requestFormat, '1.9.5', '<')) {
$request->addFilter(new RequestV26());
}
}
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
@@ -938,6 +944,9 @@ Http::init()
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
if (version_compare($responseFormat, '1.9.5', '<')) {
$response->addFilter(new ResponseV26());
}
if (version_compare($responseFormat, '1.9.4', '<')) {
$response->addFilter(new ResponseV25());
}
@@ -1159,16 +1168,16 @@ Http::options()
->inject('apiKey')
->inject('cors')
->inject('authorization')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('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) {
->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, DeletePublisher $publisherForDeletes, 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, $bus, $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, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1274,7 +1283,7 @@ Http::error()
if (!$publish && $project->getId() !== 'console') {
$errorUser = new DBUser();
try {
$resolvedUser = $utopia->getResource('user');
$resolvedUser = $utopia->context()->get('user');
if ($resolvedUser instanceof DBUser) {
$errorUser = $resolvedUser;
}
@@ -1293,7 +1302,7 @@ Http::error()
if ($logger && $publish) {
try {
/** @var Utopia\Database\Document $user */
$user = $utopia->getResource('user');
$user = $utopia->context()->get('user');
} catch (\Throwable) {
// All good, user is optional information for logger
}
@@ -1494,7 +1503,7 @@ Http::error()
// the cors resource (which depends on rule -> DB) would cascade.
// Uses override:true to avoid duplicate headers if init() already set them.
try {
$cors = $utopia->getResource('cors');
$cors = $utopia->context()->get('cors');
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
$response
->removeHeader($name)
@@ -1561,15 +1570,15 @@ Http::get('/robots.txt')
->inject('previewHostname')
->inject('apiKey')
->inject('authorization')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('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) {
->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, DeletePublisher $publisherForDeletes, 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, $bus, $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, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1595,15 +1604,15 @@ Http::get('/humans.txt')
->inject('previewHostname')
->inject('apiKey')
->inject('authorization')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('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) {
->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, DeletePublisher $publisherForDeletes, 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, $bus, $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, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
+122 -114
View File
@@ -3,23 +3,22 @@
use Appwrite\Auth\Key;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Build;
use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Message\Audit as AuditMessage;
use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Messaging;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Storage\Config\CacheControl;
use Appwrite\Platform\Modules\Storage\Config\StorageCacheControl;
use Appwrite\SDK\Method;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
@@ -484,26 +483,100 @@ Http::init()
->inject('response')
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('auditContext')
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('queueForBuilds')
->inject('usage')
->inject('queueForFunctions')
->inject('queueForMails')
->inject('dbForProject')
->inject('timelimit')
->inject('devKey')
->inject('authorization')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, callable $timelimit, Document $devKey, Authorization $authorization) {
$response->setUser($user);
$request->setUser($user);
$roles = $authorization->getRoles();
$shouldCheckAbuse = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled'
&& ! $user->isApp($roles)
&& ! $user->isPrivileged($roles)
&& $devKey->isEmpty();
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
$abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
$closestLimit = null;
foreach ($abuseKeyLabel as $abuseKey) {
$isRateLimited = false;
try {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{userId}', $user->getId())
->setParam('{userAgent}', $request->getUserAgent(''))
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath())
->setParam('{method}', $request->getMethod())
->setParam('{chunkId}', (int) ($start / ($end + 1 - $start)));
foreach ($request->getParams() as $key => $value) {
if (! empty($value)) {
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
}
}
$abuse = new Abuse($timeLimit);
$remaining = $timeLimit->remaining();
$limit = $timeLimit->limit();
$time = $timeLimit->time() + $route->getLabel('abuse-time', 3600);
if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) {
$closestLimit = $remaining;
$response
->addHeader('X-RateLimit-Limit', $limit)
->addHeader('X-RateLimit-Remaining', $remaining)
->addHeader('X-RateLimit-Reset', $time);
}
if ($shouldCheckAbuse) {
$isRateLimited = $abuse->check();
}
} catch (\Throwable $th) {
\error_log((string) $th);
continue;
}
if ($isRateLimited) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
}
}
});
Http::init()
->groups(['api'])
->inject('utopia')
->inject('request')
->inject('response')
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('auditContext')
->inject('queueForDatabase')
->inject('usage')
->inject('publisherForFunctions')
->inject('dbForProject')
->inject('resourceToken')
->inject('mode')
->inject('apiKey')
->inject('plan')
->inject('devKey')
->inject('telemetry')
->inject('platform')
->inject('authorization')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
->inject('cacheControlForStorage')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, EventDatabase $queueForDatabase, Context $usage, FunctionPublisher $publisherForFunctions, Database $dbForProject, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) {
$response->setUser($user);
$request->setUser($user);
@@ -520,70 +593,6 @@ Http::init()
default => '',
};
/*
* Abuse Check
*/
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
$timeLimitArray = [];
$abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
foreach ($abuseKeyLabel as $abuseKey) {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{userId}', $user->getId())
->setParam('{userAgent}', $request->getUserAgent(''))
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath())
->setParam('{method}', $request->getMethod())
->setParam('{chunkId}', (int) ($start / ($end + 1 - $start)));
$timeLimitArray[] = $timeLimit;
}
$closestLimit = null;
$roles = $authorization->getRoles();
$isPrivilegedUser = $user->isPrivileged($roles);
$isAppUser = $user->isApp($roles);
foreach ($timeLimitArray as $timeLimit) {
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
if (! empty($value)) {
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
}
}
$abuse = new Abuse($timeLimit);
$remaining = $timeLimit->remaining();
$limit = $timeLimit->limit();
$time = $timeLimit->time() + $route->getLabel('abuse-time', 3600);
if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) {
$closestLimit = $remaining;
$response
->addHeader('X-RateLimit-Limit', $limit)
->addHeader('X-RateLimit-Remaining', $remaining)
->addHeader('X-RateLimit-Reset', $time);
}
$enabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
if (
$enabled // Abuse is enabled
&& ! $isAppUser // User is not API key
&& ! $isPrivilegedUser // User is not an admin
&& $devKey->isEmpty() // request doesn't not contain development key
&& $abuse->check() // Route is rate-limited
) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
}
}
/**
* TODO: (@loks0n)
* Avoid mutating the message across file boundaries - it's difficult to reason about at scale.
@@ -614,24 +623,16 @@ Http::init()
}
/* Auto-set projects */
$queueForDeletes->setProject($project);
$queueForDatabase->setProject($project);
$queueForMessaging->setProject($project);
$queueForFunctions->setProject($project);
$queueForBuilds->setProject($project);
$queueForMails->setProject($project);
/* Auto-set platforms */
$queueForFunctions->setPlatform($platform);
$queueForBuilds->setPlatform($platform);
$queueForMails->setPlatform($platform);
$useCache = $route->getLabel('cache', false);
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
if ($useCache) {
$route = $utopia->match($request);
$roles = $authorization->getRoles();
$isAppUser = $user->isApp($roles);
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($authorization->getRoles());
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($roles);
$key = $request->cacheIdentifier();
Span::add('storage.cache.key', $key);
@@ -643,6 +644,7 @@ Http::init()
$data = $cache->load($key, $timestamp);
if (! empty($data) && ! $cacheLog->isEmpty()) {
$cacheControl = \sprintf('private, max-age=%d', $timestamp);
$parts = explode('/', $cacheLog->getAttribute('resourceType', ''));
$type = $parts[0];
@@ -651,7 +653,7 @@ Http::init()
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isToken = ! $resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
$isPrivilegedUser = $user->isPrivileged($roles);
if ($bucket->isEmpty() || (! $bucket->getAttribute('enabled') && ! $isAppUser && ! $isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -695,6 +697,21 @@ Http::init()
])));
}
}
if ($isImageTransformation) {
$cacheControl = $cacheControlForStorage(new StorageCacheControl(
source: CacheControl::SOURCE_CACHE,
user: $user,
maxAge: $timestamp,
project: $project,
bucket: $bucket,
file: $file,
resourceToken: $resourceToken,
fileSecurity: $fileSecurity,
cacheLog: $cacheLog,
route: $route,
));
}
}
$accessedAt = $cacheLog->getAttribute('accessedAt', '');
@@ -707,7 +724,7 @@ Http::init()
}
$response
->addHeader('Cache-Control', sprintf('private, max-age=%d', $timestamp))
->addHeader('Cache-Control', $cacheControl)
->addHeader('X-Appwrite-Cache', 'hit')
->setContentType($cacheLog->getAttribute('mimeType'));
$storageCacheOperationsCounter->add(1, ['result' => 'hit']);
@@ -798,11 +815,8 @@ Http::shutdown()
->inject('publisherForAudits')
->inject('usage')
->inject('publisherForUsage')
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('queueForBuilds')
->inject('queueForMessaging')
->inject('queueForFunctions')
->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
@@ -812,7 +826,7 @@ Http::shutdown()
->inject('bus')
->inject('apiKey')
->inject('mode')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, EventDatabase $queueForDatabase, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -841,9 +855,15 @@ Http::shutdown()
if (! empty($functionsEvents)) {
foreach ($generatedEvents as $event) {
if (isset($functionsEvents[$event])) {
$queueForFunctions
->from($queueForEvents)
->trigger();
$publisherForFunctions->enqueue(FunctionMessage::fromEvent(
event: $queueForEvents->getEvent(),
params: $queueForEvents->getParams(),
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
userId: $queueForEvents->getUserId(),
payload: $queueForEvents->getPayload(),
platform: $queueForEvents->getPlatform(),
));
break;
}
}
@@ -953,22 +973,10 @@ Http::shutdown()
$publisherForAudits->enqueue(AuditMessage::fromContext($auditContext));
}
if (! empty($queueForDeletes->getType())) {
$queueForDeletes->trigger();
}
if (! empty($queueForDatabase->getType())) {
$queueForDatabase->trigger();
}
if (! empty($queueForBuilds->getType())) {
$queueForBuilds->trigger();
}
if (! empty($queueForMessaging->getType())) {
$queueForMessaging->trigger();
}
// Cache label
$useCache = $route->getLabel('cache', false);
if ($useCache) {
+31 -39
View File
@@ -3,7 +3,7 @@
require_once __DIR__ . '/init.php';
require_once __DIR__ . '/init/span.php';
$registerRequestResources = require __DIR__ . '/init/resources/request.php';
$setRequestContext = require __DIR__ . '/init/resources/request.php';
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
@@ -26,6 +26,7 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\DI\Container;
use Utopia\Http\Adapter\Swoole\Server;
use Utopia\Http\Files;
use Utopia\Http\Http;
@@ -57,7 +58,7 @@ $container->set('pools', function ($register) {
$payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing
$totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$swooleAdapter = new Server(
$swoole = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
settings: [
@@ -69,10 +70,10 @@ $swooleAdapter = new Server(
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
],
container: $container,
resources: $container,
);
$http = $swooleAdapter->getServer();
$http = $swoole->getServer();
/**
* Assigns HTTP requests to worker threads by analyzing its payload/content.
@@ -190,13 +191,11 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
Console::success('Reload completed...');
});
$container->set('bus', function ($register) use ($swooleAdapter) {
return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContainer()->get($name));
}, ['register']);
$container->set('bus', fn ($register) => $register->get('bus')->setResolver(fn (string $name) => $swoole->context()->get($name)), ['register']);
include __DIR__ . '/controllers/general.php';
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
function createDatabase(Container $resources, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
{
$max = 15;
$sleep = 2;
@@ -205,7 +204,7 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
while (true) {
try {
$attempts++;
$resource = $app->getResource($resourceKey);
$resource = $resources->get($resourceKey);
/* @var $database Database */
$database = is_callable($resource) ? $resource() : $resource;
break; // exit loop on success
@@ -288,23 +287,21 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
Span::current()?->finish();
}
$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) {
$app = new Http($swooleAdapter, 'UTC');
$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $container) {
/** @var \Utopia\Pools\Group $pools */
$pools = $app->getResource('pools');
$pools = $container->get('pools');
go(function () use ($app, $pools) {
go(function () use ($container, $pools) {
/** @var array $collections */
$collections = Config::getParam('collections', []);
// create logs database first, `getLogsDB` is a callable.
createDatabase($app, 'getLogsDB', 'logs', $collections['logs'], $pools);
createDatabase($container, 'getLogsDB', 'logs', $collections['logs'], $pools);
// create appwrite database, `dbForPlatform` is a direct access call.
createDatabase($app, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections, $app) {
$authorization = $app->getResource('authorization');
createDatabase($container, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections, $container) {
$authorization = $container->get('authorization');
if ($dbForPlatform->getCollection(AuditAdapterSQL::COLLECTION)->isEmpty()) {
$adapter = new AdapterDatabase($dbForPlatform);
@@ -416,7 +413,7 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
$documentsSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
$vectorSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
$cache = $app->getResource('cache');
$cache = $container->get('cache');
// All shared tables pools that need project metadata collections
$allSharedTables = \array_values(\array_unique(\array_filter([
@@ -502,7 +499,7 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
});
});
$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerRequestResources) {
$swoole->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swoole, $setRequestContext) {
Span::init('http.request');
$request = new Request($utopiaRequest->getSwooleRequest());
@@ -522,21 +519,18 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
return;
}
$requestContainer = $swooleAdapter->getContainer();
$requestContainer->set('container', fn () => $requestContainer);
$requestContainer->set('request', fn () => $request);
$requestContainer->set('response', fn () => $response);
$app = new Http($swoole, 'UTC');
$app->context()->set('request', fn () => $request);
$app->context()->set('response', fn () => $response);
$app->context()->set('utopia', fn () => $app);
$app = new Http($swooleAdapter, 'UTC');
$requestContainer->set('utopia', fn () => $app);
$registerRequestResources($requestContainer);
$setRequestContext($app->context());
$app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled');
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
try {
$authorization = $app->getResource('authorization');
$authorization = $app->context()->get('authorization');
$request->setAuthorization($authorization);
$response->setAuthorization($authorization);
@@ -552,18 +546,18 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$logger = $app->getResource("logger");
$logger = $app->context()->get("logger");
if ($logger) {
try {
/** @var Utopia\Database\Document $user */
$user = $app->getResource('user');
$user = $app->context()->get('user');
} catch (\Throwable $_th) {
// All good, user is optional information for logger
}
$route = $app->getRoute();
$log = $app->getResource("log");
$log = $app->context()->get("log");
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
@@ -642,18 +636,16 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
});
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
$http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
$http->on(Constant::EVENT_TASK, function () use ($container) {
$lastSyncUpdate = null;
$app = new Http($swooleAdapter, 'UTC');
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('dbForPlatform');
$dbForPlatform = $container->get('dbForPlatform');
/** @var \Swoole\Table $riskyDomains */
$riskyDomains = $app->getResource('riskyDomains');
$riskyDomains = $container->get('riskyDomains');
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $app) {
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $container) {
try {
$time = DateTime::now();
$limit = 1000;
@@ -670,7 +662,7 @@ $http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
}
$results = [];
try {
$authorization = $app->getResource('authorization');
$authorization = $container->get('authorization');
$results = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries));
} catch (Throwable $th) {
Console::error('rules ' . $th->getMessage());
@@ -720,4 +712,4 @@ $http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
});
});
$swooleAdapter->start();
$swoole->start();
+63 -3
View File
@@ -1,5 +1,11 @@
<?php
use Appwrite\Platform\Modules\Advisor\Enums\InsightCTAMethod;
use Appwrite\Platform\Modules\Advisor\Enums\InsightCTAService;
use Appwrite\Platform\Modules\Advisor\Enums\InsightSeverity;
use Appwrite\Platform\Modules\Advisor\Enums\InsightStatus;
use Appwrite\Platform\Modules\Advisor\Enums\InsightType;
use Appwrite\Platform\Modules\Advisor\Enums\ReportType;
use Appwrite\Platform\Modules\Compute\Specification;
use Utopia\System\System;
@@ -44,8 +50,8 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 4325;
const APP_VERSION_STABLE = '1.9.4';
const APP_CACHE_BUSTER = 4326;
const APP_VERSION_STABLE = '1.9.5';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
@@ -194,7 +200,7 @@ const BUILD_TYPE_RETRY = 'retry';
const DELETE_TYPE_DATABASES = 'databases';
const DELETE_TYPE_DOCUMENT = 'document';
const DELETE_TYPE_COLLECTIONS = 'collections';
const DELETE_TYPE_TRANSACTION = 'transaction';
const DELETE_TYPE_TRANSACTIONS = 'transactions';
const DELETE_TYPE_EXPIRED_TRANSACTIONS = 'expired_transactions';
const DELETE_TYPE_PROJECTS = 'projects';
const DELETE_TYPE_SITES = 'sites';
@@ -222,6 +228,7 @@ const DELETE_TYPE_EXPIRED_TARGETS = 'invalid_targets';
const DELETE_TYPE_SESSION_TARGETS = 'session_targets';
const DELETE_TYPE_CSV_EXPORTS = 'csv_exports';
const DELETE_TYPE_MAINTENANCE = 'maintenance';
const DELETE_TYPE_REPORT = 'report';
// Rule statuses
const RULE_STATUS_CREATED = 'created'; // This is also the status when domain DNS verification fails.
@@ -424,6 +431,55 @@ const RESOURCE_TYPE_MESSAGES = 'messages';
const RESOURCE_TYPE_EXECUTIONS = 'executions';
const RESOURCE_TYPE_VCS = 'vcs';
const RESOURCE_TYPE_EMBEDDINGS_TEXT = 'embeddingsText';
const RESOURCE_TYPE_INSIGHTS = 'insights';
const RESOURCE_TYPE_REPORTS = 'reports';
// Insight types — engine-specific so the CTA action can reference the right public API.
const ADVISOR_INSIGHT_TYPES = [
InsightType::DATABASE_INDEX->value, // legacy databases.createIndex
InsightType::TABLES_DB_INDEX->value, // tablesDB.createIndex
InsightType::DOCUMENTS_DB_INDEX->value, // documentsDB.createIndex
InsightType::VECTORS_DB_INDEX->value, // vectorsDB.createIndex
InsightType::DATABASE_PERFORMANCE->value,
InsightType::SITE_PERFORMANCE->value,
InsightType::SITE_ACCESSIBILITY->value,
InsightType::SITE_SEO->value,
InsightType::FUNCTION_PERFORMANCE->value,
];
// Public API services (SDK namespaces) that an insight CTA's `service` can reference.
// Analyzers must pick the one matching the engine the resource lives in.
const ADVISOR_CTA_SERVICES = [
InsightCTAService::DATABASES->value, // legacy
InsightCTAService::TABLES_DB->value,
InsightCTAService::DOCUMENTS_DB->value,
InsightCTAService::VECTORS_DB->value,
];
// Public API method names that an insight CTA's `method` can reference for index suggestions.
const ADVISOR_CTA_METHODS = [
InsightCTAMethod::CREATE_INDEX->value,
];
// Insight severities
const ADVISOR_SEVERITIES = [
InsightSeverity::INFO->value,
InsightSeverity::WARNING->value,
InsightSeverity::CRITICAL->value,
];
// Insight statuses
const ADVISOR_STATUSES = [
InsightStatus::ACTIVE->value,
InsightStatus::DISMISSED->value,
];
// Report types
const ADVISOR_REPORT_TYPES = [
ReportType::LIGHTHOUSE->value,
ReportType::AUDIT->value,
ReportType::DATABASE_ANALYZER->value,
];
// Resource types for Tokens
const TOKENS_RESOURCE_TYPE_FILES = 'files';
@@ -458,3 +514,7 @@ const CSV_ALLOWED_DATABASE_TYPES = [
DATABASE_TYPE_TABLESDB,
DATABASE_TYPE_VECTORSDB
];
const VCS_DEPLOYMENT_SKIP_PATTERNS = [
'[skip ci]',
];
+14
View File
@@ -475,3 +475,17 @@ Database::addFilter(
]));
}
);
Database::addFilter(
'subQueryReportInsights',
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database->find('insights', [
Query::equal('projectInternalId', [$document->getAttribute('projectInternalId')]),
Query::equal('reportInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
+14
View File
@@ -92,6 +92,8 @@ use Appwrite\Utopia\Response\Model\HealthTime;
use Appwrite\Utopia\Response\Model\HealthVersion;
use Appwrite\Utopia\Response\Model\Identity;
use Appwrite\Utopia\Response\Model\Index;
use Appwrite\Utopia\Response\Model\Insight;
use Appwrite\Utopia\Response\Model\InsightCTA;
use Appwrite\Utopia\Response\Model\Installation;
use Appwrite\Utopia\Response\Model\JWT;
use Appwrite\Utopia\Response\Model\Key;
@@ -173,12 +175,16 @@ use Appwrite\Utopia\Response\Model\PolicySessionLimit;
use Appwrite\Utopia\Response\Model\PolicyUserLimit;
use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\Project;
use Appwrite\Utopia\Response\Model\ProjectAuthMethod;
use Appwrite\Utopia\Response\Model\ProjectProtocol;
use Appwrite\Utopia\Response\Model\ProjectService;
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\ProviderRepository;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFrameworkList;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntimeList;
use Appwrite\Utopia\Response\Model\Report;
use Appwrite\Utopia\Response\Model\ResourceToken;
use Appwrite\Utopia\Response\Model\Row;
use Appwrite\Utopia\Response\Model\Rule;
@@ -288,6 +294,8 @@ Response::setModel(new BaseList('Specifications List', Response::MODEL_SPECIFICA
Response::setModel(new BaseList('VCS Content List', Response::MODEL_VCS_CONTENT_LIST, 'contents', Response::MODEL_VCS_CONTENT));
Response::setModel(new BaseList('VectorsDB Collections List', Response::MODEL_VECTORSDB_COLLECTION_LIST, 'collections', Response::MODEL_VECTORSDB_COLLECTION));
Response::setModel(new BaseList('Embedding list', Response::MODEL_EMBEDDING_LIST, 'embeddings', Response::MODEL_EMBEDDING));
Response::setModel(new BaseList('Insights List', Response::MODEL_INSIGHT_LIST, 'insights', Response::MODEL_INSIGHT));
Response::setModel(new BaseList('Reports List', Response::MODEL_REPORT_LIST, 'reports', Response::MODEL_REPORT));
// Entities
Response::setModel(new Database());
@@ -397,6 +405,9 @@ Response::setModel(new FrameworkAdapter());
Response::setModel(new Deployment());
Response::setModel(new Execution());
Response::setModel(new Project());
Response::setModel(new ProjectAuthMethod());
Response::setModel(new ProjectService());
Response::setModel(new ProjectProtocol());
Response::setModel(new Webhook());
Response::setModel(new Key());
Response::setModel(new EphemeralKey());
@@ -509,6 +520,9 @@ Response::setModel(new Target());
Response::setModel(new Migration());
Response::setModel(new MigrationReport());
Response::setModel(new MigrationFirebaseProject());
Response::setModel(new Insight());
Response::setModel(new InsightCTA());
Response::setModel(new Report());
// Tests (keep last)
Response::setModel(new Mock());
+30
View File
@@ -2,12 +2,18 @@
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Audit as AuditPublisher;
use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Event\Publisher\Screenshot as ScreenshotPublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Platform\Modules\Storage\Config\StorageCacheControl;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
@@ -104,6 +110,10 @@ $container->set('publisherForExecutions', fn (Publisher $publisher) => new Execu
$publisher,
new Queue(System::getEnv('_APP_EXECUTIONS_QUEUE_NAME', Event::EXECUTIONS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
$publisher,
new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
), ['publisher']);
$container->set('publisherForMigrations', fn (Publisher $publisher) => new MigrationPublisher(
$publisher,
new Queue(System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME))
@@ -112,6 +122,22 @@ $container->set('publisherForStatsResources', fn (Publisher $publisher) => new S
$publisher,
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForBuilds', fn (Publisher $publisher) => new BuildPublisher(
$publisher,
new Queue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForDeletes', fn (Publisher $publisher) => new DeletePublisher(
$publisher,
new Queue(System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForMails', fn (Publisher $publisher) => new MailPublisher(
$publisher,
new Queue(System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForMessaging', fn (Publisher $publisher) => new MessagingPublisher(
$publisher,
new Queue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME))
), ['publisher']);
/**
* Platform configuration
@@ -198,6 +224,10 @@ $container->set('cache', function (Group $pools, Telemetry $telemetry) {
return $cache;
}, ['pools', 'telemetry']);
$container->set('cacheControlForStorage', fn () => function (StorageCacheControl $config): string {
return \sprintf('private, max-age=%d', $config->maxAge);
});
$container->set('redis', function () {
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
$port = System::getEnv('_APP_REDIS_PORT', 6379);
+92 -132
View File
@@ -4,14 +4,11 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Build;
use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
@@ -51,6 +48,7 @@ use Utopia\Locale\Locale;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Storage\Device;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
@@ -62,26 +60,18 @@ use Utopia\Validator\WhiteList;
* These resources depend (directly or transitively) on request/response
* and must be fresh for each HTTP request.
*/
return function (Container $container): void {
$container->set('utopia:graphql', function ($utopia) {
return $utopia;
}, ['utopia']);
return function (Container $context): void {
$context->set('utopia:graphql', fn ($utopia) => $utopia, ['utopia']);
$container->set('log', fn () => new Log(), []);
$context->set('log', fn () => new Log(), []);
$container->set('logger', function ($register) {
return $register->get('logger');
}, ['register']);
$context->set('logger', fn ($register) => $register->get('logger'), ['register']);
$container->set('authorization', function () {
return new Authorization();
}, []);
$context->set('authorization', fn () => new Authorization(), []);
$container->set('store', function (): Store {
return new Store();
}, []);
$context->set('store', fn (): Store => new Store(), []);
$container->set('proofForPassword', function (): Password {
$context->set('proofForPassword', function (): Password {
$hash = new Argon2();
$hash
->setMemoryCost(7168)
@@ -95,21 +85,21 @@ return function (Container $container): void {
return $password;
});
$container->set('proofForToken', function (): Token {
$context->set('proofForToken', function (): Token {
$token = new Token();
$token->setHash(new Sha());
return $token;
});
$container->set('proofForCode', function (): Code {
$context->set('proofForCode', function (): Code {
$code = new Code();
$code->setHash(new Sha());
return $code;
});
$container->set('locale', function () {
$context->set('locale', function () {
$locale = new Locale(System::getEnv('_APP_LOCALE', 'en'));
$locale->setFallback(System::getEnv('_APP_LOCALE', 'en'));
@@ -117,41 +107,18 @@ return function (Container $container): void {
});
// Per-request queue resources (stateful, accumulate event data during request)
$container->set('queueForMessaging', function (Publisher $publisher) {
return new Messaging($publisher);
}, ['publisher']);
$container->set('queueForMails', function (Publisher $publisher) {
return new Mail($publisher);
}, ['publisher']);
$container->set('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
$container->set('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
$container->set('queueForWebhooks', function (Publisher $publisher) {
return new Webhook($publisher);
}, ['publisher']);
$container->set('queueForRealtime', function () {
return new Realtime();
}, []);
$container->set('usage', function () {
return new UsageContext();
}, []);
$container->set('auditContext', fn () => new AuditContext(), []);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('eventProcessor', function () {
return new EventProcessor();
}, []);
$container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
$context->set('queueForDatabase', fn (Publisher $publisher) => new EventDatabase($publisher), ['publisher']);
$context->set('queueForEvents', fn (Publisher $publisher) => new Event($publisher), ['publisher']);
$context->set('queueForWebhooks', fn (Publisher $publisher) => new Webhook($publisher), ['publisher']);
$context->set('queueForRealtime', fn () => new Realtime(), []);
$context->set('usage', fn () => new UsageContext(), []);
$context->set('auditContext', fn () => new AuditContext(), []);
$context->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
$publisher,
new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
), ['publisher']);
$context->set('eventProcessor', fn () => new EventProcessor(), []);
$context->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
$adapter = new DatabasePool($pools->get('console'));
$database = new Database($adapter, $cache);
@@ -169,7 +136,7 @@ return function (Container $container): void {
return $database;
}, ['pools', 'cache', 'authorization']);
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
$context->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
$adapters = [];
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$adapters) {
@@ -226,7 +193,7 @@ return function (Container $container): void {
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$context->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$adapter = null;
return function (?Document $project = null) use ($pools, $cache, $authorization, &$adapter) {
@@ -258,7 +225,7 @@ return function (Container $container): void {
/**
* List of allowed request hostnames for the request.
*/
$container->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) {
$context->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) {
$allowed = [...($platform['hostnames'] ?? [])];
/* Add platform configured hostnames */
@@ -302,7 +269,7 @@ return function (Container $container): void {
/**
* List of allowed request schemes for the request.
*/
$container->set('allowedSchemes', function (array $platform, Document $project) {
$context->set('allowedSchemes', function (array $platform, Document $project) {
$allowed = [...($platform['schemas'] ?? [])];
if (! $project->isEmpty() && $project->getId() !== 'console') {
@@ -322,7 +289,7 @@ return function (Container $container): void {
/**
* Whether the request origin is verified against the request hostname.
*/
$container->set('domainVerification', function (Request $request) {
$context->set('domainVerification', function (Request $request) {
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
$selfDomain = new Domain($request->getHostname());
$endDomain = new Domain((string) $origin);
@@ -334,7 +301,7 @@ return function (Container $container): void {
/**
* Cookie domain for the current request.
*/
$container->set('cookieDomain', function (Request $request, Document $project) {
$context->set('cookieDomain', function (Request $request, Document $project) {
$localHosts = ['localhost', 'localhost:' . $request->getPort()];
$migrationHost = System::getEnv('_APP_MIGRATION_HOST');
@@ -368,7 +335,7 @@ return function (Container $container): void {
/**
* Rule associated with a request origin.
*/
$container->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
$context->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
if (empty($domain)) {
@@ -418,7 +385,7 @@ return function (Container $container): void {
/**
* CORS service
*/
$container->set('cors', function (array $allowedHostnames) {
$context->set('cors', function (array $allowedHostnames) {
$corsConfig = Config::getParam('cors');
return new Cors(
@@ -430,23 +397,23 @@ return function (Container $container): void {
);
}, ['allowedHostnames']);
$container->set('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
if (! $devKey->isEmpty()) {
return new URL();
}
$context->set(
'originValidator',
fn (Document $devKey, array $allowedHostnames, array $allowedSchemes) => $devKey->isEmpty()
? new Origin($allowedHostnames, $allowedSchemes)
: new URL(),
['devKey', 'allowedHostnames', 'allowedSchemes']
);
return new Origin($allowedHostnames, $allowedSchemes);
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
$context->set(
'redirectValidator',
fn (Document $devKey, array $allowedHostnames, array $allowedSchemes) => $devKey->isEmpty()
? new Redirect($allowedHostnames, $allowedSchemes)
: new URL(),
['devKey', 'allowedHostnames', 'allowedSchemes']
);
$container->set('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
if (! $devKey->isEmpty()) {
return new URL();
}
return new Redirect($allowedHostnames, $allowedSchemes);
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
$container->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) {
$context->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) {
/**
* Handles user authentication and session validation.
*
@@ -617,7 +584,7 @@ return function (Container $container): void {
return $user;
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']);
$container->set('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) {
$context->set('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) {
/** @var Appwrite\Utopia\Request $request */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var Utopia\Database\Document $console */
@@ -650,7 +617,7 @@ return function (Container $container): void {
return $project;
}, ['dbForPlatform', 'request', 'console', 'authorization', 'utopia']);
$container->set('session', function (User $user, Store $store, Token $proofForToken) {
$context->set('session', function (User $user, Store $store, Token $proofForToken) {
if ($user->isEmpty()) {
return;
}
@@ -671,7 +638,7 @@ return function (Container $container): void {
return;
}, ['user', 'store', 'proofForToken']);
$container->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) {
$context->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, FunctionPublisher $publisherForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -727,7 +694,7 @@ return function (Container $container): void {
* Accounts can be created in many ways beyond `createAccount`
* (anonymous, OAuth, phone, etc.), and those flows are probably not covered in event tests; so we handle this here.
*/
$eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
$eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, FunctionPublisher $publisherForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
// Only trigger events for user creation with the database listener.
if ($document->getCollection() !== 'users') {
return;
@@ -739,9 +706,15 @@ return function (Container $container): void {
->setPayload($response->output($document, Response::MODEL_USER));
// Trigger functions, webhooks, and realtime events
$queueForFunctions
->from($queueForEvents)
->trigger();
$publisherForFunctions->enqueue(FunctionMessage::fromEvent(
event: $queueForEvents->getEvent(),
params: $queueForEvents->getParams(),
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
userId: $queueForEvents->getUserId(),
payload: $queueForEvents->getPayload(),
platform: $queueForEvents->getPlatform(),
));
/** Trigger webhooks events only if a project has them enabled */
if (! empty($project->getAttribute('webhooks'))) {
@@ -921,7 +894,6 @@ return function (Container $container): void {
// Clone the queues, to prevent events triggered by the database listener
// from overwriting the events that are supposed to be triggered in the shutdown hook.
$queueForEventsClone = new Event($publisher);
$queueForFunctions = new Func($publisherFunctions);
$queueForWebhooks = new Webhook($publisherWebhooks);
$queueForRealtime = new Realtime();
@@ -936,7 +908,7 @@ return function (Container $container): void {
$document,
$response,
$queueForEventsClone->from($queueForEvents),
$queueForFunctions->from($queueForEvents),
$publisherForFunctions,
$queueForWebhooks->from($queueForEvents),
$queueForRealtime->from($queueForEvents)
))
@@ -945,9 +917,9 @@ return function (Container $container): void {
->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database));
return $database;
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization', 'request']);
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'publisherForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization', 'request']);
$container->set('schema', function ($utopia, $dbForProject, $authorization) {
$context->set('schema', function ($utopia, $dbForProject, $authorization) {
$complexity = function (int $complexity, array $args) {
$queries = Query::parseQueries($args['queries'] ?? []);
@@ -1034,13 +1006,9 @@ return function (Container $container): void {
);
}, ['utopia', 'dbForProject', 'authorization']);
$container->set('audit', function ($dbForProject) {
$adapter = new AdapterDatabase($dbForProject);
$context->set('audit', fn ($dbForProject) => new Audit(new AdapterDatabase($dbForProject)), ['dbForProject']);
return new Audit($adapter);
}, ['dbForProject']);
$container->set('mode', function ($request, Document $project) {
$context->set('mode', function ($request, Document $project) {
/** @var Appwrite\Utopia\Request $request */
/**
@@ -1058,7 +1026,7 @@ return function (Container $container): void {
return $mode;
}, ['request', 'project']);
$container->set('requestTimestamp', function ($request) {
$context->set('requestTimestamp', function ($request) {
// TODO: Move this to the Request class itself
$timestampHeader = $request->getHeader('x-appwrite-timestamp');
$requestTimestamp = null;
@@ -1073,7 +1041,7 @@ return function (Container $container): void {
return $requestTimestamp;
}, ['request']);
$container->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) {
$context->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) {
$devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', ''));
// Check if given key match project's development keys
@@ -1122,7 +1090,7 @@ return function (Container $container): void {
return $key;
}, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']);
$container->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) {
$context->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) {
$teamInternalId = '';
if ($project->getId() !== 'console') {
$teamInternalId = $project->getAttribute('teamInternalId', '');
@@ -1165,7 +1133,7 @@ return function (Container $container): void {
return $team;
}, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']);
$container->set('previewHostname', function (Request $request, ?Key $apiKey) {
$context->set('previewHostname', function (Request $request, ?Key $apiKey) {
$allowed = false;
if (Http::isDevelopment()) {
@@ -1184,7 +1152,7 @@ return function (Container $container): void {
return '';
}, ['request', 'apiKey']);
$container->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key {
$context->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key {
$key = $request->getHeader('x-appwrite-key');
if (empty($key)) {
@@ -1218,7 +1186,7 @@ return function (Container $container): void {
return $key;
}, ['request', 'project', 'team', 'user']);
$container->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
$context->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
$tokenJWT = $request->getParam('token');
if (! empty($tokenJWT) && ! $project->isEmpty()) { // JWT authentication
@@ -1285,10 +1253,10 @@ return function (Container $container): void {
return new Document([]);
}, ['project', 'dbForProject', 'request', 'authorization']);
$container->set('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
$context->set('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database {
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
$databaseDSN = $database->getAttribute('database') ?: $project->getAttribute('database', '');
$databaseType = $database->getAttribute('type', '');
try {
@@ -1447,35 +1415,27 @@ return function (Container $container): void {
}, ['pools', 'cache', 'project', 'request', 'usage', 'authorization']);
$container->set('transactionState', function (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) {
return new TransactionState($dbForProject, $authorization, $getDatabasesDB);
}, ['dbForProject', 'authorization', 'getDatabasesDB']);
$context->set(
'transactionState',
fn (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) => new TransactionState($dbForProject, $authorization, $getDatabasesDB),
['dbForProject', 'authorization', 'getDatabasesDB']
);
$container->set('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
$context->set(
'executionsRetentionCount',
fn (Document $project, array $plan) => ($project->getId() === 'console' || empty($plan))
? 0
: (int) ($plan['executionsRetentionCount'] ?? 100),
['project', 'plan']
);
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
$context->set('deviceForFiles', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())), ['project', 'telemetry']);
$context->set('deviceForSites', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())), ['project', 'telemetry']);
$context->set('deviceForMigrations', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())), ['project', 'telemetry']);
$context->set('deviceForFunctions', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())), ['project', 'telemetry']);
$context->set('deviceForBuilds', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())), ['project', 'telemetry']);
$container->set('deviceForFiles', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForSites', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForMigrations', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForFunctions', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForBuilds', function ($project, Telemetry $telemetry) {
return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('embeddingAgent', function ($register) {
$context->set('embeddingAgent', function ($register) {
$adapter = new Ollama();
$adapter->setEndpoint(System::getEnv('_APP_EMBEDDING_ENDPOINT', 'http://ollama:11434/api/embed'));
$adapter->setTimeout((int) System::getEnv('_APP_EMBEDDING_TIMEOUT', '30000'));
+20 -1
View File
@@ -3,11 +3,30 @@
use Utopia\Span\Exporter;
use Utopia\Span\Span;
use Utopia\Span\Storage;
use Utopia\System\System;
Span::setStorage(new Storage\Coroutine());
Span::addExporter(new Exporter\Pretty(), function (Span $span): bool {
// Resolve trace filters once at boot to avoid repeated env lookups per span.
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
$traceEnabled = $traceProjectId !== '' || $traceFunctionId !== '';
Span::addExporter(new Exporter\Pretty(), function (Span $span) use ($traceEnabled, $traceProjectId, $traceFunctionId): bool {
if (\str_starts_with($span->getAction(), 'listener.')) {
return $span->getError() !== null;
}
// Selective tracing: when _APP_TRACE_PROJECT_ID / _APP_TRACE_FUNCTION_ID are set,
// only export spans tagged with matching project.id / function.id.
if ($traceEnabled) {
if ($traceProjectId !== '' && $span->get('project.id') !== $traceProjectId) {
return false;
}
if ($traceFunctionId !== '' && $span->get('function.id') !== $traceFunctionId) {
return false;
}
}
return true;
});
+6 -25
View File
@@ -1,12 +1,8 @@
<?php
use Appwrite\Event\Build;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Usage\Context;
@@ -26,6 +22,7 @@ use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Registry\Registry;
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
use Utopia\System\System;
@@ -334,22 +331,6 @@ return function (Container $container): void {
return new EventDatabase($publisher);
}, ['publisher']);
$container->set('queueForMessaging', function (Publisher $publisher) {
return new Messaging($publisher);
}, ['publisher']);
$container->set('queueForMails', function (Publisher $publisher) {
return new Mail($publisher);
}, ['publisher']);
$container->set('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
@@ -358,10 +339,10 @@ return function (Container $container): void {
return new Webhook($publisher);
}, ['publisher']);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
$publisher,
new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
), ['publisher']);
$container->set('queueForRealtime', function () {
return new Realtime();
}, []);
+25 -25
View File
@@ -728,8 +728,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$success = false;
Span::init('realtime.open');
Span::add('realtime.connectionId', $connection);
Span::add('realtime.inboundBytes', $rawSize);
Span::add('realtime.connection.id', $connection);
Span::add('realtime.inbound_bytes', $rawSize);
if (!empty($request->getOrigin())) {
Span::add('realtime.origin', $request->getOrigin());
}
@@ -936,16 +936,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
Span::error($th);
} finally {
Span::add('realtime.success', $success);
Span::add('realtime.responseCode', $responseCode);
Span::add('realtime.subscriptionMode', $subscriptionMode);
Span::add('realtime.channelCount', $channelCount);
Span::add('realtime.subscriptionCount', $subscriptionCount);
Span::add('realtime.outboundBytes', $outboundBytes);
Span::add('realtime.response_code', $responseCode);
Span::add('realtime.subscription_mode', $subscriptionMode);
Span::add('realtime.channel_count', $channelCount);
Span::add('realtime.subscription_count', $subscriptionCount);
Span::add('realtime.outbound_bytes', $outboundBytes);
if (!empty($project?->getId())) {
Span::add('realtime.projectId', $project->getId());
Span::add('project.id', $project->getId());
}
if (!empty($logUser?->getId())) {
Span::add('realtime.userId', $logUser->getId());
Span::add('user.id', $logUser->getId());
}
Span::current()?->finish();
}
@@ -965,9 +965,9 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$success = false;
Span::init('realtime.message');
Span::add('realtime.connectionId', $connection);
Span::add('realtime.inboundBytes', $rawSize);
Span::add('realtime.containerId', $containerId);
Span::add('realtime.connection.id', $connection);
Span::add('realtime.inbound_bytes', $rawSize);
Span::add('realtime.container.id', $containerId);
try {
$response = new Response(new SwooleResponse());
@@ -1352,15 +1352,15 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
Span::error($th);
} finally {
Span::add('realtime.success', $success);
Span::add('realtime.responseCode', $responseCode);
Span::add('realtime.subscriptionDelta', $subscriptionDelta);
Span::add('realtime.subscriptionsRequested', $subscriptionsRequested);
Span::add('realtime.subscriptionsRemoved', $subscriptionsRemoved);
Span::add('realtime.subscribe.subscriptionsCount', $subscriptionsRequested);
Span::add('realtime.outboundBytes', $outboundBytes);
Span::add('realtime.projectId', $project?->getId() ?? $projectId);
Span::add('realtime.userId', $realtime->connections[$connection]['userId'] ?? null);
Span::add('realtime.messageType', $messageType);
Span::add('realtime.response_code', $responseCode);
Span::add('realtime.subscription_delta', $subscriptionDelta);
Span::add('realtime.subscriptions_requested', $subscriptionsRequested);
Span::add('realtime.subscriptions_removed', $subscriptionsRemoved);
Span::add('realtime.subscribe.subscriptions_count', $subscriptionsRequested);
Span::add('realtime.outbound_bytes', $outboundBytes);
Span::add('project.id', $project?->getId() ?? $projectId);
Span::add('user.id', $realtime->connections[$connection]['userId'] ?? null);
Span::add('realtime.message_type', $messageType);
Span::current()?->finish();
}
});
@@ -1372,7 +1372,7 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) {
$success = false;
Span::init('realtime.close');
Span::add('realtime.connectionId', $connection);
Span::add('realtime.connection.id', $connection);
if (array_key_exists($connection, $realtime->connections)) {
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
@@ -1411,12 +1411,12 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) {
Span::add('realtime.success', $success);
if (!empty($projectId)) {
Span::add('realtime.projectId', $projectId);
Span::add('project.id', $projectId);
}
if (!empty($userId)) {
Span::add('realtime.userId', $userId);
Span::add('user.id', $userId);
}
Span::add('realtime.subscriptionsBeforeClose', $subscriptionsBeforeClose);
Span::add('realtime.subscriptions_before_close', $subscriptionsBeforeClose);
Span::current()?->finish();
}
+14
View File
@@ -881,6 +881,13 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_DOMAIN_FUNCTIONS
- _APP_DOMAIN_SITES
- _APP_MIGRATION_HOST
- _APP_CONSOLE_SCHEMA
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -909,6 +916,13 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_DOMAIN_FUNCTIONS
- _APP_DOMAIN_SITES
- _APP_MIGRATION_HOST
- _APP_CONSOLE_SCHEMA
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
+9 -1
View File
@@ -16,6 +16,7 @@ use Utopia\Pools\Group;
use Utopia\Queue\Adapter\Swoole;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Server;
use Utopia\Span\Span;
use Utopia\System\System;
Runtime::enableCoroutine();
@@ -91,8 +92,13 @@ $adapter = new Swoole(
$worker = new Server($adapter, $container);
try {
$worker->init()->action(function () use ($worker, $registerWorkerMessageResources) {
$worker->init()->action(function () use ($worker, $registerWorkerMessageResources, $queueName) {
$registerWorkerMessageResources($worker->getContainer());
Span::init("worker.{$queueName}");
});
$worker->shutdown()->action(function () {
Span::current()?->finish();
});
$container->set('bus', function ($register) use ($worker) {
@@ -120,6 +126,8 @@ $worker
->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project, Authorization $authorization) use ($queueName) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
Span::error($error);
if ($logger) {
$log->setNamespace('appwrite-worker');
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
+11 -11
View File
@@ -51,31 +51,31 @@
"ext-sockets": "*",
"appwrite/php-runtimes": "0.20.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "1.2.*",
"utopia-php/abuse": "1.3.*",
"utopia-php/agents": "1.2.*",
"utopia-php/analytics": "0.15.*",
"utopia-php/audit": "2.2.*",
"utopia-php/audit": "2.3.*",
"utopia-php/auth": "0.5.*",
"utopia-php/cache": "1.0.*",
"utopia-php/cache": "^2.1",
"utopia-php/cli": "0.23.*",
"utopia-php/compression": "0.1.*",
"utopia-php/config": "1.*",
"utopia-php/console": "0.1.*",
"utopia-php/database": "5.*",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "1.*",
"utopia-php/emails": "0.6.*",
"utopia-php/dns": "1.6.*",
"utopia-php/domains": "2.*",
"utopia-php/emails": "0.7.*",
"utopia-php/dns": "1.7.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/http": "0.34.*",
"utopia-php/fetch": "0.5.*",
"utopia-php/http": "^2.0@RC",
"utopia-php/fetch": "^1.1",
"utopia-php/validators": "0.2.*",
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/logger": "0.8.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/migration": "1.*",
"utopia-php/platform": "0.13.*",
"utopia-php/platform": "^1.0@RC",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
"utopia-php/preloader": "0.2.*",
@@ -85,7 +85,7 @@
"utopia-php/storage": "2.*",
"utopia-php/system": "0.10.*",
"utopia-php/telemetry": "0.2.*",
"utopia-php/vcs": "3.*",
"utopia-php/vcs": "4.*",
"utopia-php/websocket": "1.0.*",
"matomo/device-detector": "6.4.*",
"dragonmantank/cron-expression": "3.4.*",
Generated
+302 -178
View File
File diff suppressed because it is too large Load Diff
+16 -2
View File
@@ -255,7 +255,7 @@ services:
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: appwrite/console:7.8.45
image: appwrite/console:8
restart: unless-stopped
networks:
- appwrite
@@ -1114,6 +1114,13 @@ services:
- _APP_WORKER_PER_CORE
- _APP_POOL_ADAPTER
- _APP_OPENSSL_KEY_V1
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_DOMAIN_FUNCTIONS
- _APP_DOMAIN_SITES
- _APP_MIGRATION_HOST
- _APP_CONSOLE_SCHEMA
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -1145,6 +1152,13 @@ services:
- _APP_WORKER_PER_CORE
- _APP_POOL_ADAPTER
- _APP_OPENSSL_KEY_V1
- _APP_OPTIONS_FORCE_HTTPS
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_DOMAIN_FUNCTIONS
- _APP_DOMAIN_SITES
- _APP_MIGRATION_HOST
- _APP_CONSOLE_SCHEMA
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -1478,4 +1492,4 @@ volumes:
appwrite-sites:
appwrite-builds:
appwrite-config:
appwrite-models:
appwrite-models:
+1
View File
@@ -0,0 +1 @@
Delete an analyzer report by its unique ID. Nested insights and CTA metadata are removed asynchronously by the deletes worker.
+1
View File
@@ -0,0 +1 @@
Get an insight by its unique ID, scoped to its parent report.
+1
View File
@@ -0,0 +1 @@
Get an analyzer report by its unique ID. The response includes the report's metadata and the nested insights it produced.
+1
View File
@@ -0,0 +1 @@
List the insights produced under a single analyzer report. You can use the query params to filter your results further.
+1
View File
@@ -0,0 +1 @@
Get a list of all the project's analyzer reports. You can use the query params to filter your results.
@@ -1 +1 @@
Get a list of all branches from a GitHub repository in your installation. This endpoint returns the names of all branches in the repository and their total count. The GitHub installation must be properly configured and have access to the requested repository for this endpoint to work.
Get a list of branches from a GitHub repository in your installation. This endpoint supports filtering by a search term and pagination using query strings such as `Query.limit()`, `Query.offset()`, `Query.cursorAfter()`, and `Query.cursorBefore()`. It returns branch names along with the total number of matches. The GitHub installation must be properly configured and have access to the requested repository for this endpoint to work.
+3
View File
@@ -0,0 +1,3 @@
The Advisor service provides read access to analyzer reports and their nested insights for a project.
Use the reports endpoints to list and fetch analyzer runs, then use the insights endpoints to inspect individual findings attached to a report.
+1
View File
@@ -38,6 +38,7 @@
<directory>./tests/e2e/Services/Messaging</directory>
<directory>./tests/e2e/Services/Migrations</directory>
<directory>./tests/e2e/Services/Project</directory>
<directory>./tests/e2e/Services/Advisor</directory>
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>
+83
View File
@@ -0,0 +1,83 @@
<?php
namespace Appwrite\Advisor\Validator;
use Utopia\Validator;
class CTAs extends Validator
{
public const MAX_COUNT_DEFAULT = 16;
protected string $message = 'Value must be an array of CTA descriptors. Each entry must define `label`, `service`, `method`, and an optional `params` object.';
protected array $allowedServices;
protected array $allowedMethods;
public function __construct(
protected int $maxCount = self::MAX_COUNT_DEFAULT,
?array $allowedServices = null,
?array $allowedMethods = null,
) {
$this->allowedServices = $allowedServices ?? ADVISOR_CTA_SERVICES;
$this->allowedMethods = $allowedMethods ?? ADVISOR_CTA_METHODS;
}
public function getDescription(): string
{
return $this->message;
}
public function isArray(): bool
{
return true;
}
public function getType(): string
{
return self::TYPE_ARRAY;
}
public function isValid($value): bool
{
if (!\is_array($value)) {
return false;
}
if (\count($value) > $this->maxCount) {
$this->message = "A maximum of {$this->maxCount} CTAs are allowed per insight.";
return false;
}
foreach ($value as $entry) {
if (!\is_array($entry)) {
return false;
}
$maxLengths = ['label' => 256, 'service' => 64, 'method' => 64];
foreach ($maxLengths as $required => $maxLength) {
if (!isset($entry[$required]) || !\is_string($entry[$required]) || $entry[$required] === '') {
return false;
}
if (\strlen($entry[$required]) > $maxLength) {
$this->message = "CTA `{$required}` must not exceed {$maxLength} characters.";
return false;
}
}
if (!empty($this->allowedServices) && !\in_array($entry['service'], $this->allowedServices, true)) {
$this->message = "CTA `service` must be one of: " . \implode(', ', $this->allowedServices) . '.';
return false;
}
if (!empty($this->allowedMethods) && !\in_array($entry['method'], $this->allowedMethods, true)) {
$this->message = "CTA `method` must be one of: " . \implode(', ', $this->allowedMethods) . '.';
return false;
}
if (isset($entry['params']) && !\is_array($entry['params']) && !\is_object($entry['params'])) {
return false;
}
}
return true;
}
}
+53 -3
View File
@@ -55,7 +55,7 @@ class Google extends OAuth2
'state' => \json_encode($this->state),
'response_type' => 'code',
'access_type' => 'offline',
'prompt' => 'consent'
'prompt' => $this->getPrompt()
]);
}
@@ -72,7 +72,7 @@ class Google extends OAuth2
'https://oauth2.googleapis.com/token?' . \http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'client_secret' => $this->getClientSecret(),
'redirect_uri' => $this->callback,
'scope' => null,
'grant_type' => 'authorization_code'
@@ -95,7 +95,7 @@ class Google extends OAuth2
'https://oauth2.googleapis.com/token?' . \http_build_query([
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'client_secret' => $this->getClientSecret(),
'grant_type' => 'refresh_token'
])
), true);
@@ -177,4 +177,54 @@ class Google extends OAuth2
return $this->user;
}
/**
* Extracts the Client Secret from the JSON stored in appSecret
*
* @return string
*/
protected function getClientSecret(): string
{
$secret = $this->getAppSecret();
return $secret['clientSecret'] ?? $this->appSecret;
}
/**
* Extracts the prompt values from the JSON stored in appSecret
*
* @return string
*/
protected function getPrompt(): string
{
$secret = $this->getAppSecret();
$prompt = $secret['prompt'] ?? [];
if (empty($prompt)) {
$prompt = ['consent'];
}
return \implode(' ', $prompt);
}
/**
* Decode the JSON stored in appSecret.
* Falls back to treating the raw string as the client secret for backwards compatibility.
*
* @return array
*/
protected function getAppSecret(): array
{
try {
$secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR);
} catch (\Throwable $th) {
return ['clientSecret' => $this->appSecret];
}
if (!\is_array($secret)) {
return ['clientSecret' => $this->appSecret];
}
return $secret;
}
}
+6 -14
View File
@@ -8,7 +8,6 @@ use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Utopia\Bus\Listener;
use Utopia\Database\Document;
use Utopia\Span\Span;
use Utopia\System\System;
class Log extends Listener
{
@@ -34,20 +33,13 @@ class Log extends Listener
{
$project = new Document($event->project);
$execution = new Document($event->execution);
if ($execution->getAttribute('resourceType', '') === 'functions') {
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
$resourceId = $execution->getAttribute('resourceId', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
Span::init('execution.trace.v1_executions_enqueue');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $resourceId);
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('status', $execution->getAttribute('status', ''));
Span::current()?->finish();
}
Span::add('project.id', $project->getId());
Span::add('function.id', $execution->getAttribute('resourceId', ''));
Span::add('execution.id', $execution->getId());
Span::add('deployment.id', $execution->getAttribute('deploymentId', ''));
Span::add('execution.status', $execution->getAttribute('status', ''));
}
$publisherForExecutions->enqueue(new ExecutionMessage(
+28 -30
View File
@@ -4,14 +4,14 @@ namespace Appwrite\Bus\Listeners;
use Appwrite\Auth\MFA\Type;
use Appwrite\Bus\Events\SessionCreated;
use Appwrite\Event\Mail;
use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Template\Template;
use Utopia\Bus\Listener;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Locale\Locale;
use Utopia\Queue\Publisher;
use Utopia\Storage\Validator\FileName;
use Utopia\System\System;
@@ -31,14 +31,14 @@ class Mails extends Listener
{
$this
->desc('Sends session alert emails')
->inject('publisher')
->inject('publisherForMails')
->inject('locale')
->inject('platform')
->inject('dbForProject')
->callback($this->handle(...));
}
public function handle(SessionCreated $event, Publisher $publisher, Locale $locale, array $platform, Database $dbForProject): void
public function handle(SessionCreated $event, MailPublisher $publisherForMails, Locale $locale, array $platform, Database $dbForProject): void
{
$project = new Document($event->project);
@@ -124,34 +124,32 @@ class Mails extends Listener
];
}
$queueForMails = new Mail($publisher);
$smtpConfig = [];
if ($smtp['enabled'] ?? false) {
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '')
->setSmtpReplyToEmail($customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '') // Includes backwards compatibility
->setSmtpReplyToName($customTemplate['replyToName'] ?? $smtp['replyToName'] ?? '')
->setSmtpSenderEmail($customTemplate['senderEmail'] ?? $smtp['senderEmail'] ?? System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM))
->setSmtpSenderName($customTemplate['senderName'] ?? $smtp['senderName'] ?? System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '', // Includes backwards compatibility
'replyToName' => $customTemplate['replyToName'] ?? $smtp['replyToName'] ?? '',
'senderEmail' => $customTemplate['senderEmail'] ?? $smtp['senderEmail'] ?? System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM),
'senderName' => $customTemplate['senderName'] ?? $smtp['senderName'] ?? System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'),
];
}
$queueForMails
->setProject($project)
->setSubject($subject)
->setPreview($preview)
->setBody($body)
->setBodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/' . $smtpBaseTemplate . '.tpl')
->appendVariables($emailVariables)
->setRecipient($event->user['email']);
if ($isBranded) {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $event->user['email'],
subject: $subject,
bodyTemplate: __DIR__ . '/../../../../app/config/locale/templates/' . $smtpBaseTemplate . '.tpl',
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $isBranded ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
}
}
-146
View File
@@ -1,146 +0,0 @@
<?php
namespace Appwrite\Event;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
use Utopia\System\System;
class Build extends Event
{
protected string $type = '';
protected ?Document $resource = null;
protected ?Document $deployment = null;
protected ?Document $template = null;
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME))
->setClass(System::getEnv('_APP_BUILDS_CLASS_NAME', Event::BUILDS_CLASS_NAME));
}
/**
* Sets template for the build event.
*
* @param Document $template
* @return self
*/
public function setTemplate(Document $template): self
{
$this->template = $template;
return $this;
}
/**
* Sets resource document for the build event.
*
* @param Document $resource
* @return self
*/
public function setResource(Document $resource): self
{
$this->resource = $resource;
return $this;
}
/**
* Returns set resource document for the build event.
*
* @return null|Document
*/
public function getResource(): ?Document
{
return $this->resource;
}
/**
* Sets deployment for the build event.
*
* @param Document $deployment
* @return self
*/
public function setDeployment(Document $deployment): self
{
$this->deployment = $deployment;
return $this;
}
/**
* Returns set deployment for the build event.
*
* @return null|Document
*/
public function getDeployment(): ?Document
{
return $this->deployment;
}
/**
* Sets type for the build event.
*
* @param string $type Can be `BUILD_TYPE_DEPLOYMENT` or `BUILD_TYPE_RETRY`.
* @return self
*/
public function setType(string $type): self
{
$this->type = $type;
return $this;
}
/**
* Returns set type for the function event.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Prepare payload for queue.
*
* @return array
*/
protected function preparePayload(): array
{
$platform = $this->platform;
if (empty($platform)) {
$platform = Config::getParam('platform', []);
}
return [
'project' => $this->project,
'resource' => $this->resource,
'deployment' => $this->deployment,
'type' => $this->type,
'template' => $this->template,
'platform' => $platform,
];
}
/**
* Resets event.
*
* @return self
*/
public function reset(): self
{
$this->type = '';
$this->resource = null;
$this->deployment = null;
$this->template = null;
$this->platform = [];
parent::reset();
return $this;
}
}
-576
View File
@@ -1,576 +0,0 @@
<?php
namespace Appwrite\Event;
use Utopia\Config\Config;
use Utopia\Queue\Publisher;
use Utopia\System\System;
class Mail extends Event
{
protected string $recipient = '';
protected string $name = '';
protected string $subject = '';
protected string $body = '';
protected string $preview = '';
protected array $smtp = [];
protected array $variables = [];
protected string $bodyTemplate = '';
protected array $attachment = [];
protected array $customMailOptions = [];
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME))
->setClass(System::getEnv('_APP_MAILS_CLASS_NAME', Event::MAILS_CLASS_NAME));
}
/**
* Sets subject for the mail event.
*
* @param string $subject
* @return self
*/
public function setSubject(string $subject): self
{
$this->subject = $subject;
return $this;
}
/**
* Returns subject for the mail event.
*
* @return string
*/
public function getSubject(): string
{
return $this->subject;
}
/**
* Sets recipient for the mail event.
*
* @param string $recipient
* @return self
*/
public function setRecipient(string $recipient): self
{
$this->recipient = $recipient;
return $this;
}
/**
* Returns set recipient for mail event.
*
* @return string
*/
public function getRecipient(): string
{
return $this->recipient;
}
/**
* Sets body for the mail event.
*
* @param string $body
* @return self
*/
public function setBody(string $body): self
{
$this->body = $body;
return $this;
}
/**
* Returns body for the mail event.
*
* @return string
*/
public function getBody(): string
{
return $this->body;
}
/**
* Sets preview for the mail event.
*
* @return self
*/
public function setPreview(string $preview): self
{
$this->preview = $preview;
return $this;
}
/**
* Returns preview for the mail event.
*
* @return string
*/
public function getPreview(): string
{
return $this->preview;
}
/**
* Sets name for the mail event.
*
* @param string $name
* @return self
*/
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
/**
* Returns set name for the mail event.
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Sets bodyTemplate for the mail event.
*
* @param string $bodyTemplate
* @return self
*/
public function setBodyTemplate(string $bodyTemplate): self
{
$this->bodyTemplate = $bodyTemplate;
return $this;
}
/**
* Returns subject for the mail event.
*
* @return string
*/
public function getBodyTemplate(): string
{
return $this->bodyTemplate;
}
/**
* Set SMTP Host
*
* @param string $host
* @return self
*/
public function setSmtpHost(string $host): self
{
$this->smtp['host'] = $host;
return $this;
}
/**
* Set SMTP port
*
* @param int $port
* @return self
*/
public function setSmtpPort(int $port): self
{
$this->smtp['port'] = $port;
return $this;
}
/**
* Set SMTP username
*
* @param string $username
* @return self
*/
public function setSmtpUsername(string $username): self
{
$this->smtp['username'] = $username;
return $this;
}
/**
* Set SMTP password
*
* @param string $password
* @return self
*/
public function setSmtpPassword(string $password): self
{
$this->smtp['password'] = $password;
return $this;
}
/**
* Set SMTP secure
*
* @param string $secure
* @return self
*/
public function setSmtpSecure(string $secure): self
{
$this->smtp['secure'] = $secure;
return $this;
}
/**
* Set SMTP sender email
*
* @param string $senderEmail
* @return self
*/
public function setSmtpSenderEmail(string $senderEmail): self
{
$this->smtp['senderEmail'] = $senderEmail;
return $this;
}
/**
* Set SMTP sender name
*
* @param string $senderName
* @return self
*/
public function setSmtpSenderName(string $senderName): self
{
$this->smtp['senderName'] = $senderName;
return $this;
}
/**
* Set SMTP reply-to email
*
* @param string $email
* @return self
*/
public function setSmtpReplyToEmail(string $email): self
{
$this->smtp['replyToEmail'] = $email;
return $this;
}
/**
* Set SMTP reply-to name
*
* @param string $name
* @return self
*/
public function setSmtpReplyToName(string $name): self
{
$this->smtp['replyToName'] = $name;
return $this;
}
/**
* Get SMTP
*
* @return string
*/
public function getSmtpHost(): string
{
return $this->smtp['host'] ?? '';
}
/**
* Get SMTP port
*
* @return integer
*/
public function getSmtpPort(): int
{
return $this->smtp['port'] ?? 0;
}
/**
* Get SMTP username
*
* @return string
*/
public function getSmtpUsername(): string
{
return $this->smtp['username'] ?? '';
}
/**
* Get SMTP password
*
* @return string
*/
public function getSmtpPassword(): string
{
return $this->smtp['password'] ?? '';
}
/**
* Get SMTP secure
*
* @return string
*/
public function getSmtpSecure(): string
{
return $this->smtp['secure'] ?? '';
}
/**
* Get SMTP sender email
*
* @return string
*/
public function getSmtpSenderEmail(): string
{
return $this->smtp['senderEmail'] ?? '';
}
/**
* Get SMTP sender name
*
* @return string
*/
public function getSmtpSenderName(): string
{
return $this->smtp['senderName'] ?? '';
}
/**
* Get SMTP reply-to email
*
* @return string
*/
public function getSmtpReplyToEmail(): string
{
return $this->smtp['replyToEmail'] ?? '';
}
/**
* Get SMTP reply-to name
*
* @return string
*/
public function getSmtpReplyToName(): string
{
return $this->smtp['replyToName'] ?? '';
}
/**
* Get Email Variables
*
* @return array
*/
public function getVariables(): array
{
return $this->variables;
}
/**
* Set Email Variables
*
* @param array $variables
* @return self
*/
public function setVariables(array $variables): self
{
$this->variables = $variables;
return $this;
}
/**
* Append variables to the email event.
*
* @param array $variables
* @return self
*/
public function appendVariables(array $variables): self
{
$this->variables = \array_merge($this->variables, $variables);
return $this;
}
/**
* Set attachment
* @param string $content
* @param string $filename
* @param string $encoding
* @param string $type
* @return self
*/
public function setAttachment(string $content, string $filename, string $encoding = 'base64', string $type = 'plain/text')
{
$this->attachment = [
'content' => base64_encode($content),
'filename' => $filename,
'encoding' => $encoding,
'type' => $type,
];
return $this;
}
/**
* Get attachment
*
* @return array
*/
public function getAttachment(): array
{
return $this->attachment;
}
/**
* Reset attachment
*
* @return self
*/
public function resetAttachment(): self
{
$this->attachment = [];
return $this;
}
/**
* Set sender email
*
* @param string $email
* @return self
*/
public function setSenderEmail(string $email): self
{
$this->customMailOptions['senderEmail'] = $email;
return $this;
}
/**
* Get sender email
*
* @return string
*/
public function getSenderEmail(): string
{
return $this->customMailOptions['senderEmail'] ?? '';
}
/**
* Set sender name
*
* @param string $name
* @return self
*/
public function setSenderName(string $name): self
{
$this->customMailOptions['senderName'] = $name;
return $this;
}
/**
* Get sender name
*
* @return string
*/
public function getSenderName(): string
{
return $this->customMailOptions['senderName'] ?? '';
}
/**
* Set reply-to email
*
* @param string $email
* @return self
*/
public function setReplyToEmail(string $email): self
{
$this->customMailOptions['replyToEmail'] = $email;
return $this;
}
/**
* Get reply-to email
*
* @return string
*/
public function getReplyToEmail(): string
{
return $this->customMailOptions['replyToEmail'] ?? '';
}
/**
* Set reply-to name
*
* @param string $name
* @return self
*/
public function setReplyToName(string $name): self
{
$this->customMailOptions['replyToName'] = $name;
return $this;
}
/**
* Get reply-to name
*
* @return string
*/
public function getReplyToName(): string
{
return $this->customMailOptions['replyToName'] ?? '';
}
/**
* Reset
*
* @return self
*/
public function reset(): self
{
$this->project = null;
$this->recipient = '';
$this->name = '';
$this->subject = '';
$this->body = '';
$this->variables = [];
$this->bodyTemplate = '';
$this->attachment = [];
$this->customMailOptions = [];
return $this;
}
/**
* Prepare the payload for the event
*
* @return array
*/
protected function preparePayload(): array
{
$platform = $this->platform;
if (empty($platform)) {
$platform = Config::getParam('platform', []);
}
return [
'project' => $this->project,
'recipient' => $this->recipient,
'name' => $this->name,
'subject' => $this->subject,
'bodyTemplate' => $this->bodyTemplate,
'body' => $this->body,
'preview' => $this->preview,
'smtp' => $this->smtp,
'variables' => $this->variables,
'attachment' => $this->attachment,
'customMailOptions' => $this->customMailOptions,
'events' => Event::generateEvents($this->getEvent(), $this->getParams()),
'platform' => $platform,
];
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Config\Config;
use Utopia\Database\Document;
final class Build extends Base
{
public function __construct(
public readonly Document $project,
public readonly Document $resource,
public readonly Document $deployment,
public readonly string $type,
public readonly ?Document $template = null,
public readonly array $platform = [],
) {
}
public function toArray(): array
{
$platform = !empty($this->platform) ? $this->platform : Config::getParam('platform', []);
return [
'project' => $this->project->getArrayCopy(),
'resource' => $this->resource->getArrayCopy(),
'deployment' => $this->deployment->getArrayCopy(),
'type' => $this->type,
'template' => $this->template?->getArrayCopy(),
'platform' => $platform,
];
}
public static function fromArray(array $data): static
{
return new self(
project: new Document($data['project'] ?? []),
resource: new Document($data['resource'] ?? []),
deployment: new Document($data['deployment'] ?? []),
type: $data['type'] ?? '',
template: !empty($data['template']) ? new Document($data['template']) : null,
platform: $data['platform'] ?? [],
);
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Delete extends Base
{
public function __construct(
public readonly ?Document $project = null,
public readonly string $type = '',
public readonly ?Document $document = null,
public readonly ?string $resource = null,
public readonly ?string $resourceType = null,
public readonly ?string $datetime = null,
public readonly ?string $hourlyUsageRetentionDatetime = null,
) {
}
public function toArray(): array
{
return [
'project' => $this->project?->getArrayCopy(),
'type' => $this->type,
'document' => $this->document?->getArrayCopy(),
'resource' => $this->resource,
'resourceType' => $this->resourceType,
'datetime' => $this->datetime,
'hourlyUsageRetentionDatetime' => $this->hourlyUsageRetentionDatetime,
];
}
public static function fromArray(array $data): static
{
return new self(
project: !empty($data['project']) ? new Document($data['project']) : null,
type: $data['type'] ?? '',
document: !empty($data['document']) ? new Document($data['document']) : null,
resource: $data['resource'] ?? null,
resourceType: $data['resourceType'] ?? null,
datetime: $data['datetime'] ?? null,
hourlyUsageRetentionDatetime: $data['hourlyUsageRetentionDatetime'] ?? null,
);
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace Appwrite\Event\Message;
use Appwrite\Event\Event;
use Utopia\Config\Config;
use Utopia\Database\Document;
final class Func extends Base
{
public function __construct(
public readonly ?Document $project = null,
public readonly ?Document $user = null,
public readonly ?string $userId = null,
public readonly ?Document $function = null,
public readonly ?string $functionId = null,
public readonly ?Document $execution = null,
public readonly string $type = '',
public readonly string $jwt = '',
public readonly array $payload = [],
public readonly array $events = [],
public readonly string $body = '',
public readonly string $path = '',
public readonly array $headers = [],
public readonly string $method = '',
public readonly array $platform = [],
) {
}
public static function fromEvent(
string $event,
array $params,
?Document $project = null,
?Document $user = null,
?string $userId = null,
array $payload = [],
array $platform = [],
): static {
return new self(
project: $project,
user: $user,
userId: $userId,
payload: $payload,
events: $event !== '' ? Event::generateEvents($event, $params) : [],
platform: $platform,
);
}
public function toArray(): array
{
$platform = !empty($this->platform) ? $this->platform : Config::getParam('platform', []);
return [
'project' => $this->project?->getArrayCopy(),
'user' => $this->user?->getArrayCopy(),
'userId' => $this->userId,
'function' => $this->function?->getArrayCopy(),
'functionId' => $this->functionId,
'execution' => $this->execution?->getArrayCopy(),
'type' => $this->type,
'jwt' => $this->jwt,
'payload' => $this->payload,
'events' => $this->events,
'body' => $this->body,
'path' => $this->path,
'headers' => $this->headers,
'method' => $this->method,
'platform' => $platform,
];
}
public static function fromArray(array $data): static
{
return new self(
project: !empty($data['project']) ? new Document($data['project']) : null,
user: !empty($data['user']) ? new Document($data['user']) : null,
userId: $data['userId'] ?? null,
function: !empty($data['function']) ? new Document($data['function']) : null,
functionId: $data['functionId'] ?? null,
execution: !empty($data['execution']) ? new Document($data['execution']) : null,
type: $data['type'] ?? '',
jwt: $data['jwt'] ?? '',
payload: $data['payload'] ?? [],
events: $data['events'] ?? [],
body: $data['body'] ?? '',
path: $data['path'] ?? '',
headers: $data['headers'] ?? [],
method: $data['method'] ?? '',
platform: $data['platform'] ?? [],
);
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Config\Config;
use Utopia\Database\Document;
final class Mail extends Base
{
public function __construct(
public readonly ?Document $project = null,
public readonly string $recipient = '',
public readonly string $name = '',
public readonly string $subject = '',
public readonly string $bodyTemplate = '',
public readonly string $body = '',
public readonly string $preview = '',
public readonly array $smtp = [],
public readonly array $variables = [],
public readonly array $attachment = [],
public readonly array $customMailOptions = [],
public readonly array $events = [],
public readonly array $platform = [],
) {
}
public function toArray(): array
{
$platform = !empty($this->platform) ? $this->platform : Config::getParam('platform', []);
return [
'project' => $this->project?->getArrayCopy(),
'recipient' => $this->recipient,
'name' => $this->name,
'subject' => $this->subject,
'bodyTemplate' => $this->bodyTemplate,
'body' => $this->body,
'preview' => $this->preview,
'smtp' => $this->smtp,
'variables' => $this->variables,
'attachment' => $this->attachment,
'customMailOptions' => $this->customMailOptions,
'events' => $this->events,
'platform' => $platform,
];
}
public static function fromArray(array $data): static
{
return new self(
project: !empty($data['project']) ? new Document($data['project']) : null,
recipient: $data['recipient'] ?? '',
name: $data['name'] ?? '',
subject: $data['subject'] ?? '',
bodyTemplate: $data['bodyTemplate'] ?? '',
body: $data['body'] ?? '',
preview: $data['preview'] ?? '',
smtp: $data['smtp'] ?? [],
variables: $data['variables'] ?? [],
attachment: $data['attachment'] ?? [],
customMailOptions: $data['customMailOptions'] ?? [],
events: $data['events'] ?? [],
platform: $data['platform'] ?? [],
);
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Messaging extends Base
{
public function __construct(
public readonly string $type,
public readonly Document $project,
public readonly ?Document $user = null,
public readonly ?string $messageId = null,
public readonly ?Document $message = null,
public readonly ?array $recipients = null,
public readonly ?string $providerType = null,
) {
}
public function toArray(): array
{
return [
'type' => $this->type,
'project' => $this->project->getArrayCopy(),
'user' => $this->user?->getArrayCopy(),
'messageId' => $this->messageId,
'message' => $this->message?->getArrayCopy(),
'recipients' => $this->recipients,
'providerType' => $this->providerType,
];
}
public static function fromArray(array $data): static
{
return new self(
type: $data['type'] ?? '',
project: new Document($data['project'] ?? []),
user: !empty($data['user']) ? new Document($data['user']) : null,
messageId: $data['messageId'] ?? null,
message: !empty($data['message']) ? new Document($data['message']) : null,
recipients: $data['recipients'] ?? null,
providerType: $data['providerType'] ?? null,
);
}
}
-182
View File
@@ -1,182 +0,0 @@
<?php
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
use Utopia\System\System;
class Messaging extends Event
{
protected string $type = '';
protected ?string $messageId = null;
protected ?Document $message = null;
protected ?array $recipients = null;
protected ?string $scheduledAt = null;
protected ?string $providerType = null;
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME))
->setClass(System::getEnv('_APP_MESSAGING_CLASS_NAME', Event::MESSAGING_CLASS_NAME));
}
/**
* Sets type for the build event.
*
* @param string $type Can be `MESSAGE_SEND_TYPE_INTERNAL` or `MESSAGE_SEND_TYPE_EXTERNAL`.
* @return self
*/
public function setType(string $type): self
{
$this->type = $type;
return $this;
}
/**
* Returns set type for the function event.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Sets recipient for the messaging event.
*
* @param string[] $recipients
* @return self
*/
public function setRecipients(array $recipients): self
{
$this->recipients = $recipients;
return $this;
}
/**
* Returns set recipient for messaging event.
*
* @return string[]
*/
public function getRecipient(): array
{
return $this->recipients;
}
/**
* Sets message document for the messaging event.
*
* @param Document $message
* @return self
*/
public function setMessage(Document $message): self
{
$this->message = $message;
return $this;
}
/**
* Returns message document for the messaging event.
*
* @return Document
*/
public function getMessage(): Document
{
return $this->message;
}
/**
* Sets message ID for the messaging event.
*
* @param string $messageId
* @return self
*/
public function setMessageId(string $messageId): self
{
$this->messageId = $messageId;
return $this;
}
/**
* Returns set message ID for the messaging event.
*
* @return string
*/
public function getMessageId(): string
{
return $this->messageId;
}
/**
* Sets provider type for the messaging event.
*
* @param string $providerType
* @return self
*/
public function setProviderType(string $providerType): self
{
$this->providerType = $providerType;
return $this;
}
/**
* Returns set provider type for the messaging event.
*
* @return string
*/
public function getProviderType(): string
{
return $this->providerType;
}
/**
* Sets Scheduled delivery time for the messaging event.
*
* @param string $scheduledAt
* @return self
*/
public function setScheduledAt(string $scheduledAt): self
{
$this->scheduledAt = $scheduledAt;
return $this;
}
/**
* Returns set Delivery Time for the messaging event.
*
* @return string
*/
public function getScheduledAt(): string
{
return $this->scheduledAt;
}
/**
* Prepare the payload for the event
*
* @return array
*/
protected function preparePayload(): array
{
return [
'type' => $this->type,
'project' => $this->project,
'user' => $this->user,
'messageId' => $this->messageId,
'message' => $this->message,
'recipients' => $this->recipients,
'providerType' => $this->providerType,
];
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Build as BuildMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Build extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(BuildMessage $message, ?Queue $queue = null): string|bool
{
return $this->publish($queue ?? $this->queue, $message);
}
public function getSize(bool $failed = false, ?Queue $queue = null): int
{
return $this->getQueueSize($queue ?? $this->queue, $failed);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Delete extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue,
) {
parent::__construct($publisher);
}
public function enqueue(DeleteMessage $message, ?Queue $queue = null): string|bool
{
return $this->publish($queue ?? $this->queue, $message);
}
public function getSize(bool $failed = false, ?Queue $queue = null): int
{
return $this->getQueueSize($queue ?? $this->queue, $failed);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Func as FunctionMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Func extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue,
) {
parent::__construct($publisher);
}
public function enqueue(FunctionMessage $message, ?Queue $queue = null): string|bool
{
return $this->publish($queue ?? $this->queue, $message);
}
public function getSize(bool $failed = false, ?Queue $queue = null): int
{
return $this->getQueueSize($queue ?? $this->queue, $failed);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Mail as MailMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Mail extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(MailMessage $message, ?Queue $queue = null): string|bool
{
return $this->publish($queue ?? $this->queue, $message);
}
public function getSize(bool $failed = false, ?Queue $queue = null): int
{
return $this->getQueueSize($queue ?? $this->queue, $failed);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Messaging as MessagingMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Messaging extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(MessagingMessage $message, ?Queue $queue = null): string|bool
{
return $this->publish($queue ?? $this->queue, $message);
}
public function getSize(bool $failed = false, ?Queue $queue = null): int
{
return $this->getQueueSize($queue ?? $this->queue, $failed);
}
}
+12
View File
@@ -348,6 +348,10 @@ class Exception extends \Exception
public const string MIGRATION_IN_PROGRESS = 'migration_in_progress';
public const string MIGRATION_PROVIDER_ERROR = 'migration_provider_error';
public const string MIGRATION_DATABASE_TYPE_UNSUPPORTED = 'migration_database_type_unsupported';
public const string MIGRATION_SOURCE_PROJECT_ID_REQUIRED = 'migration_source_project_id_required';
public const string MIGRATION_SOURCE_PROJECT_NOT_FOUND = 'migration_source_project_not_found';
public const string MIGRATION_SOURCE_TYPE_INVALID = 'migration_source_type_invalid';
public const string MIGRATION_DESTINATION_TYPE_INVALID = 'migration_destination_type_invalid';
/** Realtime */
public const string REALTIME_MESSAGE_FORMAT_INVALID = 'realtime_message_format_invalid';
@@ -402,6 +406,14 @@ class Exception extends \Exception
public const string TOKEN_EXPIRED = 'token_expired';
public const string TOKEN_RESOURCE_TYPE_INVALID = 'token_resource_type_invalid';
/** Advisor */
public const string INSIGHT_NOT_FOUND = 'insight_not_found';
public const string INSIGHT_ALREADY_EXISTS = 'insight_already_exists';
/** Reports */
public const string REPORT_NOT_FOUND = 'report_not_found';
public const string REPORT_ALREADY_EXISTS = 'report_already_exists';
protected string $type = '';
protected array $errors = [];
protected bool $publish;
+21 -39
View File
@@ -6,7 +6,6 @@ use Appwrite\GraphQL\Exception as GQLException;
use Appwrite\Promises\Swoole;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\DI\Container;
use Utopia\Http\Exception;
use Utopia\Http\Http;
use Utopia\Http\Route;
@@ -53,22 +52,6 @@ class Resolvers
}
}
/**
* Get the current request container.
*/
private static function getResolverContainer(Http $utopia): Container
{
$container = $utopia->getResource('container');
if ($container instanceof Container || (\is_object($container) && \method_exists($container, 'get') && \method_exists($container, 'set'))) {
/** @var Container $container */
return $container;
}
/** @var callable(): Container $container */
return $container();
}
/**
* Get the request-scoped lock shared by GraphQL resolver coroutines
* for the current HTTP request.
@@ -95,9 +78,9 @@ class Resolvers
?Route $route,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->context()->get('utopia:graphql');
$request = $utopia->context()->get('request');
$response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -167,9 +150,9 @@ class Resolvers
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->context()->get('utopia:graphql');
$request = $utopia->context()->get('request');
$response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -203,9 +186,9 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->context()->get('utopia:graphql');
$request = $utopia->context()->get('request');
$response = $utopia->context()->get('response');
$beforeResolve = function ($payload) {
return $payload['documents'];
@@ -245,9 +228,9 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->context()->get('utopia:graphql');
$request = $utopia->context()->get('request');
$response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -282,9 +265,9 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->context()->get('utopia:graphql');
$request = $utopia->context()->get('request');
$response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -317,9 +300,9 @@ class Resolvers
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$utopia = $utopia->context()->get('utopia:graphql');
$request = $utopia->context()->get('request');
$response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -373,10 +356,9 @@ class Resolvers
}
/** @var Response $resolverResponse */
$resolverResponse = clone $utopia->getResource('response');
$container = self::getResolverContainer($utopia);
$container->set('request', static fn () => $request);
$container->set('response', static fn () => $resolverResponse);
$resolverResponse = clone $utopia->context()->get('response');
$utopia->context()->set('request', static fn () => $request);
$utopia->context()->set('response', static fn () => $resolverResponse);
$resolverResponse->setContentType(Response::CONTENT_TYPE_NULL);
$resolverResponse->setSent(false);
+1 -1
View File
@@ -84,7 +84,7 @@ class Schema
protected static function api(Http $utopia, callable $complexity): array
{
Mapper::init($utopia
->getResource('response')
->context()->get('response')
->getModels());
$queries = [];
+1 -1
View File
@@ -254,7 +254,7 @@ class Mapper
array $injections
): Type {
$validator = \is_callable($validator)
? \call_user_func_array($validator, $utopia->getResources($injections))
? \call_user_func_array($validator, \array_map($utopia->context()->get(...), $injections))
: $validator;
$isNullable = $validator instanceof Nullable;
@@ -774,6 +774,21 @@ class Realtime extends MessagingAdapter
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
}
break;
case 'reports':
// Plain report event: `reports.{reportId}.{action}`
$channels[] = 'reports';
if (isset($parts[1])) {
$channels[] = 'reports.' . $parts[1];
}
// Nested insight event: `reports.{reportId}.insights.{insightId}.{action}`
if (isset($parts[2]) && $parts[2] === 'insights') {
$channels[] = 'reports.' . $parts[1] . '.insights';
if (isset($parts[3])) {
$channels[] = 'reports.' . $parts[1] . '.insights.' . $parts[3];
}
}
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
break;
}
// Action is the last segment for plain CRUD events (e.g. `documents.X.create`),
+1
View File
@@ -97,6 +97,7 @@ abstract class Migration
'1.9.2' => 'V24',
'1.9.3' => 'V24',
'1.9.4' => 'V24',
'1.9.5' => 'V24',
];
/**
+2
View File
@@ -3,6 +3,7 @@
namespace Appwrite\Platform;
use Appwrite\Platform\Modules\Account;
use Appwrite\Platform\Modules\Advisor;
use Appwrite\Platform\Modules\Avatars;
use Appwrite\Platform\Modules\Console;
use Appwrite\Platform\Modules\Core;
@@ -42,5 +43,6 @@ class Appwrite extends Platform
$this->addModule(new Webhooks\Module());
$this->addModule(new Migrations\Module());
$this->addModule(new Project\Module());
$this->addModule(new Advisor\Module());
}
}
+1 -1
View File
@@ -154,7 +154,7 @@ class Server
$nativeServer = $adapter->getNativeServer();
$container = $adapter->getContainer();
$container = $adapter->resources();
$container->set('installerState', fn () => $state);
$container->set('installerConfig', fn () => $config);
$container->set('installerPaths', fn () => $paths);
@@ -5,8 +5,10 @@ namespace Appwrite\Platform\Modules\Account\Http\Account\MFA\Challenges;
use Appwrite\Auth\MFA\Type;
use Appwrite\Detector\Detector;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Message\Messaging as MessagingMessage;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -101,8 +103,8 @@ class Create extends Action
->inject('platform')
->inject('request')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('queueForMails')
->inject('publisherForMessaging')
->inject('publisherForMails')
->inject('timelimit')
->inject('usage')
->inject('plan')
@@ -121,8 +123,8 @@ class Create extends Action
array $platform,
Request $request,
Event $queueForEvents,
Messaging $queueForMessaging,
Mail $queueForMails,
MessagingPublisher $publisherForMessaging,
MailPublisher $publisherForMails,
callable $timelimit,
Context $usage,
array $plan,
@@ -180,16 +182,18 @@ class Create extends Action
$message = $message->render();
$phone = $user->getAttribute('phone');
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_INTERNAL)
->setMessage(new Document([
$publisherForMessaging->enqueue(new MessagingMessage(
type: MESSAGE_SEND_TYPE_INTERNAL,
project: $project,
message: new Document([
'$id' => $challenge->getId(),
'data' => [
'content' => $code,
],
]))
->setRecipients([$phone])
->setProviderType(MESSAGE_TYPE_SMS);
]),
recipients: [$phone],
providerType: MESSAGE_TYPE_SMS,
));
$helper = PhoneNumberUtil::getInstance();
try {
@@ -252,6 +256,7 @@ class Create extends Action
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
$smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -269,13 +274,6 @@ class Create extends Action
$replyToName = $smtp['replyToName'];
}
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '');
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -296,11 +294,17 @@ class Create extends Action
$subject = $customTemplate['subject'] ?? $subject;
}
$queueForMails
->setSmtpReplyToEmail($replyToEmail)
->setSmtpReplyToName($replyToName)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
$smtpConfig = [
'host' => $smtp['host'] ?? '',
'port' => $smtp['port'] ?? '',
'username' => $smtp['username'] ?? '',
'password' => $smtp['password'] ?? '',
'secure' => $smtp['secure'] ?? '',
'replyToEmail' => $replyToEmail,
'replyToName' => $replyToName,
'senderEmail' => $senderEmail,
'senderName' => $senderName,
];
}
$emailVariables = [
@@ -327,20 +331,18 @@ class Create extends Action
]);
}
$queueForMails
->setSubject($subject)
->setPreview($preview)
->setBody($body)
->setBodyTemplate($bodyTemplate)
->appendVariables($emailVariables)
->setRecipient($user->getAttribute('email'));
// since this is console project, set email sender name!
if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) {
$queueForMails->setSenderName($platform['emailSenderName']);
}
$queueForMails->trigger();
$publisherForMails->enqueue(new MailMessage(
project: $project,
recipient: $user->getAttribute('email'),
subject: $subject,
bodyTemplate: $bodyTemplate,
body: $body,
preview: $preview,
smtp: $smtpConfig,
variables: $emailVariables,
customMailOptions: $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE ? ['senderName' => $platform['emailSenderName']] : [],
platform: $platform,
));
break;
}
@@ -0,0 +1,8 @@
<?php
namespace Appwrite\Platform\Modules\Advisor\Enums;
enum InsightCTAMethod: string
{
case CREATE_INDEX = 'createIndex';
}
@@ -0,0 +1,11 @@
<?php
namespace Appwrite\Platform\Modules\Advisor\Enums;
enum InsightCTAService: string
{
case DATABASES = 'databases';
case TABLES_DB = 'tablesDB';
case DOCUMENTS_DB = 'documentsDB';
case VECTORS_DB = 'vectorsDB';
}
@@ -0,0 +1,10 @@
<?php
namespace Appwrite\Platform\Modules\Advisor\Enums;
enum InsightSeverity: string
{
case INFO = 'info';
case WARNING = 'warning';
case CRITICAL = 'critical';
}
@@ -0,0 +1,9 @@
<?php
namespace Appwrite\Platform\Modules\Advisor\Enums;
enum InsightStatus: string
{
case ACTIVE = 'active';
case DISMISSED = 'dismissed';
}
@@ -0,0 +1,16 @@
<?php
namespace Appwrite\Platform\Modules\Advisor\Enums;
enum InsightType: string
{
case DATABASE_INDEX = 'databaseIndex';
case TABLES_DB_INDEX = 'tablesDBIndex';
case DOCUMENTS_DB_INDEX = 'documentsDBIndex';
case VECTORS_DB_INDEX = 'vectorsDBIndex';
case DATABASE_PERFORMANCE = 'databasePerformance';
case SITE_PERFORMANCE = 'sitePerformance';
case SITE_ACCESSIBILITY = 'siteAccessibility';
case SITE_SEO = 'siteSeo';
case FUNCTION_PERFORMANCE = 'functionPerformance';
}
@@ -0,0 +1,10 @@
<?php
namespace Appwrite\Platform\Modules\Advisor\Enums;
enum ReportType: string
{
case LIGHTHOUSE = 'lighthouse';
case AUDIT = 'audit';
case DATABASE_ANALYZER = 'databaseAnalyzer';
}
@@ -0,0 +1,84 @@
<?php
namespace Appwrite\Platform\Modules\Advisor\Http\Insights;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
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\Validator\UID;
use Utopia\Platform\Scope\HTTP;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getInsight';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/reports/:reportId/insights/:insightId')
->desc('Get insight')
->groups(['api', 'advisor'])
->label('scope', 'insights.read')
->label('resourceType', RESOURCE_TYPE_INSIGHTS)
->label('sdk', new Method(
namespace: 'advisor',
group: 'insights',
name: 'getInsight',
description: '/docs/references/advisor/get-insight.md',
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_INSIGHT,
),
]
))
->param('reportId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Parent report ID.', false, ['dbForPlatform'])
->param('insightId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Insight ID.', false, ['dbForPlatform'])
->inject('response')
->inject('project')
->inject('dbForPlatform')
->callback($this->action(...));
}
public function action(
string $reportId,
string $insightId,
Response $response,
Document $project,
Database $dbForPlatform
) {
// Skip the insights subquery — we only need ownership metadata.
$report = $dbForPlatform->skipFilters(
fn () => $dbForPlatform->getDocument('reports', $reportId),
['subQueryReportInsights'],
);
if ($report->isEmpty() || $report->getAttribute('projectInternalId') !== $project->getSequence()) {
throw new Exception(Exception::REPORT_NOT_FOUND);
}
$insight = $dbForPlatform->getDocument('insights', $insightId);
if (
$insight->isEmpty()
|| $insight->getAttribute('projectInternalId') !== $project->getSequence()
|| $insight->getAttribute('reportInternalId') !== $report->getSequence()
) {
throw new Exception(Exception::INSIGHT_NOT_FOUND);
}
$response->dynamic($insight, Response::MODEL_INSIGHT);
}
}
@@ -0,0 +1,126 @@
<?php
namespace Appwrite\Platform\Modules\Advisor\Http\Insights;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Insights;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Action
{
use HTTP;
public static function getName()
{
return 'listInsights';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/reports/:reportId/insights')
->desc('List insights')
->groups(['api', 'advisor'])
->label('scope', 'insights.read')
->label('resourceType', RESOURCE_TYPE_INSIGHTS)
->label('sdk', new Method(
namespace: 'advisor',
group: 'insights',
name: 'listInsights',
description: '/docs/references/advisor/list-insights.md',
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_INSIGHT_LIST,
),
]
))
->param('reportId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Parent report ID.', false, ['dbForPlatform'])
->param('queries', [], new Insights(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Insights::ALLOWED_ATTRIBUTES), true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('project')
->inject('dbForPlatform')
->callback($this->action(...));
}
public function action(
string $reportId,
array $queries,
bool $includeTotal,
Response $response,
Document $project,
Database $dbForPlatform
) {
// Skip the insights subquery — we're about to fetch a filtered, paginated slice ourselves.
$report = $dbForPlatform->skipFilters(
fn () => $dbForPlatform->getDocument('reports', $reportId),
['subQueryReportInsights'],
);
if ($report->isEmpty() || $report->getAttribute('projectInternalId') !== $project->getSequence()) {
throw new Exception(Exception::REPORT_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$queries[] = Query::equal('projectInternalId', [$project->getSequence()]);
$queries[] = Query::equal('reportInternalId', [$report->getSequence()]);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$insightId = $cursor->getValue();
$cursorDocument = $dbForPlatform->getDocument('insights', $insightId);
if (
$cursorDocument->isEmpty()
|| $cursorDocument->getAttribute('projectInternalId') !== $project->getSequence()
|| $cursorDocument->getAttribute('reportInternalId') !== $report->getSequence()
) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Insight '{$insightId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$insights = $dbForPlatform->find('insights', $queries);
$total = $includeTotal ? $dbForPlatform->count('insights', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
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.");
}
$response->dynamic(new Document([
'insights' => $insights,
'total' => $total,
]), Response::MODEL_INSIGHT_LIST);
}
}
@@ -0,0 +1,100 @@
<?php
namespace Appwrite\Platform\Modules\Advisor\Http\Reports;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
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;
class Delete extends Action
{
use HTTP;
public static function getName(): string
{
return 'deleteReport';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/reports/:reportId')
->desc('Delete report')
->groups(['api', 'advisor'])
->label('scope', 'reports.write')
->label('event', 'reports.[reportId].delete')
->label('resourceType', RESOURCE_TYPE_REPORTS)
->label('audits.event', 'report.delete')
->label('audits.resource', 'report/{request.reportId}')
->label('abuse-key', 'projectId:{projectId},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', new Method(
namespace: 'advisor',
group: 'reports',
name: 'deleteReport',
description: '/docs/references/advisor/delete-report.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
),
],
contentType: ContentType::NONE
))
->param('reportId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Report ID.', false, ['dbForPlatform'])
->inject('response')
->inject('project')
->inject('dbForPlatform')
->inject('publisherForDeletes')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(
string $reportId,
Response $response,
Document $project,
Database $dbForPlatform,
DeletePublisher $publisherForDeletes,
Event $queueForEvents
): void {
$report = $dbForPlatform->skipFilters(
fn () => $dbForPlatform->getDocument('reports', $reportId),
['subQueryReportInsights'],
);
if ($report->isEmpty() || $report->getAttribute('projectInternalId') !== $project->getSequence()) {
throw new Exception(Exception::REPORT_NOT_FOUND);
}
if (!$dbForPlatform->deleteDocument('reports', $report->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove report from DB');
}
$publisherForDeletes->enqueue(new DeleteMessage(
project: $project,
type: DELETE_TYPE_REPORT,
document: $report,
));
$queueForEvents
->setParam('reportId', $report->getId())
->setPayload($response->output($report, Response::MODEL_REPORT));
$response->noContent();
}
}
@@ -0,0 +1,80 @@
<?php
namespace Appwrite\Platform\Modules\Advisor\Http\Reports;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
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\UID;
use Utopia\Platform\Scope\HTTP;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getReport';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/reports/:reportId')
->desc('Get report')
->groups(['api', 'advisor'])
->label('scope', 'reports.read')
->label('resourceType', RESOURCE_TYPE_REPORTS)
->label('sdk', new Method(
namespace: 'advisor',
group: 'reports',
name: 'getReport',
description: '/docs/references/advisor/get-report.md',
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_REPORT,
),
]
))
->param('reportId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Report ID.', false, ['dbForPlatform'])
->inject('response')
->inject('project')
->inject('dbForPlatform')
->callback($this->action(...));
}
public function action(
string $reportId,
Response $response,
Document $project,
Database $dbForPlatform
) {
$report = $dbForPlatform->skipFilters(
fn () => $dbForPlatform->getDocument('reports', $reportId),
['subQueryReportInsights'],
);
if ($report->isEmpty() || $report->getAttribute('projectInternalId') !== $project->getSequence()) {
throw new Exception(Exception::REPORT_NOT_FOUND);
}
$insights = $dbForPlatform->find('insights', [
Query::equal('projectInternalId', [$project->getSequence()]),
Query::equal('reportInternalId', [$report->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
$report->setAttribute('insights', $insights);
$response->dynamic($report, Response::MODEL_REPORT);
}
}
@@ -0,0 +1,133 @@
<?php
namespace Appwrite\Platform\Modules\Advisor\Http\Reports;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Reports;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Action
{
use HTTP;
public static function getName()
{
return 'listReports';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/reports')
->desc('List reports')
->groups(['api', 'advisor'])
->label('scope', 'reports.read')
->label('resourceType', RESOURCE_TYPE_REPORTS)
->label('sdk', new Method(
namespace: 'advisor',
group: 'reports',
name: 'listReports',
description: '/docs/references/advisor/list-reports.md',
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_REPORT_LIST,
),
]
))
->param('queries', [], new Reports(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Reports::ALLOWED_ATTRIBUTES), true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('project')
->inject('dbForPlatform')
->callback($this->action(...));
}
public function action(
array $queries,
bool $includeTotal,
Response $response,
Document $project,
Database $dbForPlatform
) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$queries[] = Query::equal('projectInternalId', [$project->getSequence()]);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$reportId = $cursor->getValue();
$cursorDocument = $dbForPlatform->skipFilters(
fn () => $dbForPlatform->getDocument('reports', $reportId),
['subQueryReportInsights'],
);
if ($cursorDocument->isEmpty() || $cursorDocument->getAttribute('projectInternalId') !== $project->getSequence()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Report '{$reportId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$reports = $dbForPlatform->skipFilters(
fn () => $dbForPlatform->find('reports', $queries),
['subQueryReportInsights'],
);
$total = $includeTotal ? $dbForPlatform->count('reports', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
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.");
}
if (!empty($reports)) {
$reportSequences = \array_map(fn (Document $r) => $r->getSequence(), $reports);
$insights = $dbForPlatform->find('insights', [
Query::equal('projectInternalId', [$project->getSequence()]),
Query::equal('reportInternalId', $reportSequences),
Query::limit(APP_LIMIT_SUBQUERY),
]);
$insightsByReport = [];
foreach ($insights as $insight) {
$insightsByReport[$insight->getAttribute('reportInternalId')][] = $insight;
}
foreach ($reports as $report) {
$report->setAttribute('insights', $insightsByReport[$report->getSequence()] ?? []);
}
}
$response->dynamic(new Document([
'reports' => $reports,
'total' => $total,
]), Response::MODEL_REPORT_LIST);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Appwrite\Platform\Modules\Advisor;
use Appwrite\Platform\Modules\Advisor\Services\Http;
use Utopia\Platform;
class Module extends Platform\Module
{
public function __construct()
{
$this->addService('http', new Http());
}
}
@@ -0,0 +1,25 @@
<?php
namespace Appwrite\Platform\Modules\Advisor\Services;
use Appwrite\Platform\Modules\Advisor\Http\Insights\Get as GetInsight;
use Appwrite\Platform\Modules\Advisor\Http\Insights\XList as ListInsights;
use Appwrite\Platform\Modules\Advisor\Http\Reports\Delete as DeleteReport;
use Appwrite\Platform\Modules\Advisor\Http\Reports\Get as GetReport;
use Appwrite\Platform\Modules\Advisor\Http\Reports\XList as ListReports;
use Utopia\Platform\Service;
class Http extends Service
{
public function __construct()
{
$this->type = Service::TYPE_HTTP;
$this->addAction(GetReport::getName(), new GetReport());
$this->addAction(ListReports::getName(), new ListReports());
$this->addAction(DeleteReport::getName(), new DeleteReport());
$this->addAction(GetInsight::getName(), new GetInsight());
$this->addAction(ListInsights::getName(), new ListInsights());
}
}
+20 -13
View File
@@ -2,7 +2,8 @@
namespace Appwrite\Platform\Modules\Compute;
use Appwrite\Event\Build;
use Appwrite\Event\Message\Build as BuildMessage;
use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Filter\BranchDomain as BranchDomainFilter;
use Appwrite\Platform\Action;
@@ -57,7 +58,7 @@ class Base extends Action
return $allowedSpecifications[0] ?? APP_COMPUTE_SPECIFICATION_DEFAULT;
}
public function redeployVcsFunction(Request $request, Document $function, Document $project, Document $installation, Database $dbForProject, Build $queueForBuilds, Document $template, GitHub $github, bool $activate, string $referenceType = 'branch', string $reference = ''): Document
public function redeployVcsFunction(Request $request, Document $function, Document $project, Document $installation, Database $dbForProject, BuildPublisher $publisherForBuilds, Document $template, GitHub $github, bool $activate, array $platform = [], string $referenceType = 'branch', string $reference = ''): Document
{
$deploymentId = ID::unique();
$entrypoint = $function->getAttribute('entrypoint', '');
@@ -150,16 +151,19 @@ class Base extends Action
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($function)
->setDeployment($deployment)
->setTemplate($template);
$publisherForBuilds->enqueue(new BuildMessage(
project: $project,
resource: $function,
deployment: $deployment,
type: BUILD_TYPE_DEPLOYMENT,
template: $template,
platform: $platform,
));
return $deployment;
}
public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Database $dbForPlatform, Build $queueForBuilds, Document $template, GitHub $github, bool $activate, Authorization $authorization, array $platform, string $referenceType = 'branch', string $reference = ''): Document
public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Database $dbForPlatform, BuildPublisher $publisherForBuilds, Document $template, GitHub $github, bool $activate, Authorization $authorization, array $platform, string $referenceType = 'branch', string $reference = ''): Document
{
$deploymentId = ID::unique();
$providerInstallationId = $installation->getAttribute('providerInstallationId', '');
@@ -358,11 +362,14 @@ class Base extends Action
$this->updateEmptyManualRule($project, $site, $deployment, $dbForPlatform, $authorization);
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($site)
->setDeployment($deployment)
->setTemplate($template);
$publisherForBuilds->enqueue(new BuildMessage(
project: $project,
resource: $site,
deployment: $deployment,
type: BUILD_TYPE_DEPLOYMENT,
template: $template,
platform: $platform,
));
return $deployment;
}
@@ -3,6 +3,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Databases\Http\Databases\Action as DatabasesAction;
@@ -421,7 +423,7 @@ abstract class Action extends DatabasesAction
* @param Document[] $documents
* @param Event $queueForEvents
* @param Event $queueForRealtime
* @param Event $queueForFunctions
* @param FunctionPublisher $publisherForFunctions
* @param Event $queueForWebhooks
* @param Database $dbForProject
* @param EventProcessor $eventProcessor
@@ -434,7 +436,7 @@ abstract class Action extends DatabasesAction
array $documents,
Event $queueForEvents,
Event $queueForRealtime,
Event $queueForFunctions,
FunctionPublisher $publisherForFunctions,
Event $queueForWebhooks,
Database $dbForProject,
EventProcessor $eventProcessor
@@ -472,9 +474,15 @@ abstract class Action extends DatabasesAction
if (!empty($functionsEvents)) {
foreach ($generatedEvents as $event) {
if (isset($functionsEvents[$event])) {
$queueForFunctions
->from($queueForEvents)
->trigger();
$publisherForFunctions->enqueue(FunctionMessage::fromEvent(
event: $queueForEvents->getEvent(),
params: $queueForEvents->getParams(),
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
userId: $queueForEvents->getUserId(),
payload: $queueForEvents->getPayload(),
platform: $queueForEvents->getPlatform(),
));
break;
}
}
@@ -494,7 +502,6 @@ abstract class Action extends DatabasesAction
$queueForEvents->reset();
$queueForRealtime->reset();
$queueForFunctions->reset();
$queueForWebhooks->reset();
}
}
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
@@ -80,14 +81,14 @@ class Delete extends Action
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
@@ -206,7 +207,7 @@ class Delete extends Action
$documents,
$queueForEvents,
$queueForRealtime,
$queueForFunctions,
$publisherForFunctions,
$queueForWebhooks,
$dbForProject,
$eventProcessor
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
@@ -84,14 +85,14 @@ class Update extends Action
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string|array $data, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
public function action(string $databaseId, string $collectionId, string|array $data, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
{
$data = \is_string($data)
? \json_decode($data, true)
@@ -237,7 +238,7 @@ class Update extends Action
$documents,
$queueForEvents,
$queueForRealtime,
$queueForFunctions,
$publisherForFunctions,
$queueForWebhooks,
$dbForProject,
$eventProcessor
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
@@ -82,14 +83,14 @@ class Upsert extends Action
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
public function action(string $databaseId, string $collectionId, array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
@@ -212,7 +213,7 @@ class Upsert extends Action
$upserted,
$queueForEvents,
$queueForRealtime,
$queueForFunctions,
$publisherForFunctions,
$queueForWebhooks,
$dbForProject,
$eventProcessor
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\SDK\AuthType;
@@ -137,7 +138,7 @@ class Create extends Action
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('authorization')
@@ -145,7 +146,7 @@ class Create extends Action
->callback($this->action(...));
}
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
{
$data = \is_string($data)
? \json_decode($data, true)
@@ -517,7 +518,7 @@ class Create extends Action
$created,
$queueForEvents,
$queueForRealtime,
$queueForFunctions,
$publisherForFunctions,
$queueForWebhooks,
$dbForProject,
$eventProcessor
@@ -2,7 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -10,6 +11,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\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
@@ -51,11 +53,12 @@ class Delete extends Action
->param('transactionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Transaction ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('project')
->callback($this->action(...));
}
public function action(string $transactionId, UtopiaResponse $response, Database $dbForProject, DeleteEvent $queueForDeletes): void
public function action(string $transactionId, UtopiaResponse $response, Database $dbForProject, DeletePublisher $publisherForDeletes, Document $project): void
{
$transaction = $dbForProject->getDocument('transactions', $transactionId);
@@ -65,9 +68,11 @@ class Delete extends Action
$dbForProject->deleteDocument('transactions', $transactionId);
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($transaction);
$publisherForDeletes->enqueue(new DeleteMessage(
project: $project,
type: DELETE_TYPE_DOCUMENT,
document: $transaction,
));
$response->noContent();
}
@@ -3,8 +3,11 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\SDK\AuthType;
@@ -73,11 +76,11 @@ class Update extends Action
->inject('getDatabasesDB')
->inject('user')
->inject('transactionState')
->inject('queueForDeletes')
->inject('publisherForDeletes')
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('authorization')
->inject('eventProcessor')
@@ -93,11 +96,11 @@ class Update extends Action
* @param callable $getDatabasesDB
* @param User $user
* @param TransactionState $transactionState
* @param Delete $queueForDeletes
* @param DeletePublisher $publisherForDeletes
* @param Event $queueForEvents
* @param Context $usage
* @param Event $queueForRealtime
* @param Event $queueForFunctions
* @param FunctionPublisher $publisherForFunctions
* @param Event $queueForWebhooks
* @param EventProcessor $eventProcessor
* @return void
@@ -108,7 +111,7 @@ class Update extends Action
* @throws StructureException
* @throws \Utopia\Http\Exception
*/
public function action(string $transactionId, bool $commit, bool $rollback, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
public function action(string $transactionId, bool $commit, bool $rollback, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, TransactionState $transactionState, DeletePublisher $publisherForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
{
if (!$commit && !$rollback) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true');
@@ -154,9 +157,11 @@ class Update extends Action
new Document(['status' => 'committed'])
));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($transaction);
$publisherForDeletes->enqueue(new DeleteMessage(
project: $project,
type: DELETE_TYPE_DOCUMENT,
document: $transaction,
));
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
@@ -293,9 +298,11 @@ class Update extends Action
new Document(['status' => 'committed'])
));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($transaction);
$publisherForDeletes->enqueue(new DeleteMessage(
project: $project,
type: DELETE_TYPE_DOCUMENT,
document: $transaction,
));
} catch (NotFoundException $e) {
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
'status' => 'failed',
@@ -461,7 +468,15 @@ class Update extends Action
if (!empty($functionsEvents)) {
foreach ($generatedEvents as $event) {
if (isset($functionsEvents[$event])) {
$queueForFunctions->from($queueForEvents)->trigger();
$publisherForFunctions->enqueue(FunctionMessage::fromEvent(
event: $queueForEvents->getEvent(),
params: $queueForEvents->getParams(),
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
userId: $queueForEvents->getUserId(),
payload: $queueForEvents->getPayload(),
platform: $queueForEvents->getPlatform(),
));
break;
}
}
@@ -480,7 +495,6 @@ class Update extends Action
$queueForEvents->reset();
$queueForRealtime->reset();
$queueForFunctions->reset();
$queueForWebhooks->reset();
}
}
@@ -492,9 +506,11 @@ class Update extends Action
new Document(['status' => 'failed'])
));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($transaction);
$publisherForDeletes->enqueue(new DeleteMessage(
project: $project,
type: DELETE_TYPE_DOCUMENT,
document: $transaction,
));
}
$response
@@ -63,7 +63,7 @@ class Delete extends DocumentsDelete
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
@@ -65,7 +65,7 @@ class Update extends DocumentsUpdate
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
@@ -65,7 +65,7 @@ class Upsert extends DocumentsUpsert
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
@@ -112,7 +112,7 @@ class Create extends DocumentCreate
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('authorization')

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