mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
142
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d0950b8bc | ||
|
|
647b4bc632 | ||
|
|
d38c921781 | ||
|
|
44610462b3 | ||
|
|
28ece7de02 | ||
|
|
8c6d4d8b36 | ||
|
|
2ebc6f70ef | ||
|
|
2b7690d6db | ||
|
|
d9af799cc7 | ||
|
|
3ed1ca736d | ||
|
|
9fa35db838 | ||
|
|
b6e020389b | ||
|
|
ccc0cfbfdc | ||
|
|
b91506fc2d | ||
|
|
9ffc23946c | ||
|
|
8bb6b5cd2a | ||
|
|
afea4ca57b | ||
|
|
b30b89ade9 | ||
|
|
829cf887dc | ||
|
|
32728ae4a0 | ||
|
|
d862a64874 | ||
|
|
168166b9c1 | ||
|
|
142e671c7d | ||
|
|
b4085d1083 | ||
|
|
18ed6a9c59 | ||
|
|
3d64ccd056 | ||
|
|
4f73eb021f | ||
|
|
7dcfd3ff3d | ||
|
|
4dfdfb5e59 | ||
|
|
5d1009b324 | ||
|
|
4568316345 | ||
|
|
c444bccacc | ||
|
|
f9aee4de5d | ||
|
|
037878cc75 | ||
|
|
416b26ac7d | ||
|
|
b47ac00ca8 | ||
|
|
2f53d09c5b | ||
|
|
1fa1aa8621 | ||
|
|
ec20fb59e2 | ||
|
|
7e49cf0bed | ||
|
|
4e32497be1 | ||
|
|
fe6f79c9c6 | ||
|
|
5c84efdd7c | ||
|
|
02d234ad3a | ||
|
|
8b7459f634 | ||
|
|
100cbf50f0 | ||
|
|
c7c59f4e7b | ||
|
|
a955dff55a | ||
|
|
7bcd5b4ebc | ||
|
|
dfb23612b0 | ||
|
|
4e71a54fae | ||
|
|
ea032c2804 | ||
|
|
a80ecd0cb6 | ||
|
|
8be7c6182e | ||
|
|
3bb6a8bcc8 | ||
|
|
c72d438a10 | ||
|
|
0085d93aeb | ||
|
|
3b9b0c96c4 | ||
|
|
2611bf4af1 | ||
|
|
de219de31d | ||
|
|
d8bbd82556 | ||
|
|
5d962e21b0 | ||
|
|
551c83dd2c | ||
|
|
d964c5a439 | ||
|
|
4850fb54ed | ||
|
|
796bc13c19 | ||
|
|
30e080c4d3 | ||
|
|
aaebeec61e | ||
|
|
1789d2ce89 | ||
|
|
239a9c5457 | ||
|
|
995e9fdf39 | ||
|
|
8ce42fe057 | ||
|
|
c14ebab1a0 | ||
|
|
f1ea496764 | ||
|
|
95b6da085b | ||
|
|
32005c0a49 | ||
|
|
98c63a87d1 | ||
|
|
c5b9ac3dba | ||
|
|
be50318e5d | ||
|
|
f901b6e0ac | ||
|
|
53d0e14f97 | ||
|
|
325b1d67b4 | ||
|
|
3ff7cadcab | ||
|
|
f2c8500d73 | ||
|
|
babb0196e4 | ||
|
|
9353fbfb74 | ||
|
|
918f1d37d4 | ||
|
|
196acd45a2 | ||
|
|
ef6fdc6c72 | ||
|
|
915a8bfbe0 | ||
|
|
741267c6c5 | ||
|
|
e85080499a | ||
|
|
ee1ca5ace6 | ||
|
|
b36472f0da | ||
|
|
52ae8b3880 | ||
|
|
8f09e74462 | ||
|
|
30907d716f | ||
|
|
eb46855e72 | ||
|
|
45557e5929 | ||
|
|
6ad6a5dea3 | ||
|
|
098f7aa3e3 | ||
|
|
f8c8c17757 | ||
|
|
5b1ee93927 | ||
|
|
5fbaa7ab6e | ||
|
|
e8a0a895ed | ||
|
|
560ebeaddd | ||
|
|
0731cc1535 | ||
|
|
983d0b8fad | ||
|
|
92423acc01 | ||
|
|
42414a46b0 | ||
|
|
b7bd86d634 | ||
|
|
7a39da0afd | ||
|
|
9aa488c961 | ||
|
|
cfc325635d | ||
|
|
5c47d4f48b | ||
|
|
7aff75ae1c | ||
|
|
651a24a211 | ||
|
|
6536463d49 | ||
|
|
82d7926c4b | ||
|
|
6041468fc4 | ||
|
|
669f323156 | ||
|
|
1ab05acc93 | ||
|
|
2f66ae5533 | ||
|
|
87c462ebd4 | ||
|
|
f89621cf74 | ||
|
|
4fb8d873b1 | ||
|
|
c5186324ed | ||
|
|
b95b4f12f9 | ||
|
|
52efca0832 | ||
|
|
ee3d4dfeee | ||
|
|
b742f1c50d | ||
|
|
f3fcdec1be | ||
|
|
647099efc7 | ||
|
|
d535b6b39b | ||
|
|
2057eab918 | ||
|
|
f89b3274de | ||
|
|
956edae593 | ||
|
|
617d6fe1eb | ||
|
|
a7cdfed253 | ||
|
|
ce5d5cf6ef | ||
|
|
2017183ecc | ||
|
|
1a1740ac7a |
+16
-31
@@ -180,38 +180,11 @@ jobs:
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
|
||||
const allModes = ['dedicated', 'shared_v1', 'shared_v2'];
|
||||
const allDatabases = ['MongoDB'];
|
||||
const allModes = ['shared_v1', 'shared_v2'];
|
||||
|
||||
const defaultDatabases = ['MongoDB'];
|
||||
const defaultModes = ['dedicated'];
|
||||
|
||||
const pr = context.payload.pull_request;
|
||||
if (!pr) {
|
||||
core.setOutput('databases', JSON.stringify(allDatabases));
|
||||
core.setOutput('modes', JSON.stringify(allModes));
|
||||
return;
|
||||
}
|
||||
|
||||
const getContent = (ref) => github.rest.repos.getContent({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
path: 'composer.lock',
|
||||
ref,
|
||||
});
|
||||
|
||||
const getDbVersion = (lock) => lock.packages?.find(p => p.name === 'utopia-php/database')?.version;
|
||||
|
||||
const [{ data: base }, { data: head }] = await Promise.all([
|
||||
getContent(pr.base.sha),
|
||||
getContent(pr.head.sha),
|
||||
]);
|
||||
|
||||
const decode = (content) => JSON.parse(Buffer.from(content, 'base64').toString());
|
||||
const databaseChanged = getDbVersion(decode(base.content)) !== getDbVersion(decode(head.content));
|
||||
|
||||
core.setOutput('databases', JSON.stringify(databaseChanged ? allDatabases : defaultDatabases));
|
||||
core.setOutput('modes', JSON.stringify(databaseChanged ? allModes : defaultModes));
|
||||
core.setOutput('databases', JSON.stringify(allDatabases));
|
||||
core.setOutput('modes', JSON.stringify(allModes));
|
||||
|
||||
build:
|
||||
name: Build
|
||||
@@ -451,6 +424,10 @@ jobs:
|
||||
_APP_BROWSER_HOST: http://invalid-browser/v1
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -525,6 +502,10 @@ jobs:
|
||||
_APP_OPTIONS_ABUSE: enabled
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -582,6 +563,10 @@ jobs:
|
||||
env:
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
|
||||
@@ -1,107 +1,120 @@
|
||||
# AGENTS.md
|
||||
# Appwrite
|
||||
|
||||
Appwrite is an end-to-end backend server for web, mobile, native, and backend apps. This guide provides context and instructions for AI coding agents working on the Appwrite codebase.
|
||||
Self-hosted Backend-as-a-Service platform. Hybrid monolithic-microservice architecture built with PHP 8.3+ on Swoole, delivered as Docker containers.
|
||||
|
||||
## Project Overview
|
||||
## Commands
|
||||
|
||||
Appwrite is a self-hosted Backend-as-a-Service (BaaS) platform that provides developers with a set of APIs and tools to build secure, scalable applications. The project uses a hybrid monolithic-microservice architecture built with PHP, running on Swoole for high performance.
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `docker compose up -d --force-recreate --build` | Build and start all services |
|
||||
| `docker compose exec appwrite test tests/e2e/Services/[Service]` | Run E2E tests for a service |
|
||||
| `docker compose exec appwrite test tests/e2e/Services/[Service] --filter=[Method]` | Run a single test method |
|
||||
| `docker compose exec appwrite test tests/unit/` | Run unit tests |
|
||||
| `composer format` | Auto-format code (Pint, PSR-12) |
|
||||
| `composer format <file>` | Format a specific file |
|
||||
| `composer lint <file>` | Check formatting of a file |
|
||||
| `composer analyze` | Static analysis (PHPStan level 3) |
|
||||
| `composer check` | Same as `analyze` |
|
||||
|
||||
**Key Technologies:**
|
||||
- **Backend:** PHP 8.3+, Swoole
|
||||
- **Libraries:** Utopia PHP
|
||||
- **Database:** MariaDB, Redis
|
||||
- **Cache:** Redis
|
||||
- **Queue:** Redis
|
||||
- **Containers:** Docker
|
||||
## Stack
|
||||
|
||||
## Development Commands
|
||||
- PHP 8.3+, Swoole 6.x (async runtime, replaces PHP-FPM)
|
||||
- Utopia PHP framework (HTTP routing, CLI, DI, queue)
|
||||
- MongoDB (default), MariaDB, MySQL, PostgreSQL (adapters via utopia-php/database)
|
||||
- Redis (cache, queue, pub/sub)
|
||||
- Docker + Traefik (reverse proxy)
|
||||
- PHPUnit 12, Pint (PSR-12), PHPStan level 3
|
||||
|
||||
```bash
|
||||
# Run Appwrite
|
||||
docker compose up -d --force-recreate --build
|
||||
## Project layout
|
||||
|
||||
# Run specific test
|
||||
docker compose exec appwrite test /usr/src/code/tests/e2e/Services/[ServiceName] --filter=[FunctionName]
|
||||
- **src/Appwrite/Platform/Modules/** -- feature modules (Account, Avatars, Compute, Console, Databases, Functions, Health, Project, Projects, Proxy, Sites, Storage, Teams, Tokens, VCS, Webhooks)
|
||||
- **src/Appwrite/Platform/Workers/** -- background job workers
|
||||
- **src/Appwrite/Platform/Tasks/** -- CLI tasks
|
||||
- **app/init.php** -- bootstrap (registers services, resources, listeners)
|
||||
- **app/init/** -- configs, constants, locales, models, registers, resources, span, database filters/formats
|
||||
- **bin/** -- CLI entry points: `worker-*` (14 workers), `schedule-*`, `queue-*`, plus `doctor`, `install`, `migrate`, `realtime`, `upgrade`, `ssl`, `vars`, `maintenance`, `interval`, `specs`, `sdks`, etc.
|
||||
- **tests/e2e/** -- end-to-end tests per service
|
||||
- **tests/unit/** -- unit tests
|
||||
- **public/** -- static assets and generated SDKs
|
||||
|
||||
# Format code
|
||||
composer format
|
||||
## Module structure
|
||||
|
||||
Each module under `src/Appwrite/Platform/Modules/{Name}/` contains:
|
||||
|
||||
```
|
||||
Module.php -- registers all services for the module
|
||||
Services/Http.php -- registers HTTP endpoints
|
||||
Services/Workers.php -- registers background workers
|
||||
Services/Tasks.php -- registers CLI tasks
|
||||
Http/{Service}/ -- endpoint actions (Create.php, Get.php, Update.php, Delete.php, XList.php)
|
||||
Workers/ -- worker implementations
|
||||
Tasks/ -- CLI task implementations
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
HTTP endpoint nesting reflects the URL path. Sub-resources get subdirectories. For example, within the Functions module:
|
||||
`Http/Deployments/Template/Create.php` -> `POST /v1/functions/:functionId/deployments/template`
|
||||
|
||||
- Follow [PSR-12](https://www.php-fig.org/psr/psr-12/) coding standard
|
||||
- Use PSR-4 autoloading
|
||||
- Strict type declarations where applicable
|
||||
- Comprehensive PHPDoc comments
|
||||
File names in Http directories must only be `Get.php`, `Create.php`, `Update.php`, `Delete.php`, or `XList.php`. For non-CRUD operations, model the endpoint as a property update. For example, updating a team membership status lives at `Teams/Http/Memberships/Status/Update.php` (`PATCH /v1/teams/:teamId/memberships/:membershipId/status`).
|
||||
|
||||
### Naming Conventions
|
||||
Register new modules in `src/Appwrite/Platform/Appwrite.php`. Detailed module guide: `src/Appwrite/Platform/AGENTS.md`.
|
||||
|
||||
#### `resourceType` Naming Rule
|
||||
## Action pattern (HTTP endpoints)
|
||||
|
||||
When a collection has a combination of `resourceType`, `resourceId`, and/or `resourceInternalId`, the value of `resourceType` MUST always be **plural** - for example: `functions`, `sites`, `deployments`.
|
||||
|
||||
Examples:
|
||||
```php
|
||||
'resourceType' => 'functions'
|
||||
'resourceType' => 'sites'
|
||||
'resourceType' => 'deployments'
|
||||
class Create extends Action
|
||||
{
|
||||
public static function getName(): string { return 'createTeam'; }
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/teams')
|
||||
->desc('Create team')
|
||||
->groups(['api', 'teams'])
|
||||
->label('event', 'teams.[teamId].create')
|
||||
->label('scope', 'teams.write')
|
||||
->param('teamId', '', new CustomId(), 'Team ID.')
|
||||
->param('name', null, new Text(128), 'Team name.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $teamId,
|
||||
string $name,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Event $queueForEvents,
|
||||
): void {
|
||||
// implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Patterns
|
||||
Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `$user`, `$project`, `$queueForEvents`, `$queueForMails`, `$queueForDeletes`.
|
||||
|
||||
### Document Update Optimization
|
||||
## Conventions
|
||||
|
||||
When updating documents, always pass only the changed attributes as a sparse `Document` rather than the full document. This is more efficient because `updateDocument()` internally performs `array_merge($old, $new)`.
|
||||
- PSR-12 formatting enforced by Pint. PSR-4 autoloading.
|
||||
- `resourceType` values are always **plural**: `'functions'`, `'sites'`, `'deployments'`.
|
||||
- When updating documents, pass only changed attributes as a sparse Document:
|
||||
```php
|
||||
// correct
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'name' => $name,
|
||||
]));
|
||||
// incorrect -- passing full document is inefficient
|
||||
$user->setAttribute('name', $name);
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
```
|
||||
Exceptions: migrations, `array_merge()` with `getArrayCopy()`, updates where nearly all attributes change, complex nested relationship logic requiring full document state.
|
||||
- Avoid introducing dependencies outside the `utopia-php` ecosystem.
|
||||
- Never hardcode credentials -- use environment variables.
|
||||
- Code changes may require container restart. No central log location -- check relevant containers.
|
||||
|
||||
**Correct Pattern:**
|
||||
```php
|
||||
// Good: Pass only changed attributes directly
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
]));
|
||||
```
|
||||
## Cross-repo context
|
||||
|
||||
**Incorrect Pattern:**
|
||||
```php
|
||||
$user->setAttribute('name', $name);
|
||||
$user->setAttribute('email', $email);
|
||||
|
||||
// Bad: Passing full document is inefficient
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
```
|
||||
|
||||
**Exceptions:**
|
||||
- Migration files (need full document updates by design)
|
||||
- Cases already using `array_merge()` with `getArrayCopy()`
|
||||
- Updates where almost all attributes of the document change at once (sparse update provides little benefit compared to passing the full document)
|
||||
- Complex nested relationship logic where full document state is required
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Critical Security Practices
|
||||
|
||||
- **Never hardcode credentials** - Use environment variables
|
||||
- **Rate limiting** - Respect abuse prevention mechanisms
|
||||
|
||||
## Dependencies
|
||||
|
||||
Avoid introducing new dependencies other than utopia-php.
|
||||
|
||||
## Adding new endpoints
|
||||
|
||||
When adding new endpoints, make sure to use modules and follow its patterns. Find instruction in [Modules AGENTS.md](src/Appwrite/Platform/AGENTS.md) file.
|
||||
|
||||
## Pull Request Guidelines
|
||||
### Before Submitting
|
||||
|
||||
- Run `composer format`
|
||||
- Update documentation if adding features
|
||||
- Add/update tests for your changes
|
||||
- Check that Docker build succeeds
|
||||
`docs/specs/authentication.drawio.svg`
|
||||
|
||||
## Known Issues and Gotchas
|
||||
|
||||
- **Hot Reload:** Code changes require container restart in some cases
|
||||
- **Logging:** There is no central place for logs, so when debugging, ensure to check all possibly relevant containers
|
||||
Appwrite is the base server for `appwrite/cloud`. Changes to the Action pattern, module structure, DI system, or response models affect cloud. The `feat-dedicated-db` feature spans cloud, edge, and console.
|
||||
|
||||
@@ -64,7 +64,7 @@ return [
|
||||
[
|
||||
'$id' => ID::custom('database'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'size' => 128,
|
||||
'size' => 2000,
|
||||
'required' => false,
|
||||
'signed' => true,
|
||||
'array' => false,
|
||||
|
||||
@@ -57,21 +57,21 @@
|
||||
"emails.recovery.thanks": "Thanks,",
|
||||
"emails.recovery.buttonText": "Reset password",
|
||||
"emails.recovery.signature": "{{project}} team",
|
||||
"emails.csvExport.success.subject": "Your CSV export is ready",
|
||||
"emails.csvExport.success.preview": "Your data export has been completed successfully.",
|
||||
"emails.csvExport.success.hello": "Hello {{user}},",
|
||||
"emails.csvExport.success.body": "Your CSV export is ready to download. Click the button below to download your data export.",
|
||||
"emails.csvExport.success.footer": "This download link will expire in 1 hour.",
|
||||
"emails.csvExport.success.thanks": "Thanks,",
|
||||
"emails.csvExport.success.buttonText": "Download CSV",
|
||||
"emails.csvExport.success.signature": "Appwrite team",
|
||||
"emails.csvExport.failure.subject": "Your CSV export failed - file too large",
|
||||
"emails.csvExport.failure.preview": "Your data export failed because the file size exceeds your plan limit.",
|
||||
"emails.csvExport.failure.hello": "Hello {{user}},",
|
||||
"emails.csvExport.failure.body": "Your CSV export could not be completed because the export file size ({{size}}MB) exceeds your plan limit. Please consider upgrading your plan or exporting a smaller dataset.",
|
||||
"emails.csvExport.failure.footer": "If you have any questions, please contact our support team.",
|
||||
"emails.csvExport.failure.thanks": "Thanks,",
|
||||
"emails.csvExport.failure.signature": "{{project}} team",
|
||||
"emails.dataExport.success.subject": "Your {{type}} export is ready",
|
||||
"emails.dataExport.success.preview": "Your data export has been completed successfully.",
|
||||
"emails.dataExport.success.hello": "Hello {{user}},",
|
||||
"emails.dataExport.success.body": "Your {{type}} export is ready to download. Click the button below to download your data export.",
|
||||
"emails.dataExport.success.footer": "This download link will expire in 1 hour.",
|
||||
"emails.dataExport.success.thanks": "Thanks,",
|
||||
"emails.dataExport.success.buttonText": "Download {{type}}",
|
||||
"emails.dataExport.success.signature": "Appwrite team",
|
||||
"emails.dataExport.failure.subject": "Your {{type}} export failed - file too large",
|
||||
"emails.dataExport.failure.preview": "Your data export failed because the file size exceeds your plan limit.",
|
||||
"emails.dataExport.failure.hello": "Hello {{user}},",
|
||||
"emails.dataExport.failure.body": "Your {{type}} export could not be completed because the export file size ({{size}}MB) exceeds your plan limit. Please consider upgrading your plan or exporting a smaller dataset.",
|
||||
"emails.dataExport.failure.footer": "If you have any questions, please contact our support team.",
|
||||
"emails.dataExport.failure.thanks": "Thanks,",
|
||||
"emails.dataExport.failure.signature": "{{project}} team",
|
||||
"emails.invitation.subject": "Invitation to {{team}} Team at {{project}}",
|
||||
"emails.invitation.preview": "{{owner}} invited you to join {{team}} at {{project}}",
|
||||
"emails.invitation.hello": "Hello {{user}},",
|
||||
|
||||
+35
-24
@@ -250,26 +250,16 @@ return [
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'markdown',
|
||||
'name' => 'Markdown',
|
||||
'version' => '0.3.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-md.git',
|
||||
'package' => 'https://www.npmjs.com/package/@appwrite.io/docs',
|
||||
'enabled' => true,
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'family' => APP_SDK_PLATFORM_CONSOLE,
|
||||
'prism' => 'markdown',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/console-md'),
|
||||
'gitUrl' => 'git@github.com:appwrite/sdk-for-md.git',
|
||||
'gitRepoName' => 'sdk-for-md',
|
||||
'gitUserName' => 'appwrite',
|
||||
'gitBranch' => 'dev',
|
||||
'repoBranch' => 'main',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/md/CHANGELOG.md'),
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
APP_SDK_PLATFORM_STATIC => [
|
||||
'key' => APP_SDK_PLATFORM_STATIC,
|
||||
'name' => 'Static',
|
||||
'description' => 'SDK artifacts for Appwrite integrations that do not require a generated platform API specification.',
|
||||
'enabled' => true,
|
||||
'beta' => false,
|
||||
'sdks' => [
|
||||
[
|
||||
'key' => 'agent-skills',
|
||||
'name' => 'AgentSkills',
|
||||
@@ -279,9 +269,10 @@ return [
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'family' => APP_SDK_PLATFORM_CONSOLE,
|
||||
'spec' => 'static',
|
||||
'family' => APP_SDK_PLATFORM_STATIC,
|
||||
'prism' => 'agent-skills',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/console-agent-skills'),
|
||||
'source' => \realpath(__DIR__ . '/../sdks/static-agent-skills'),
|
||||
'gitUrl' => 'git@github.com:appwrite/agent-skills.git',
|
||||
'gitRepoName' => 'agent-skills',
|
||||
'gitUserName' => 'appwrite',
|
||||
@@ -298,9 +289,10 @@ return [
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'family' => APP_SDK_PLATFORM_CONSOLE,
|
||||
'spec' => 'static',
|
||||
'family' => APP_SDK_PLATFORM_STATIC,
|
||||
'prism' => 'cursor-plugin',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/console-cursor-plugin'),
|
||||
'source' => \realpath(__DIR__ . '/../sdks/static-cursor-plugin'),
|
||||
'gitUrl' => 'git@github.com:appwrite/cursor-plugin.git',
|
||||
'gitRepoName' => 'cursor-plugin',
|
||||
'gitUserName' => 'appwrite',
|
||||
@@ -494,6 +486,25 @@ return [
|
||||
'gitBranch' => 'dev',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/swift/CHANGELOG.md'),
|
||||
],
|
||||
[
|
||||
'key' => 'rust',
|
||||
'name' => 'Rust',
|
||||
'version' => '0.1.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-rust',
|
||||
'package' => 'https://crates.io/crates/appwrite',
|
||||
'enabled' => true,
|
||||
'beta' => true,
|
||||
'dev' => true,
|
||||
'hidden' => false,
|
||||
'family' => APP_SDK_PLATFORM_SERVER,
|
||||
'prism' => 'rust',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/server-rust'),
|
||||
'gitUrl' => 'git@github.com:appwrite/sdk-for-rust.git',
|
||||
'gitRepoName' => 'sdk-for-rust',
|
||||
'gitUserName' => 'appwrite',
|
||||
'gitBranch' => 'dev',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/rust/CHANGELOG.md'),
|
||||
],
|
||||
[
|
||||
'key' => 'graphql',
|
||||
'name' => 'GraphQL',
|
||||
|
||||
@@ -28,12 +28,18 @@ use Utopia\Validator\Text;
|
||||
Http::init()
|
||||
->groups(['graphql'])
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('authorization')
|
||||
->action(function (Document $project, Authorization $authorization) {
|
||||
->action(function (Document $project, User $user, Request $request, Response $response, Authorization $authorization) {
|
||||
$response->setUser($user);
|
||||
$request->setUser($user);
|
||||
|
||||
if (
|
||||
array_key_exists('graphql', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['graphql']
|
||||
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ use Utopia\Migration\Resource;
|
||||
use Utopia\Migration\Sources\Appwrite;
|
||||
use Utopia\Migration\Sources\CSV;
|
||||
use Utopia\Migration\Sources\Firebase;
|
||||
use Utopia\Migration\Sources\JSON;
|
||||
use Utopia\Migration\Sources\NHost;
|
||||
use Utopia\Migration\Sources\Supabase;
|
||||
use Utopia\Migration\Transfer;
|
||||
@@ -53,6 +54,15 @@ function getDatabaseTransferResourceServices(string $databaseType)
|
||||
};
|
||||
}
|
||||
|
||||
function getDatabaseResourceType(string $databaseType): string
|
||||
{
|
||||
return match($databaseType) {
|
||||
DATABASE_TYPE_VECTORSDB => Resource::TYPE_DATABASE_VECTORSDB,
|
||||
DATABASE_TYPE_DOCUMENTSDB => Resource::TYPE_DATABASE_DOCUMENTSDB,
|
||||
default => Resource::TYPE_DATABASE,
|
||||
};
|
||||
}
|
||||
|
||||
Http::post('/v1/migrations/appwrite')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Create Appwrite migration')
|
||||
@@ -447,6 +457,7 @@ Http::post('/v1/migrations/csv/imports')
|
||||
}
|
||||
$fileSize = $deviceForMigrations->getFileSize($newPath);
|
||||
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
|
||||
$resourceType = getDatabaseResourceType($databaseType);
|
||||
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => $migrationId,
|
||||
@@ -456,7 +467,7 @@ Http::post('/v1/migrations/csv/imports')
|
||||
'destination' => Appwrite::getName(),
|
||||
'resources' => $resources,
|
||||
'resourceId' => $resourceId,
|
||||
'resourceType' => Resource::TYPE_DATABASE,
|
||||
'resourceType' => $resourceType,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
@@ -565,16 +576,6 @@ Http::post('/v1/migrations/csv/exports')
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$validator = new Documents(
|
||||
attributes: $collection->getAttribute('attributes', []),
|
||||
indexes: $collection->getAttribute('indexes', []),
|
||||
idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(),
|
||||
);
|
||||
|
||||
if (!$validator->isValid($parsedQueries)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
// getting databasetype
|
||||
$resources = explode(':', $resourceId);
|
||||
$databaseId = $resources[0];
|
||||
@@ -583,7 +584,23 @@ Http::post('/v1/migrations/csv/exports')
|
||||
if (!in_array($databaseType, CSV_ALLOWED_DATABASE_TYPES)) {
|
||||
throw new Exception(Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED, 'Database type not supported for csv');
|
||||
}
|
||||
|
||||
// Schemaless databases (DocumentsDB, VectorsDB) allow queries on dynamic fields
|
||||
$isSchemaless = in_array($databaseType, [DATABASE_TYPE_DOCUMENTSDB, DATABASE_TYPE_VECTORSDB]);
|
||||
|
||||
$validator = new Documents(
|
||||
attributes: $collection->getAttribute('attributes', []),
|
||||
indexes: $collection->getAttribute('indexes', []),
|
||||
idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(),
|
||||
supportForAttributes: !$isSchemaless,
|
||||
);
|
||||
|
||||
if (!$validator->isValid($parsedQueries)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
|
||||
$resourceType = getDatabaseResourceType($databaseType);
|
||||
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
@@ -593,7 +610,7 @@ Http::post('/v1/migrations/csv/exports')
|
||||
'destination' => CSV::getName(),
|
||||
'resources' => $resources,
|
||||
'resourceId' => $resourceId,
|
||||
'resourceType' => Resource::TYPE_DATABASE,
|
||||
'resourceType' => $resourceType,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
@@ -624,6 +641,291 @@ Http::post('/v1/migrations/csv/exports')
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
Http::post('/v1/migrations/json/imports')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Import documents from a JSON')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.[migrationId].create')
|
||||
->label('audits.event', 'migration.create')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'migrations',
|
||||
group: null,
|
||||
name: 'createJSONImport',
|
||||
description: '/docs/references/migrations/migration-json-import.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_ACCEPTED,
|
||||
model: Response::MODEL_MIGRATION,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).')
|
||||
->param('fileId', '', new UID(), 'File ID.')
|
||||
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.')
|
||||
->param('internalFile', false, new Boolean(), 'Is the file stored in an internal bucket?', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('deviceForFiles')
|
||||
->inject('deviceForMigrations')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (
|
||||
string $bucketId,
|
||||
string $fileId,
|
||||
string $resourceId,
|
||||
bool $internalFile,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
array $platform,
|
||||
Device $deviceForFiles,
|
||||
Device $deviceForMigrations,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
) {
|
||||
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
|
||||
if ($internalFile) {
|
||||
return $dbForPlatform->getDocument('buckets', 'default');
|
||||
}
|
||||
return $dbForProject->getDocument('buckets', $bucketId);
|
||||
});
|
||||
|
||||
if ($bucket->isEmpty()) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
$file = $authorization->skip(fn () => $internalFile ? $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $fileId) : $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
|
||||
if ($file->isEmpty()) {
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$path = $file->getAttribute('path', '');
|
||||
if (!$deviceForFiles->exists($path)) {
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path);
|
||||
}
|
||||
|
||||
// No encryption or compression on files above 20MB.
|
||||
$hasEncryption = !empty($file->getAttribute('openSSLCipher'));
|
||||
$compression = $file->getAttribute('algorithm', Compression::NONE);
|
||||
$hasCompression = $compression !== Compression::NONE;
|
||||
|
||||
$migrationId = ID::unique();
|
||||
$newPath = $deviceForMigrations->getPath($migrationId . '_' . $fileId . '.json');
|
||||
|
||||
if ($hasEncryption || $hasCompression) {
|
||||
$source = $deviceForFiles->read($path);
|
||||
|
||||
if ($hasEncryption) {
|
||||
$source = OpenSSL::decrypt(
|
||||
$source,
|
||||
$file->getAttribute('openSSLCipher'),
|
||||
System::getEnv('_APP_OPENSSL_KEY_V' . $file->getAttribute('openSSLVersion')),
|
||||
0,
|
||||
hex2bin($file->getAttribute('openSSLIV')),
|
||||
hex2bin($file->getAttribute('openSSLTag'))
|
||||
);
|
||||
}
|
||||
|
||||
if ($hasCompression) {
|
||||
switch ($compression) {
|
||||
case Compression::ZSTD:
|
||||
$source = (new Zstd())->decompress($source);
|
||||
break;
|
||||
case Compression::GZIP:
|
||||
$source = (new GZIP())->decompress($source);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Manual write after decryption and/or decompression
|
||||
if (!$deviceForMigrations->write($newPath, $source, 'application/json')) {
|
||||
throw new \Exception('Unable to copy file');
|
||||
}
|
||||
} elseif (!$deviceForFiles->transfer($path, $newPath, $deviceForMigrations)) {
|
||||
throw new \Exception('Unable to copy file');
|
||||
}
|
||||
|
||||
$fileSize = $deviceForMigrations->getFileSize($newPath);
|
||||
|
||||
[$databaseId] = \explode(':', $resourceId, 2);
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
}
|
||||
$databaseType = $database->getAttribute('type');
|
||||
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
|
||||
$resourceType = getDatabaseResourceType($databaseType);
|
||||
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => $migrationId,
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => JSON::getName(),
|
||||
'destination' => Appwrite::getName(),
|
||||
'resources' => $resources,
|
||||
'resourceId' => $resourceId,
|
||||
'resourceType' => $resourceType,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
'options' => [
|
||||
'path' => $newPath,
|
||||
'size' => $fileSize,
|
||||
],
|
||||
]));
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->trigger();
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
Http::post('/v1/migrations/json/exports')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Export documents to JSON')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.[migrationId].create')
|
||||
->label('audits.event', 'migration.create')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'migrations',
|
||||
group: null,
|
||||
name: 'createJSONExport',
|
||||
description: '/docs/references/migrations/migration-json-export.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_ACCEPTED,
|
||||
model: Response::MODEL_MIGRATION,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database to export.')
|
||||
->param('filename', '', new Text(255), 'The name of the file to be created for the export, excluding the .json extension.')
|
||||
->param('columns', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of attributes to export. If empty, all attributes will be exported. You can use the `*` wildcard to export all attributes from the collection.', true)
|
||||
->param('queries', [], new ArrayList(new Text(0)), 'Array of query strings generated using the Query class provided by the SDK to filter documents to export. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
|
||||
->param('notify', true, new Boolean(), 'Set to true to receive an email when the export is complete. Default is true.', true)
|
||||
->inject('user')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (
|
||||
string $resourceId,
|
||||
string $filename,
|
||||
array $columns,
|
||||
array $queries,
|
||||
bool $notify,
|
||||
Document $user,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
array $platform,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
) {
|
||||
try {
|
||||
$parsedQueries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$bucket = $authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'default'));
|
||||
if ($bucket->isEmpty()) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
[$databaseId, $collectionId] = \explode(':', $resourceId, 2);
|
||||
if (empty($databaseId)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
}
|
||||
if (empty($collectionId)) {
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$databaseType = $database->getAttribute('type');
|
||||
|
||||
// Schemaless databases (DocumentsDB, VectorsDB) allow queries on dynamic fields
|
||||
$isSchemaless = in_array($databaseType, [DATABASE_TYPE_DOCUMENTSDB, DATABASE_TYPE_VECTORSDB]);
|
||||
|
||||
$validator = new Documents(
|
||||
attributes: $collection->getAttribute('attributes', []),
|
||||
indexes: $collection->getAttribute('indexes', []),
|
||||
idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(),
|
||||
supportForAttributes: !$isSchemaless,
|
||||
);
|
||||
|
||||
if (!$validator->isValid($parsedQueries)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
|
||||
$resourceType = getDatabaseResourceType($databaseType);
|
||||
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => Appwrite::getName(),
|
||||
'destination' => JSON::getName(),
|
||||
'resources' => $resources,
|
||||
'resourceId' => $resourceId,
|
||||
'resourceType' => $resourceType,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
'options' => [
|
||||
'bucketId' => 'default', // Always use internal bucket
|
||||
'filename' => $filename,
|
||||
'columns' => $columns,
|
||||
'queries' => $queries,
|
||||
'notify' => $notify,
|
||||
'userInternalId' => $user->getSequence(),
|
||||
],
|
||||
]));
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->trigger();
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
Http::get('/v1/migrations')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('List migrations')
|
||||
|
||||
@@ -1270,7 +1270,16 @@ Http::error()
|
||||
* If not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php
|
||||
*/
|
||||
if (!$publish && $project->getId() !== 'console') {
|
||||
if (!DBUser::isPrivileged($authorization->getRoles())) {
|
||||
$errorUser = new DBUser();
|
||||
try {
|
||||
$resolvedUser = $utopia->getResource('user');
|
||||
if ($resolvedUser instanceof DBUser) {
|
||||
$errorUser = $resolvedUser;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// User resource may not be available in error context
|
||||
}
|
||||
if (!$errorUser->isPrivileged($authorization->getRoles())) {
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
|
||||
@@ -96,7 +96,7 @@ Http::init()
|
||||
->inject('team')
|
||||
->inject('apiKey')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
/**
|
||||
@@ -419,7 +419,7 @@ Http::init()
|
||||
if (
|
||||
array_key_exists($namespace, $project->getAttribute('services', []))
|
||||
&& ! $project->getAttribute('services', [])[$namespace]
|
||||
&& ! (User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
|
||||
}
|
||||
@@ -483,7 +483,10 @@ Http::init()
|
||||
->inject('telemetry')
|
||||
->inject('platform')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, 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) {
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, 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) {
|
||||
|
||||
$response->setUser($user);
|
||||
$request->setUser($user);
|
||||
|
||||
$route = $utopia->getRoute();
|
||||
$path = $route->getMatchedPath();
|
||||
@@ -496,7 +499,7 @@ Http::init()
|
||||
if (
|
||||
array_key_exists('rest', $project->getAttribute('apis', []))
|
||||
&& ! $project->getAttribute('apis', [])['rest']
|
||||
&& ! (User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
@@ -528,8 +531,8 @@ Http::init()
|
||||
$closestLimit = null;
|
||||
|
||||
$roles = $authorization->getRoles();
|
||||
$isPrivilegedUser = User::isPrivileged($roles);
|
||||
$isAppUser = User::isApp($roles);
|
||||
$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
|
||||
@@ -611,7 +614,7 @@ Http::init()
|
||||
if ($useCache) {
|
||||
$route = $utopia->match($request);
|
||||
$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($authorization->getRoles());
|
||||
|
||||
$key = $request->cacheIdentifier();
|
||||
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
@@ -630,7 +633,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($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (! $bucket->getAttribute('enabled') && ! $isAppUser && ! $isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -663,7 +666,7 @@ Http::init()
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
// Do not update transformedAt if it's a console user
|
||||
if (! User::isPrivileged($authorization->getRoles())) {
|
||||
if (! $user->isPrivileged($authorization->getRoles())) {
|
||||
$transformedAt = $file->getAttribute('transformedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
|
||||
$file->setAttribute('transformedAt', DateTime::now());
|
||||
@@ -697,7 +700,7 @@ Http::init()
|
||||
->groups(['session'])
|
||||
->inject('user')
|
||||
->inject('request')
|
||||
->action(function (Document $user, Request $request) {
|
||||
->action(function (User $user, Request $request) {
|
||||
if (\str_contains($request->getURI(), 'oauth2')) {
|
||||
return;
|
||||
}
|
||||
@@ -984,7 +987,7 @@ Http::shutdown()
|
||||
}
|
||||
|
||||
if ($project->getId() !== 'console') {
|
||||
if (! User::isPrivileged($authorization->getRoles())) {
|
||||
if (! $user->isPrivileged($authorization->getRoles())) {
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
|
||||
@@ -36,8 +36,9 @@ Http::init()
|
||||
->inject('request')
|
||||
->inject('project')
|
||||
->inject('geodb')
|
||||
->inject('user')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Document $project, Reader $geodb, Authorization $authorization) {
|
||||
->action(function (Http $utopia, Request $request, Document $project, Reader $geodb, User $user, Authorization $authorization) {
|
||||
$denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', '');
|
||||
if (!empty($denylist && $project->getId() === 'console')) {
|
||||
$countries = explode(',', $denylist);
|
||||
@@ -50,8 +51,8 @@ Http::init()
|
||||
|
||||
$route = $utopia->match($request);
|
||||
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAppUser = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
$isAppUser = $user->isApp($authorization->getRoles());
|
||||
|
||||
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
|
||||
return;
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ function dispatch(Server $server, int $fd, int $type, $data = null): int
|
||||
$lines = explode("\n", $data, 3);
|
||||
$request = $lines[0];
|
||||
if (count($lines) > 1) {
|
||||
$domain = trim(explode('Host: ', $lines[1])[1]);
|
||||
$domain = trim(explode('Host: ', $lines[1])[1] ?? '');
|
||||
}
|
||||
|
||||
// Sync executions are considered risky
|
||||
|
||||
@@ -97,6 +97,7 @@ const APP_COMPUTE_DEPLOYMENT_MAX_RETENTION = 100 * 365; // 100 years
|
||||
const APP_SDK_PLATFORM_SERVER = 'server';
|
||||
const APP_SDK_PLATFORM_CLIENT = 'client';
|
||||
const APP_SDK_PLATFORM_CONSOLE = 'console';
|
||||
const APP_SDK_PLATFORM_STATIC = 'static';
|
||||
const APP_VCS_GITHUB_USERNAME = 'Appwrite';
|
||||
const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io';
|
||||
const APP_VCS_GITHUB_URL = 'https://github.com/TeamAppwrite';
|
||||
|
||||
+30
-5
@@ -432,8 +432,10 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
$jwtUserId = $payload['userId'] ?? '';
|
||||
if (! empty($jwtUserId)) {
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
/** @var User $user */
|
||||
$user = $dbForPlatform->getDocument('users', $jwtUserId);
|
||||
} else {
|
||||
/** @var User $user */
|
||||
$user = $dbForProject->getDocument('users', $jwtUserId);
|
||||
}
|
||||
}
|
||||
@@ -453,6 +455,7 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
|
||||
}
|
||||
|
||||
/** @var User $accountKeyUser */
|
||||
$accountKeyUser = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId));
|
||||
if (! $accountKeyUser->isEmpty()) {
|
||||
$key = $accountKeyUser->find(
|
||||
@@ -885,8 +888,9 @@ Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authori
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
|
||||
$initializedPools = [];
|
||||
|
||||
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database {
|
||||
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization, &$initializedPools): Database {
|
||||
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
|
||||
$databaseType = $database->getAttribute('type', '');
|
||||
|
||||
@@ -904,11 +908,12 @@ Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Docume
|
||||
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
|
||||
}
|
||||
|
||||
$pool = $pools->get($databaseDSN->getHost());
|
||||
$databaseHost = $databaseDSN->getHost();
|
||||
$pool = $pools->get($databaseHost);
|
||||
|
||||
$adapter = new DatabasePool($pool);
|
||||
$database = new Database($adapter, $cache);
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
@@ -919,10 +924,30 @@ Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Docume
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
// inside pools authorization needs to be set first
|
||||
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
|
||||
// When the database uses a separate pool (e.g. vectorsdb on PostgreSQL),
|
||||
// always use dedicated mode with namespace isolation. Shared tables mode
|
||||
// can't be used across different engines (e.g. MongoDB UUID tenants are
|
||||
// incompatible with PostgreSQL's integer _tenant column).
|
||||
if ($databaseHost !== $dsn->getHost()) {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
|
||||
$poolKey = $databaseHost . ':' . $database->getNamespace();
|
||||
if (!isset($initializedPools[$poolKey])) {
|
||||
try {
|
||||
$database->create();
|
||||
} catch (\Utopia\Database\Exception\Duplicate) {
|
||||
// Schema already exists
|
||||
}
|
||||
$initializedPools[$poolKey] = true;
|
||||
}
|
||||
} elseif (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int)$project->getSequence())
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
|
||||
+6
-6
@@ -518,7 +518,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
$project = $consoleDatabase->getAuthorization()->skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
|
||||
$database = getProjectDB($project);
|
||||
|
||||
/** @var Appwrite\Utopia\Database\Documents\User $user */
|
||||
/** @var User $user */
|
||||
$user = $database->getDocument('users', $userId);
|
||||
|
||||
$roles = $user->getRoles($database->getAuthorization());
|
||||
@@ -642,10 +642,14 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing or unknown project ID');
|
||||
}
|
||||
|
||||
$timelimit = $app->getResource('timelimit');
|
||||
$user = $app->getResource('user'); /** @var User $user */
|
||||
$logUser = $user;
|
||||
|
||||
if (
|
||||
array_key_exists('realtime', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['realtime']
|
||||
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
@@ -656,10 +660,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Project is not accessible in this region. Please make sure you are using the correct endpoint');
|
||||
}
|
||||
|
||||
$timelimit = $app->getResource('timelimit');
|
||||
$user = $app->getResource('user'); /** @var User $user */
|
||||
$logUser = $user;
|
||||
|
||||
/*
|
||||
* Abuse Check
|
||||
*
|
||||
|
||||
@@ -13,7 +13,7 @@ $enabledDatabases = $enabledDatabases ?? ['mongodb', 'mariadb', 'postgresql'];
|
||||
$isLocalInstall = $isLocalInstall ?? false;
|
||||
|
||||
|
||||
$cardStep = min(4, $step);
|
||||
$cardStep = ($step === 5) ? 4 : $step;
|
||||
$stepFile = __DIR__ . "/installer/templates/steps/step-{$cardStep}.phtml";
|
||||
if (!is_file($stepFile)) {
|
||||
$stepFile = __DIR__ . "/installer/templates/steps/step-1.phtml";
|
||||
|
||||
@@ -478,6 +478,10 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.installer-page[data-upgrade='true'] .installer-step {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.action-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1808,3 +1812,92 @@ body {
|
||||
gap: var(--gap-s);
|
||||
}
|
||||
}
|
||||
|
||||
.migration-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--gap-l);
|
||||
padding: var(--space-6);
|
||||
background: var(--bgcolor-neutral-default);
|
||||
border-radius: var(--border-radius-m);
|
||||
outline: var(--border-width-s) solid var(--border-neutral);
|
||||
outline-offset: calc(var(--border-width-s) * -1);
|
||||
cursor: pointer;
|
||||
transition: outline-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.migration-option:hover {
|
||||
outline-color: var(--border-neutral-stronger);
|
||||
}
|
||||
|
||||
.migration-option-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.migration-switch {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.migration-switch-track {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
background: var(--bgcolor-neutral-invert-weaker);
|
||||
transition: background 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.migration-switch-thumb {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--bgcolor-neutral-primary);
|
||||
transition: transform 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
#run-migration:checked ~ .migration-switch-track {
|
||||
background: var(--bgcolor-neutral-invert-weak);
|
||||
}
|
||||
|
||||
#run-migration:checked ~ .migration-switch-track .migration-switch-thumb {
|
||||
transform: translateX(12px);
|
||||
}
|
||||
|
||||
#run-migration:focus-visible ~ .migration-switch-track {
|
||||
box-shadow: 0 0 0 var(--border-width-l) var(--border-focus);
|
||||
}
|
||||
|
||||
.migration-hint {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--gap-s);
|
||||
padding: 0 var(--space-2);
|
||||
}
|
||||
|
||||
.migration-hint-icon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--fgcolor-neutral-tertiary);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.migration-hint-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.migration-code {
|
||||
padding: 1px 4px;
|
||||
border-radius: var(--border-radius-xs, 4px);
|
||||
background: var(--bgcolor-neutral-secondary);
|
||||
font-family: monospace;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
const { validateInstallRequest } = window.InstallerStepsProgress || {};
|
||||
|
||||
const isUpgrade = document.body?.dataset.upgrade === 'true';
|
||||
const stepFlow = isUpgrade ? [1, 4, 5] : [1, 2, 3, 4, 5];
|
||||
const stepFlow = isUpgrade ? [1, 6, 4, 5] : [1, 2, 3, 4, 5];
|
||||
const cardSteps = stepFlow.filter((step) => step !== 5);
|
||||
|
||||
const normalizeStep = (step) => {
|
||||
@@ -53,7 +53,7 @@
|
||||
let pendingStep = null;
|
||||
let pendingPushState = false;
|
||||
|
||||
const clampStep = (step) => Math.max(1, Math.min(5, step));
|
||||
const clampStep = (step) => Math.max(1, Math.min(6, step));
|
||||
const isInstallLocked = () => Boolean(window.InstallerSteps?.isInstallLocked?.());
|
||||
|
||||
const scrollToFirstError = (panel) => {
|
||||
@@ -399,11 +399,18 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
if (action === 'next' && String(target) === '5' && typeof validateInstallRequest === 'function') {
|
||||
const isValid = await validateInstallRequest();
|
||||
if (!isValid) {
|
||||
return;
|
||||
if (action === 'next' && String(target) === '5') {
|
||||
if (typeof validateInstallRequest === 'function') {
|
||||
const isValid = await validateInstallRequest();
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Clear stale install data from previous runs so initStep5
|
||||
// starts a fresh install instead of trying to resume.
|
||||
const { clearInstallLock, clearInstallId } = window.InstallerStepsState || {};
|
||||
clearInstallLock?.();
|
||||
clearInstallId?.();
|
||||
}
|
||||
if (isInstallLocked() && Number(target) !== 5) {
|
||||
requestStep(5, true);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
ENV_VARS: 'env-vars',
|
||||
DOCKER_CONTAINERS: 'docker-containers',
|
||||
ACCOUNT_SETUP: 'account-setup',
|
||||
MIGRATION: 'migration',
|
||||
SSL_CERTIFICATE: 'ssl-certificate',
|
||||
REDIRECT: 'redirect'
|
||||
});
|
||||
@@ -52,6 +53,11 @@
|
||||
id: STEP_IDS.DOCKER_CONTAINERS,
|
||||
inProgress: 'Restarting Docker containers...',
|
||||
done: 'Docker containers restarted'
|
||||
},
|
||||
{
|
||||
id: STEP_IDS.MIGRATION,
|
||||
inProgress: 'Running database migration...',
|
||||
done: 'Database migration completed'
|
||||
}
|
||||
] : [
|
||||
{
|
||||
@@ -95,7 +101,7 @@
|
||||
const clampStep = (step) => {
|
||||
const numeric = Number(step);
|
||||
if (Number.isNaN(numeric)) return 1;
|
||||
return Math.max(1, Math.min(5, numeric));
|
||||
return Math.max(1, Math.min(6, numeric));
|
||||
};
|
||||
|
||||
window.InstallerStepsContext = Object.freeze({
|
||||
|
||||
@@ -373,7 +373,8 @@
|
||||
opensslKey: (formState?.opensslKey || '').trim(),
|
||||
assistantOpenAIKey: normalizedAssistantKey,
|
||||
accountEmail: normalizedAccountEmail,
|
||||
accountPassword: normalizedAccountPassword
|
||||
accountPassword: normalizedAccountPassword,
|
||||
migrate: formState?.migrate ?? false
|
||||
};
|
||||
};
|
||||
|
||||
@@ -721,7 +722,8 @@
|
||||
});
|
||||
startSyncedSpinnerRotation(list);
|
||||
|
||||
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
|
||||
const completeId = activeInstall?.installId || getStoredInstallId?.();
|
||||
notifyInstallComplete(completeId, sessionDetails).finally(() => {
|
||||
setTimeout(() => redirectToApp(protocol), TIMINGS?.redirectDelay ?? 0);
|
||||
});
|
||||
};
|
||||
@@ -911,21 +913,28 @@
|
||||
};
|
||||
|
||||
const isSnapshotTerminal = (snapshot) => {
|
||||
if (!snapshot?.steps) return true;
|
||||
if (!snapshot?.steps) return 'empty';
|
||||
const stepEntries = Object.values(snapshot.steps);
|
||||
if (stepEntries.length === 0) return true;
|
||||
if (stepEntries.length === 0) return 'empty';
|
||||
const hasError = stepEntries.some((s) => s.status === STATUS.ERROR);
|
||||
if (hasError) return true;
|
||||
if (hasError) return 'error';
|
||||
const allCompleted = INSTALLATION_STEPS.every((step) => {
|
||||
const detail = snapshot.steps[step.id];
|
||||
return detail && detail.status === STATUS.COMPLETED;
|
||||
});
|
||||
return allCompleted;
|
||||
if (allCompleted) return 'completed';
|
||||
return false;
|
||||
};
|
||||
|
||||
const resumeInstall = async (installId) => {
|
||||
const snapshot = await fetchInstallStatus(installId);
|
||||
if (!snapshot || isSnapshotTerminal(snapshot)) return false;
|
||||
const terminal = isSnapshotTerminal(snapshot);
|
||||
if (!snapshot || terminal) {
|
||||
if (terminal === 'completed') {
|
||||
return 'completed';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
activeInstall = {
|
||||
installId,
|
||||
controller: new AbortController(),
|
||||
@@ -1069,14 +1078,33 @@
|
||||
startInstallStream(newInstallId);
|
||||
};
|
||||
|
||||
const recoverToLastStep = () => {
|
||||
clearInstallId?.();
|
||||
clearInstallLock?.();
|
||||
const url = new URL(window.location.href);
|
||||
const lastStep = url.searchParams.get('step');
|
||||
// Stay on the current URL so the user keeps their place;
|
||||
// only navigate away if we're already on step 5 (the
|
||||
// progress screen) since there's nothing to show.
|
||||
if (!lastStep || String(lastStep) === '5') {
|
||||
window.location.href = '/?step=1';
|
||||
}
|
||||
};
|
||||
|
||||
const lock = getInstallLock?.();
|
||||
const existingInstallId = lock?.installId || getStoredInstallId?.();
|
||||
if (existingInstallId) {
|
||||
resumeInstall(existingInstallId).then((resumed) => {
|
||||
if (!resumed) {
|
||||
clearInstallId?.();
|
||||
resumeInstall(existingInstallId).then((result) => {
|
||||
if (result === 'completed') {
|
||||
// Install already finished — redirect to console
|
||||
// instead of bouncing back to step 1.
|
||||
stopSyncedSpinnerRotation();
|
||||
setUnloadGuard(false);
|
||||
clearInstallLock?.();
|
||||
window.location.href = '/?step=1';
|
||||
clearInstallId?.();
|
||||
startSslCheck(null);
|
||||
} else if (!result) {
|
||||
recoverToLastStep();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -329,6 +329,30 @@
|
||||
}
|
||||
};
|
||||
|
||||
const initStep6 = (root) => {
|
||||
if (!root) return;
|
||||
syncInstallLockFlag?.();
|
||||
applyLockPayload?.();
|
||||
applyBodyDefaults?.();
|
||||
|
||||
const checkbox = root.querySelector('#run-migration');
|
||||
if (checkbox) {
|
||||
if (formState.migrate !== undefined) {
|
||||
checkbox.checked = formState.migrate;
|
||||
} else {
|
||||
formState.migrate = checkbox.checked;
|
||||
}
|
||||
checkbox.addEventListener('change', () => {
|
||||
formState.migrate = checkbox.checked;
|
||||
dispatchStateChange?.('migrate');
|
||||
});
|
||||
}
|
||||
|
||||
if (isInstallLocked?.()) {
|
||||
disableControls?.(root);
|
||||
}
|
||||
};
|
||||
|
||||
const initStep = (step, container) => {
|
||||
if (!container) return;
|
||||
const root = container.querySelector('.step-layout') || container;
|
||||
@@ -346,6 +370,7 @@
|
||||
if (normalized === 3) initStep3(root);
|
||||
if (normalized === 4) initStep4(root);
|
||||
if (normalized === 5) Progress.initStep5?.(root);
|
||||
if (normalized === 6) initStep6(root);
|
||||
};
|
||||
|
||||
window.InstallerSteps = {
|
||||
|
||||
@@ -62,12 +62,14 @@ $badgeClass = $defaultSecretKey !== '' ? 'badge-success' : 'badge-warning';
|
||||
<span class="badge badge-neutral typography-text-xs-400" data-review-assistant-badge>Disabled</span>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Appwrite Assistant</div>
|
||||
</div>
|
||||
<?php if (!$isUpgrade) { ?>
|
||||
<div class="review-row">
|
||||
<span class="badge <?php echo $badgeClass; ?> typography-text-xs-400" data-review-badge>
|
||||
<?php echo htmlspecialchars((string) $badgeLabel, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</span>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Secret API key</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ $isUpgrade = $isUpgrade ?? false;
|
||||
<div class="install-panel">
|
||||
<div class="install-header">
|
||||
<div class="typography-text-m-400 text-neutral-primary">
|
||||
<?php echo $isUpgrade ? 'Updating your app…' : 'Installing your app…'; ?>
|
||||
<?php echo $isUpgrade ? 'Updating Appwrite…' : 'Installing Appwrite…'; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="install-list" data-install-list></div>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
$isUpgrade = $isUpgrade ?? false;
|
||||
?>
|
||||
<div class="step-layout" data-step="6">
|
||||
<div class="stack-xl">
|
||||
<div class="stack-xxxs">
|
||||
<h1 class="typography-title-s text-neutral-primary">Database migration</h1>
|
||||
<p class="typography-text-m-400 text-neutral-secondary">
|
||||
Run database migration after the update to apply schema changes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="stack-xl">
|
||||
<label class="migration-option" for="run-migration">
|
||||
<span class="migration-option-content">
|
||||
<span class="typography-text-m-500 text-neutral-primary">Run migration automatically</span>
|
||||
<span class="typography-text-xs-400 text-neutral-tertiary">Recommended when upgrading to a new version</span>
|
||||
</span>
|
||||
<span class="migration-switch">
|
||||
<input type="checkbox" id="run-migration" name="migrate" class="sr-only" checked>
|
||||
<span class="migration-switch-track" aria-hidden="true">
|
||||
<span class="migration-switch-thumb"></span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="migration-hint">
|
||||
<span class="migration-hint-icon">
|
||||
<?php include __DIR__ . '/../../icons/info.svg'; ?>
|
||||
</span>
|
||||
<span class="typography-text-xs-400 text-neutral-tertiary">
|
||||
To run manually later: <code class="migration-code">docker compose exec appwrite migrate</code>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+26
-5
@@ -221,7 +221,9 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authoriza
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
|
||||
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
|
||||
$initializedPools = [];
|
||||
|
||||
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization, &$initializedPools): Database {
|
||||
$projectDocument ??= $project;
|
||||
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
|
||||
$databaseType = $database->getAttribute('type', '');
|
||||
@@ -246,7 +248,8 @@ Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$pool = $pools->get($databaseDSN->getHost());
|
||||
$databaseHost = $databaseDSN->getHost();
|
||||
$pool = $pools->get($databaseHost);
|
||||
|
||||
$adapter = new DatabasePool($pool);
|
||||
$database = new Database($adapter, $cache);
|
||||
@@ -255,12 +258,30 @@ Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register
|
||||
->setAuthorization($authorization);
|
||||
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables, true)) {
|
||||
// When using a separate pool, always use dedicated mode with namespace isolation.
|
||||
// Shared tables mode can't be used across different engines (e.g. MongoDB UUID
|
||||
// tenants are incompatible with PostgreSQL's integer _tenant column).
|
||||
if ($databaseHost !== $dsn->getHost()) {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $projectDocument->getSequence());
|
||||
|
||||
$poolKey = $databaseHost . ':' . $database->getNamespace();
|
||||
if (!isset($initializedPools[$poolKey])) {
|
||||
try {
|
||||
$database->create();
|
||||
} catch (\Utopia\Database\Exception\Duplicate) {
|
||||
// Schema already exists
|
||||
}
|
||||
$initializedPools[$poolKey] = true;
|
||||
}
|
||||
} elseif (\in_array($dsn->getHost(), $sharedTables, true)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int) $projectDocument->getSequence())
|
||||
->setTenant($projectDocument->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@
|
||||
"utopia-php/locale": "0.8.*",
|
||||
"utopia-php/logger": "0.6.*",
|
||||
"utopia-php/messaging": "0.20.*",
|
||||
"utopia-php/migration": "1.8.*",
|
||||
"utopia-php/migration": "1.9.*",
|
||||
"utopia-php/platform": "0.7.*",
|
||||
"utopia-php/pools": "1.*",
|
||||
"utopia-php/span": "1.1.*",
|
||||
|
||||
Generated
+64
-64
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "f9225f2b580de0ccb796b2fb8c881384",
|
||||
"content-hash": "b5261855586680e467168f527e0634ae",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -161,16 +161,16 @@
|
||||
},
|
||||
{
|
||||
"name": "appwrite/php-runtimes",
|
||||
"version": "0.19.4",
|
||||
"version": "0.19.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/runtimes.git",
|
||||
"reference": "eea9d1b3ca2540eab623b419c8afde09ef406c0b"
|
||||
"reference": "aa2f7760cd0493c0880209b92df812c9386b3546"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/runtimes/zipball/eea9d1b3ca2540eab623b419c8afde09ef406c0b",
|
||||
"reference": "eea9d1b3ca2540eab623b419c8afde09ef406c0b",
|
||||
"url": "https://api.github.com/repos/appwrite/runtimes/zipball/aa2f7760cd0493c0880209b92df812c9386b3546",
|
||||
"reference": "aa2f7760cd0493c0880209b92df812c9386b3546",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -210,9 +210,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/runtimes/issues",
|
||||
"source": "https://github.com/appwrite/runtimes/tree/0.19.4"
|
||||
"source": "https://github.com/appwrite/runtimes/tree/0.19.5"
|
||||
},
|
||||
"time": "2026-02-17T10:04:39+00:00"
|
||||
"time": "2026-04-01T01:39:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -1226,16 +1226,16 @@
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/api",
|
||||
"version": "1.8.0",
|
||||
"version": "1.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/api.git",
|
||||
"reference": "df5197c6fd0ddd8e9883b87de042d9341300e2ad"
|
||||
"reference": "6f8d237ce2c304ca85f31970f788e7f074d147be"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/api/zipball/df5197c6fd0ddd8e9883b87de042d9341300e2ad",
|
||||
"reference": "df5197c6fd0ddd8e9883b87de042d9341300e2ad",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/api/zipball/6f8d237ce2c304ca85f31970f788e7f074d147be",
|
||||
"reference": "6f8d237ce2c304ca85f31970f788e7f074d147be",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1292,20 +1292,20 @@
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2026-01-21T04:14:03+00:00"
|
||||
"time": "2026-02-25T13:24:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/context",
|
||||
"version": "1.4.0",
|
||||
"version": "1.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/context.git",
|
||||
"reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf"
|
||||
"reference": "3c414b246e0dabb7d6145404e6a5e4536ca18d07"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/context/zipball/d4c4470b541ce72000d18c339cfee633e4c8e0cf",
|
||||
"reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/context/zipball/3c414b246e0dabb7d6145404e6a5e4536ca18d07",
|
||||
"reference": "3c414b246e0dabb7d6145404e6a5e4536ca18d07",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1347,11 +1347,11 @@
|
||||
],
|
||||
"support": {
|
||||
"chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
|
||||
"docs": "https://opentelemetry.io/docs/php",
|
||||
"docs": "https://opentelemetry.io/docs/languages/php",
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2025-09-19T00:05:49+00:00"
|
||||
"time": "2025-10-19T06:44:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/exporter-otlp",
|
||||
@@ -1419,16 +1419,16 @@
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/gen-otlp-protobuf",
|
||||
"version": "1.8.0",
|
||||
"version": "1.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git",
|
||||
"reference": "673af5b06545b513466081884b47ef15a536edde"
|
||||
"reference": "a229cf161d42001d64c8f21e8f678581fe1c66b9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/673af5b06545b513466081884b47ef15a536edde",
|
||||
"reference": "673af5b06545b513466081884b47ef15a536edde",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/a229cf161d42001d64c8f21e8f678581fe1c66b9",
|
||||
"reference": "a229cf161d42001d64c8f21e8f678581fe1c66b9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1474,30 +1474,30 @@
|
||||
],
|
||||
"support": {
|
||||
"chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
|
||||
"docs": "https://opentelemetry.io/docs/php",
|
||||
"docs": "https://opentelemetry.io/docs/languages/php",
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2025-09-17T23:10:12+00:00"
|
||||
"time": "2025-10-19T06:44:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/sdk",
|
||||
"version": "1.13.0",
|
||||
"version": "1.14.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/sdk.git",
|
||||
"reference": "c76f91203bf7ef98ab3f4e0a82ca21699af185e1"
|
||||
"reference": "6e3d0ce93e76555dd5e2f1d19443ff45b990e410"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/c76f91203bf7ef98ab3f4e0a82ca21699af185e1",
|
||||
"reference": "c76f91203bf7ef98ab3f4e0a82ca21699af185e1",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/6e3d0ce93e76555dd5e2f1d19443ff45b990e410",
|
||||
"reference": "6e3d0ce93e76555dd5e2f1d19443ff45b990e410",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"nyholm/psr7-server": "^1.1",
|
||||
"open-telemetry/api": "^1.7",
|
||||
"open-telemetry/api": "^1.8",
|
||||
"open-telemetry/context": "^1.4",
|
||||
"open-telemetry/sem-conv": "^1.0",
|
||||
"php": "^8.1",
|
||||
@@ -1575,7 +1575,7 @@
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2026-01-28T11:38:11+00:00"
|
||||
"time": "2026-03-21T11:50:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/sem-conv",
|
||||
@@ -3403,16 +3403,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/agents",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/agents.git",
|
||||
"reference": "052227953678a30ecc4b5467401fcb0b2386471e"
|
||||
"reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/agents/zipball/052227953678a30ecc4b5467401fcb0b2386471e",
|
||||
"reference": "052227953678a30ecc4b5467401fcb0b2386471e",
|
||||
"url": "https://api.github.com/repos/utopia-php/agents/zipball/06064fd9fb19b77ae45a12ec7bcbc17670912c30",
|
||||
"reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3450,9 +3450,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/agents/issues",
|
||||
"source": "https://github.com/utopia-php/agents/tree/1.2.1"
|
||||
"source": "https://github.com/utopia-php/agents/tree/1.3.0"
|
||||
},
|
||||
"time": "2026-02-24T06:03:55+00:00"
|
||||
"time": "2026-03-26T03:51:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/analytics",
|
||||
@@ -3850,16 +3850,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "5.3.17",
|
||||
"version": "5.3.19",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "cff2b6ed63d3291b74110d086e16ff089fe05993"
|
||||
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/cff2b6ed63d3291b74110d086e16ff089fe05993",
|
||||
"reference": "cff2b6ed63d3291b74110d086e16ff089fe05993",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
|
||||
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3903,9 +3903,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/database/issues",
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.17"
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.19"
|
||||
},
|
||||
"time": "2026-03-20T01:18:52+00:00"
|
||||
"time": "2026-03-31T15:52:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -4002,16 +4002,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/dns",
|
||||
"version": "1.6.5",
|
||||
"version": "1.6.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/dns.git",
|
||||
"reference": "574327f0f5fabefa7048030c5634cde33ad10640"
|
||||
"reference": "917901ecfe5f09a540e4f689b6cbb80b9f55035d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/dns/zipball/574327f0f5fabefa7048030c5634cde33ad10640",
|
||||
"reference": "574327f0f5fabefa7048030c5634cde33ad10640",
|
||||
"url": "https://api.github.com/repos/utopia-php/dns/zipball/917901ecfe5f09a540e4f689b6cbb80b9f55035d",
|
||||
"reference": "917901ecfe5f09a540e4f689b6cbb80b9f55035d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4053,9 +4053,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/dns/issues",
|
||||
"source": "https://github.com/utopia-php/dns/tree/1.6.5"
|
||||
"source": "https://github.com/utopia-php/dns/tree/1.6.6"
|
||||
},
|
||||
"time": "2026-02-19T16:06:46+00:00"
|
||||
"time": "2026-03-27T11:13:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/domains",
|
||||
@@ -4518,16 +4518,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
"version": "1.8.3",
|
||||
"version": "1.9.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/migration.git",
|
||||
"reference": "8633523b3343d492427331b6eec53f020f6ab7a7"
|
||||
"reference": "7a86aeadf182b63a9f4ceba7e137588b31c5d2e2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/8633523b3343d492427331b6eec53f020f6ab7a7",
|
||||
"reference": "8633523b3343d492427331b6eec53f020f6ab7a7",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/7a86aeadf182b63a9f4ceba7e137588b31c5d2e2",
|
||||
"reference": "7a86aeadf182b63a9f4ceba7e137588b31c5d2e2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4567,9 +4567,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/migration/issues",
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.8.3"
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.9.1"
|
||||
},
|
||||
"time": "2026-03-19T09:18:47+00:00"
|
||||
"time": "2026-03-25T07:05:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/mongo",
|
||||
@@ -5439,16 +5439,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.12.1",
|
||||
"version": "1.14.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "a724aa8db52f83ea35854a004837fa5ce990b736"
|
||||
"reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/a724aa8db52f83ea35854a004837fa5ce990b736",
|
||||
"reference": "a724aa8db52f83ea35854a004837fa5ce990b736",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/7e7e257b10a8c1384a237e7d8d73452e2108901e",
|
||||
"reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5484,9 +5484,9 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.12.1"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.14.0"
|
||||
},
|
||||
"time": "2026-03-24T05:18:43+00:00"
|
||||
"time": "2026-03-26T12:50:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
@@ -6195,11 +6195,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.42",
|
||||
"version": "2.1.44",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/1279e1ce86ba768f0780c9d889852b4e02ff40d0",
|
||||
"reference": "1279e1ce86ba768f0780c9d889852b4e02ff40d0",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/4a88c083c668b2c364a425c9b3171b2d9ea5d218",
|
||||
"reference": "4a88c083c668b2c364a425c9b3171b2d9ea5d218",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6244,7 +6244,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-17T14:58:32+00:00"
|
||||
"time": "2026-03-25T17:34:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Create multiple operations in a single transaction.
|
||||
@@ -0,0 +1 @@
|
||||
Create a new transaction.
|
||||
@@ -0,0 +1 @@
|
||||
Delete a transaction by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get a transaction by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
List transactions across all databases.
|
||||
@@ -0,0 +1 @@
|
||||
Update a transaction, to either commit or roll back its operations.
|
||||
@@ -1 +1 @@
|
||||
Get the number of audit logs that are waiting to be processed in the Appwrite internal queue server.
|
||||
Get the number of audit logs that are waiting to be processed in the Appwrite internal queue server.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Create a new Collection. Before using this route, you should create a new database resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
|
||||
@@ -0,0 +1 @@
|
||||
Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
|
||||
@@ -0,0 +1 @@
|
||||
Create new Documents. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
|
||||
@@ -0,0 +1,2 @@
|
||||
Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.
|
||||
Attributes can be `key`, `fulltext`, and `unique`.
|
||||
@@ -0,0 +1 @@
|
||||
Create multiple operations in a single transaction.
|
||||
@@ -0,0 +1 @@
|
||||
Create a new transaction.
|
||||
@@ -0,0 +1 @@
|
||||
Create a new Database.
|
||||
@@ -0,0 +1 @@
|
||||
Decrement a specific column of a row by a given value.
|
||||
@@ -0,0 +1 @@
|
||||
Delete a collection by its unique ID. Only users with write permissions have access to delete this resource.
|
||||
@@ -0,0 +1 @@
|
||||
Delete a document by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Bulk delete documents using queries, if no queries are passed then all documents are deleted.
|
||||
@@ -0,0 +1 @@
|
||||
Delete an index.
|
||||
@@ -0,0 +1 @@
|
||||
Delete a transaction by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Delete a database by its unique ID. Only API keys with with databases.write scope can delete a database.
|
||||
@@ -0,0 +1 @@
|
||||
Get the collection activity logs list by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get usage metrics and statistics for a collection. Returning the total number of documents. The response includes both current totals and historical data over time. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, range defaults to 30 days.
|
||||
@@ -0,0 +1 @@
|
||||
Get a collection by its unique ID. This endpoint response returns a JSON object with the collection metadata.
|
||||
@@ -0,0 +1 @@
|
||||
Get usage metrics and statistics for a database. You can view the total number of collections, documents, and storage usage. The response includes both current totals and historical data over time. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, range defaults to 30 days.
|
||||
@@ -0,0 +1 @@
|
||||
Get the document activity logs list by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get a document by its unique ID. This endpoint response returns a JSON object with the document data.
|
||||
@@ -0,0 +1 @@
|
||||
Get index by ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get the database activity logs list by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get a transaction by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Get a database by its unique ID. This endpoint response returns a JSON object with the database metadata.
|
||||
@@ -0,0 +1 @@
|
||||
Increment a specific column of a row by a given value.
|
||||
@@ -0,0 +1 @@
|
||||
List attributes in the collection.
|
||||
@@ -0,0 +1 @@
|
||||
Get a list of all collections that belong to the provided databaseId. You can use the search parameter to filter your results.
|
||||
@@ -0,0 +1 @@
|
||||
Get a list of all the user's documents in a given collection. You can use the query params to filter your results.
|
||||
@@ -0,0 +1 @@
|
||||
List indexes in the collection.
|
||||
@@ -0,0 +1 @@
|
||||
List transactions across all databases.
|
||||
@@ -0,0 +1 @@
|
||||
List usage metrics and statistics for all databases in the project. You can view the total number of databases, collections, documents, and storage usage. The response includes both current totals and historical data over time. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, range defaults to 30 days.
|
||||
@@ -0,0 +1 @@
|
||||
Get a list of all databases from the current Appwrite project. You can use the search parameter to filter your results.
|
||||
@@ -0,0 +1 @@
|
||||
Update a collection by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Update a document by its unique ID. Using the patch method you can pass only specific fields that will get updated.
|
||||
@@ -0,0 +1 @@
|
||||
Update all documents that match your queries, if no queries are submitted then all documents are updated. You can pass only specific fields to be updated.
|
||||
@@ -0,0 +1 @@
|
||||
Update a transaction, to either commit or roll back its operations.
|
||||
@@ -0,0 +1 @@
|
||||
Update a database by its unique ID.
|
||||
@@ -0,0 +1 @@
|
||||
Create or update a Document. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
|
||||
@@ -0,0 +1 @@
|
||||
Create or update Documents. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
|
||||
@@ -1,17 +0,0 @@
|
||||
# Change Log
|
||||
|
||||
## 0.3.0
|
||||
|
||||
* Add `bytesMax` and `bytesUsed` properties to Collection and Table documentation
|
||||
* Add `queries` parameter to `listKeys` and `keyId` parameter to `createKey` documentation
|
||||
* Add `dart-3.10` and `flutter-3.38` runtimes
|
||||
* Fix Teams membership docs to use `string[]` instead of `Roles[]`
|
||||
|
||||
## 0.2.0
|
||||
|
||||
* Document array-based enum parameters in Markdown examples (e.g., `permissions: BrowserPermission[]`).
|
||||
* Breaking change: `Output` enum has been removed; use `ImageFormat` instead.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
* Initial release
|
||||
@@ -0,0 +1,5 @@
|
||||
# Change Log
|
||||
|
||||
## 0.1.0
|
||||
|
||||
* Initial release
|
||||
@@ -0,0 +1,68 @@
|
||||
## Getting Started
|
||||
|
||||
### Init your SDK
|
||||
Initialize your SDK with your Appwrite server API endpoint and project ID which can be found on your project settings page and your new API secret Key from project's API keys section.
|
||||
|
||||
```rust
|
||||
use appwrite::client::Client;
|
||||
|
||||
let client = Client::new()
|
||||
.set_endpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
|
||||
.set_project("5df5acd0d48c2") // Your project ID
|
||||
.set_key("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key
|
||||
.set_self_signed(true); // Use only on dev mode with a self-signed SSL cert
|
||||
```
|
||||
|
||||
### Make Your First Request
|
||||
Once your SDK object is set, create any of the Appwrite service objects and choose any request to send. Full documentation for any service method you would like to use can be found in your SDK documentation or in the [API References](https://appwrite.io/docs) section.
|
||||
|
||||
```rust
|
||||
use appwrite::client::Client;
|
||||
use appwrite::services::users::Users;
|
||||
use appwrite::id::ID;
|
||||
|
||||
let client = Client::new()
|
||||
.set_endpoint("https://[HOSTNAME_OR_IP]/v1")
|
||||
.set_project("5df5acd0d48c2")
|
||||
.set_key("919c2d18fb5d4...a2ae413da83346ad2")
|
||||
.set_self_signed(true);
|
||||
|
||||
let users = Users::new(&client);
|
||||
|
||||
let user = users.create(
|
||||
ID::unique(),
|
||||
Some("email@example.com"),
|
||||
Some("+123456789"),
|
||||
Some("password"),
|
||||
Some("Walter O'Brien"),
|
||||
).await?;
|
||||
|
||||
println!("{}", user.name);
|
||||
println!("{}", user.email);
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
The Appwrite Rust SDK returns `Result` types. You can handle errors using standard Rust error handling patterns. Below is an example.
|
||||
|
||||
```rust
|
||||
use appwrite::error::AppwriteError;
|
||||
|
||||
match users.create(
|
||||
ID::unique(),
|
||||
Some("email@example.com"),
|
||||
Some("+123456789"),
|
||||
Some("password"),
|
||||
Some("Walter O'Brien"),
|
||||
).await {
|
||||
Ok(user) => println!("{}", user.name),
|
||||
Err(AppwriteError { message, code, .. }) => {
|
||||
eprintln!("Error {}: {}", code, message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Learn more
|
||||
You can use the following resources to learn more and get help
|
||||
- 🚀 [Getting Started Tutorial](https://appwrite.io/docs/getting-started-for-server)
|
||||
- 📜 [Appwrite Docs](https://appwrite.io/docs)
|
||||
- 💬 [Discord Community](https://appwrite.io/discord)
|
||||
+2
-535
@@ -114,12 +114,6 @@ parameters:
|
||||
count: 1
|
||||
path: app/controllers/mock.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Utopia\\Database\\Document\:\:getRoles\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: app/controllers/shared/api.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$register might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
@@ -192,108 +186,6 @@ parameters:
|
||||
count: 3
|
||||
path: app/worker.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @return with type string is incompatible with native type int\.$#'
|
||||
identifier: return.phpDocType
|
||||
count: 1
|
||||
path: src/Appwrite/Auth/OAuth2.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$token$#'
|
||||
identifier: parameter.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Auth/OAuth2/Disqus.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$value$#'
|
||||
identifier: parameter.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Auth/Validator/PersonalData.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param has invalid value \(DeviceDetector\)\: Unexpected token "\\n ", expected variable at offset 32 on line 2$#'
|
||||
identifier: phpDoc.parseError
|
||||
count: 1
|
||||
path: src/Appwrite/Detector/Detector.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param has invalid value \(string\)\: Unexpected token "\\n ", expected variable at offset 24 on line 2$#'
|
||||
identifier: phpDoc.parseError
|
||||
count: 1
|
||||
path: src/Appwrite/Detector/Detector.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @var above a method has no effect\.$#'
|
||||
identifier: varTag.misplaced
|
||||
count: 1
|
||||
path: src/Appwrite/Docker/Compose.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @var above a method has no effect\.$#'
|
||||
identifier: varTag.misplaced
|
||||
count: 1
|
||||
path: src/Appwrite/Docker/Compose/Service.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @var above a method has no effect\.$#'
|
||||
identifier: varTag.misplaced
|
||||
count: 1
|
||||
path: src/Appwrite/Docker/Env.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param has invalid value \(int port\)\: Unexpected token "port", expected variable at offset 50 on line 4$#'
|
||||
identifier: phpDoc.parseError
|
||||
count: 1
|
||||
path: src/Appwrite/Event/Mail.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$password$#'
|
||||
identifier: parameter.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Event/Mail.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @return with type string is incompatible with native type Appwrite\\Event\\Mail\.$#'
|
||||
identifier: return.phpDocType
|
||||
count: 1
|
||||
path: src/Appwrite/Event/Mail.php
|
||||
|
||||
-
|
||||
message: '#^Method Appwrite\\Event\\Message\\Usage\:\:fromArray\(\) should return static\(Appwrite\\Event\\Message\\Usage\) but returns Appwrite\\Event\\Message\\Usage\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/Appwrite/Event/Message/Usage.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$message$#'
|
||||
identifier: parameter.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Event/Messaging.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @return with type string is incompatible with native type Utopia\\Database\\Document\.$#'
|
||||
identifier: return.phpDocType
|
||||
count: 1
|
||||
path: src/Appwrite/Event/Messaging.php
|
||||
|
||||
-
|
||||
message: '#^Method Appwrite\\Functions\\EventProcessor\:\:getFunctionsEvents\(\) should return array\<string, bool\> but returns array\<int\<0, max\>\>\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/Appwrite/Functions/EventProcessor.php
|
||||
|
||||
-
|
||||
message: '#^Method Appwrite\\Functions\\EventProcessor\:\:getWebhooksEvents\(\) should return array\<string, bool\> but returns array\<int\<0, max\>\>\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/Appwrite/Functions/EventProcessor.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$hostname on left side of \?\? always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Functions/EventProcessor.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$context\.$#'
|
||||
identifier: closure.unusedUse
|
||||
@@ -366,30 +258,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Appwrite/GraphQL/Types/Mapper.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Appwrite\\GraphQL\\Types\\Mapper\:\:\$models through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 3
|
||||
path: src/Appwrite/GraphQL/Types/Mapper.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe call to private method Appwrite\\GraphQL\\Types\\Mapper\:\:getColumnImplementation\(\) through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateMethod
|
||||
count: 2
|
||||
path: src/Appwrite/GraphQL/Types/Mapper.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe call to private method Appwrite\\GraphQL\\Types\\Mapper\:\:getHashOptionsImplementation\(\) through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateMethod
|
||||
count: 1
|
||||
path: src/Appwrite/GraphQL/Types/Mapper.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe call to private method Appwrite\\GraphQL\\Types\\Mapper\:\:getUnionImplementation\(\) through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateMethod
|
||||
count: 1
|
||||
path: src/Appwrite/GraphQL/Types/Mapper.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Appwrite\\Migration\\Version\\V15\:\:documentsIterator\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
@@ -456,12 +324,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Appwrite/OpenSSL/OpenSSL.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$projectId$#'
|
||||
identifier: parameter.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Action.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$output in empty\(\) always exists and is not falsy\.$#'
|
||||
identifier: empty.variable
|
||||
@@ -594,12 +456,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$currentDocumentId on left side of \?\? always exists and is always null\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
|
||||
|
||||
-
|
||||
message: '#^Offset ''deviceBrand'' does not exist on int\.$#'
|
||||
identifier: offsetAccess.notFound
|
||||
@@ -1164,12 +1020,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Appwrite/SDK/Specification/Format/Swagger2.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @var above a method has no effect\.$#'
|
||||
identifier: varTag.misplaced
|
||||
count: 2
|
||||
path: src/Appwrite/Template/Template.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param has invalid value \(Document \$this\)\: Unexpected token "\$this", expected variable at offset 69 on line 4$#'
|
||||
identifier: phpDoc.parseError
|
||||
@@ -1182,35 +1032,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Database/Documents/User.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe call to private method Appwrite\\Utopia\\Request\\Filters\\V17\:\:appendSymbol\(\) through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateMethod
|
||||
count: 4
|
||||
path: src/Appwrite/Utopia/Request/Filters/V17.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe call to private method Appwrite\\Utopia\\Request\\Filters\\V17\:\:isSpecialChar\(\) through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateMethod
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Request/Filters/V17.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param has invalid value \(callable The callback to show sensitive information for\)\: Unexpected token "The", expected variable at offset 91 on line 4$#'
|
||||
identifier: phpDoc.parseError
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Response.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @return with type Appwrite\\Utopia\\Response\\Filter is incompatible with native type array\.$#'
|
||||
identifier: return.phpDocType
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Response.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @return with type string is incompatible with native type Utopia\\Database\\Document\.$#'
|
||||
identifier: return.phpDocType
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Response/Model/User.php
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Databases\\Legacy\\DatabasesStringTypesTest\:\:\$setupCache through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
@@ -1246,108 +1067,12 @@ parameters:
|
||||
count: 8
|
||||
path: tests/e2e/Services/Functions/FunctionsCustomServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsClientTest\:\:\$cachedDeployment through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 3
|
||||
path: tests/e2e/Services/GraphQL/FunctionsClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsClientTest\:\:\$cachedExecution through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/FunctionsClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsClientTest\:\:\$cachedFunction through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 3
|
||||
path: tests/e2e/Services/GraphQL/FunctionsClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsServerTest\:\:\$cachedDeployment through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/FunctionsServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsServerTest\:\:\$cachedExecution through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/FunctionsServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsServerTest\:\:\$cachedFunction through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/FunctionsServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Binary operation "\+" between string and 1 results in an error\.$#'
|
||||
identifier: binaryOp.invalid
|
||||
count: 1
|
||||
path: tests/e2e/Services/GraphQL/Legacy/AbuseTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\Legacy\\DatabaseClientTest\:\:\$bulkData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\Legacy\\DatabaseClientTest\:\:\$collection through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\Legacy\\DatabaseClientTest\:\:\$database through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\Legacy\\DatabaseClientTest\:\:\$document through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedEmail through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 2
|
||||
path: tests/e2e/Services/GraphQL/MessagingTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedProviders through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 8
|
||||
path: tests/e2e/Services/GraphQL/MessagingTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedPush through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/MessagingTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedSms through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/MessagingTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedSubscriber through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/MessagingTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedTopic through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 9
|
||||
path: tests/e2e/Services/GraphQL/MessagingTest.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$from in empty\(\) is never defined\.$#'
|
||||
identifier: empty.variable
|
||||
@@ -1360,174 +1085,18 @@ parameters:
|
||||
count: 1
|
||||
path: tests/e2e/Services/GraphQL/StorageClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\StorageClientTest\:\:\$cachedBucket through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 3
|
||||
path: tests/e2e/Services/GraphQL/StorageClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\StorageClientTest\:\:\$cachedFile through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/StorageClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Method Tests\\E2E\\Services\\GraphQL\\StorageServerTest\:\:testGetFileDownload\(\) should return array but return statement is missing\.$#'
|
||||
identifier: return.missing
|
||||
count: 1
|
||||
path: tests/e2e/Services/GraphQL/StorageServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\StorageServerTest\:\:\$cachedBucket through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/StorageServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\StorageServerTest\:\:\$cachedFile through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 6
|
||||
path: tests/e2e/Services/GraphQL/StorageServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Binary operation "\+" between string and 1 results in an error\.$#'
|
||||
identifier: binaryOp.invalid
|
||||
count: 1
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/AbuseTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedBooleanColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedBulkData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedDatabase through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedDatetimeColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedEmailColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedEnumColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedFloatColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedIPColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedIndexData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 7
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedIntegerColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedRelationshipColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedRowData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedStringColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedTableData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedURLColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsClientTest\:\:\$cachedMembership through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/TeamsClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsClientTest\:\:\$cachedTeam through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 3
|
||||
path: tests/e2e/Services/GraphQL/TeamsClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsServerTest\:\:\$cachedMembership through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/TeamsServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsServerTest\:\:\$cachedTeam through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 3
|
||||
path: tests/e2e/Services/GraphQL/TeamsServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsServerTest\:\:\$cachedTeamWithPrefs through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 3
|
||||
path: tests/e2e/Services/GraphQL/TeamsServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\UsersTest\:\:\$cachedUser through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 3
|
||||
path: tests/e2e/Services/GraphQL/UsersTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\UsersTest\:\:\$cachedUserTarget through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/UsersTest.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$from in empty\(\) is never defined\.$#'
|
||||
identifier: empty.variable
|
||||
@@ -1549,13 +1118,13 @@ parameters:
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$databaseId\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 5
|
||||
count: 6
|
||||
path: tests/e2e/Services/Migrations/MigrationsConsoleClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$tableId\.$#'
|
||||
identifier: closure.unusedUse
|
||||
count: 5
|
||||
count: 6
|
||||
path: tests/e2e/Services/Migrations/MigrationsConsoleClientTest.php
|
||||
|
||||
-
|
||||
@@ -1599,36 +1168,6 @@ parameters:
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: tests/e2e/Services/TablesDB/Permissions/TablesDBPermissionsTeamTest.php
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensConsoleClientTest\:\:\$bucketAndFileData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/Tokens/TokensConsoleClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensConsoleClientTest\:\:\$tokenData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/Tokens/TokensConsoleClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensCustomClientTest\:\:\$bucketAndFileData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/Tokens/TokensCustomClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensCustomServerTest\:\:\$bucketAndFileData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/Tokens/TokensCustomServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensCustomServerTest\:\:\$tokenData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/Tokens/TokensCustomServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Users\\UsersCustomServerTest\:\:\$cachedHashedPasswordUsers through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
@@ -1688,75 +1227,3 @@ parameters:
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: tests/unit/Event/EventTest.php
|
||||
|
||||
-
|
||||
message: '#^Call to method parse\(\) on an unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter\.$#'
|
||||
identifier: class.notFound
|
||||
count: 6
|
||||
path: tests/unit/Utopia/Response/Filters/V16Test.php
|
||||
|
||||
-
|
||||
message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V16Test\:\:\$filter \(Tests\\Unit\\Utopia\\Response\\Filters\\Filter\) does not accept Appwrite\\Utopia\\Response\\Filters\\V16\.$#'
|
||||
identifier: assign.propertyType
|
||||
count: 1
|
||||
path: tests/unit/Utopia/Response/Filters/V16Test.php
|
||||
|
||||
-
|
||||
message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V16Test\:\:\$filter has unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter as its type\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: tests/unit/Utopia/Response/Filters/V16Test.php
|
||||
|
||||
-
|
||||
message: '#^Call to method parse\(\) on an unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter\.$#'
|
||||
identifier: class.notFound
|
||||
count: 5
|
||||
path: tests/unit/Utopia/Response/Filters/V17Test.php
|
||||
|
||||
-
|
||||
message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V17Test\:\:\$filter \(Tests\\Unit\\Utopia\\Response\\Filters\\Filter\) does not accept Appwrite\\Utopia\\Response\\Filters\\V17\.$#'
|
||||
identifier: assign.propertyType
|
||||
count: 1
|
||||
path: tests/unit/Utopia/Response/Filters/V17Test.php
|
||||
|
||||
-
|
||||
message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V17Test\:\:\$filter has unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter as its type\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: tests/unit/Utopia/Response/Filters/V17Test.php
|
||||
|
||||
-
|
||||
message: '#^Call to method parse\(\) on an unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter\.$#'
|
||||
identifier: class.notFound
|
||||
count: 4
|
||||
path: tests/unit/Utopia/Response/Filters/V18Test.php
|
||||
|
||||
-
|
||||
message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V18Test\:\:\$filter \(Tests\\Unit\\Utopia\\Response\\Filters\\Filter\) does not accept Appwrite\\Utopia\\Response\\Filters\\V18\.$#'
|
||||
identifier: assign.propertyType
|
||||
count: 1
|
||||
path: tests/unit/Utopia/Response/Filters/V18Test.php
|
||||
|
||||
-
|
||||
message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V18Test\:\:\$filter has unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter as its type\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: tests/unit/Utopia/Response/Filters/V18Test.php
|
||||
|
||||
-
|
||||
message: '#^Call to method parse\(\) on an unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter\.$#'
|
||||
identifier: class.notFound
|
||||
count: 11
|
||||
path: tests/unit/Utopia/Response/Filters/V19Test.php
|
||||
|
||||
-
|
||||
message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V19Test\:\:\$filter \(Tests\\Unit\\Utopia\\Response\\Filters\\Filter\) does not accept Appwrite\\Utopia\\Response\\Filters\\V19\.$#'
|
||||
identifier: assign.propertyType
|
||||
count: 1
|
||||
path: tests/unit/Utopia/Response/Filters/V19Test.php
|
||||
|
||||
-
|
||||
message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V19Test\:\:\$filter has unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter as its type\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: tests/unit/Utopia/Response/Filters/V19Test.php
|
||||
|
||||
@@ -155,7 +155,7 @@ abstract class OAuth2
|
||||
/**
|
||||
* @param string $code
|
||||
*
|
||||
* @return string
|
||||
* @return int
|
||||
*/
|
||||
public function getAccessTokenExpiry(string $code): int
|
||||
{
|
||||
|
||||
@@ -108,7 +108,7 @@ class Disqus extends OAuth2
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $token
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -33,7 +33,7 @@ class PersonalData extends Password
|
||||
/**
|
||||
* Is valid.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param mixed $password
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
@@ -6,14 +6,8 @@ use DeviceDetector\DeviceDetector;
|
||||
|
||||
class Detector
|
||||
{
|
||||
/**
|
||||
* @param string
|
||||
*/
|
||||
protected $userAgent = '';
|
||||
|
||||
/**
|
||||
* @param DeviceDetector
|
||||
*/
|
||||
protected $detctor;
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,9 +12,6 @@ class Compose
|
||||
*/
|
||||
protected $compose = [];
|
||||
|
||||
/**
|
||||
* @var string $data
|
||||
*/
|
||||
public function __construct(string $data)
|
||||
{
|
||||
$this->compose = yaml_parse($data);
|
||||
|
||||
@@ -11,9 +11,6 @@ class Service
|
||||
*/
|
||||
protected $service = [];
|
||||
|
||||
/**
|
||||
* @var string $path
|
||||
*/
|
||||
public function __construct(array $service)
|
||||
{
|
||||
$this->service = $service;
|
||||
|
||||
@@ -9,9 +9,6 @@ class Env
|
||||
*/
|
||||
protected $vars = [];
|
||||
|
||||
/**
|
||||
* @var string $data
|
||||
*/
|
||||
public function __construct(string $data)
|
||||
{
|
||||
$data = explode("\n", $data);
|
||||
|
||||
@@ -101,7 +101,8 @@ class Mail extends Event
|
||||
/**
|
||||
* Sets preview for the mail event.
|
||||
*
|
||||
* @return string
|
||||
* @param string $preview
|
||||
* @return self
|
||||
*/
|
||||
public function setPreview(string $preview): self
|
||||
{
|
||||
@@ -115,7 +116,7 @@ class Mail extends Event
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPreview(string $preview): string
|
||||
public function getPreview(): string
|
||||
{
|
||||
return $this->preview;
|
||||
}
|
||||
@@ -181,7 +182,7 @@ class Mail extends Event
|
||||
/**
|
||||
* Set SMTP port
|
||||
*
|
||||
* @param int port
|
||||
* @param int $port
|
||||
* @return self
|
||||
*/
|
||||
public function setSmtpPort(int $port): self
|
||||
@@ -217,7 +218,7 @@ class Mail extends Event
|
||||
/**
|
||||
* Set SMTP secure
|
||||
*
|
||||
* @param string $password
|
||||
* @param string $secure
|
||||
* @return self
|
||||
*/
|
||||
public function setSmtpSecure(string $secure): self
|
||||
|
||||
@@ -40,7 +40,7 @@ class Usage extends Base
|
||||
*/
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new self(
|
||||
return new static(
|
||||
project: new Document($data['project'] ?? []),
|
||||
metrics: $data['metrics'] ?? [],
|
||||
reduce: array_map(fn (array $doc) => new Document($doc), $data['reduce'] ?? []),
|
||||
|
||||
@@ -86,7 +86,7 @@ class Messaging extends Event
|
||||
/**
|
||||
* Returns message document for the messaging event.
|
||||
*
|
||||
* @return string
|
||||
* @return Document
|
||||
*/
|
||||
public function getMessage(): Document
|
||||
{
|
||||
@@ -96,7 +96,7 @@ class Messaging extends Event
|
||||
/**
|
||||
* Sets message ID for the messaging event.
|
||||
*
|
||||
* @param string $message
|
||||
* @param string $messageId
|
||||
* @return self
|
||||
*/
|
||||
public function setMessageId(string $messageId): self
|
||||
|
||||
@@ -8,6 +8,18 @@ use Utopia\Database\Query;
|
||||
|
||||
class EventProcessor
|
||||
{
|
||||
/**
|
||||
* @param array<mixed> $events
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
private function getEventMap(array $events): array
|
||||
{
|
||||
return \array_fill_keys(
|
||||
\array_map('strval', \array_unique($events)),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get function events for a project, using Redis cache
|
||||
* @param Document|null $project
|
||||
@@ -26,7 +38,7 @@ class EventProcessor
|
||||
$cacheKey = \sprintf(
|
||||
'%s-cache-%s:%s:%s:project:%s:functions:events',
|
||||
$dbForProject->getCacheName(),
|
||||
$hostname ?? '',
|
||||
$hostname,
|
||||
$dbForProject->getNamespace(),
|
||||
$dbForProject->getTenant(),
|
||||
$project->getId()
|
||||
@@ -36,7 +48,9 @@ class EventProcessor
|
||||
$cachedFunctionEvents = $dbForProject->getCache()->load($cacheKey, $ttl);
|
||||
|
||||
if ($cachedFunctionEvents !== false) {
|
||||
return \json_decode($cachedFunctionEvents, true) ?? [];
|
||||
$decoded = \json_decode($cachedFunctionEvents, true);
|
||||
|
||||
return \is_array($decoded) ? $this->getEventMap(\array_keys($decoded)) : [];
|
||||
}
|
||||
|
||||
$events = [];
|
||||
@@ -63,7 +77,7 @@ class EventProcessor
|
||||
}
|
||||
}
|
||||
|
||||
$uniqueEvents = \array_flip(\array_unique($events));
|
||||
$uniqueEvents = $this->getEventMap($events);
|
||||
$dbForProject->getCache()->save($cacheKey, \json_encode($uniqueEvents));
|
||||
|
||||
return $uniqueEvents;
|
||||
@@ -97,6 +111,6 @@ class EventProcessor
|
||||
}
|
||||
}
|
||||
|
||||
return \array_flip(\array_unique($events));
|
||||
return $this->getEventMap($events);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,16 +101,16 @@ class Mapper
|
||||
|
||||
if (\is_array($modelName)) {
|
||||
foreach ($modelName as $name) {
|
||||
$models[] = static::$models[$name];
|
||||
$models[] = self::$models[$name];
|
||||
}
|
||||
} else {
|
||||
$models[] = static::$models[$modelName];
|
||||
$models[] = self::$models[$modelName];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If single response, get its model and wrap in array
|
||||
$modelName = $responses->getModel();
|
||||
$models = [static::$models[$modelName]];
|
||||
$models = [self::$models[$modelName]];
|
||||
}
|
||||
|
||||
foreach ($models as $model) {
|
||||
@@ -425,7 +425,7 @@ class Mapper
|
||||
'name' => $unionName,
|
||||
'types' => $types,
|
||||
'resolveType' => static function ($object) use ($unionName) {
|
||||
return static::getUnionImplementation($unionName, $object);
|
||||
return self::getUnionImplementation($unionName, $object);
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -440,11 +440,11 @@ class Mapper
|
||||
|
||||
switch ($name) {
|
||||
case 'Attributes':
|
||||
return static::getColumnImplementation($object);
|
||||
return self::getColumnImplementation($object);
|
||||
case 'Columns':
|
||||
return static::getColumnImplementation($object, true);
|
||||
return self::getColumnImplementation($object, true);
|
||||
case 'HashOptions':
|
||||
return static::getHashOptionsImplementation($object);
|
||||
return self::getHashOptionsImplementation($object);
|
||||
}
|
||||
|
||||
throw new Exception('Unknown union type: ' . $name);
|
||||
|
||||
@@ -187,6 +187,20 @@ class V24 extends Migration
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'users':
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->dbForProject, $id, 'impersonator');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create attribute \"impersonator\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
$this->createIndexFromCollection($this->dbForProject, $id, 'impersonator');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create index \"impersonator\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'teams':
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->dbForProject, $id, 'labels');
|
||||
|
||||
@@ -37,7 +37,7 @@ class Action extends UtopiaAction
|
||||
* Foreach Document
|
||||
* Call provided callback for each document in the collection
|
||||
*
|
||||
* @param string $projectId
|
||||
* @param Database $database
|
||||
* @param string $collection
|
||||
* @param array $queries
|
||||
* @param callable $callback
|
||||
|
||||
@@ -43,6 +43,7 @@ class Install extends Action
|
||||
->param('database', '', new WhiteList(['mongodb', 'mariadb', 'postgresql']), 'Database adapter', true)
|
||||
->param('installId', '', new Text(64, 0), 'Installation ID', true)
|
||||
->param('retryStep', null, new Nullable(new WhiteList([Server::STEP_DOCKER_COMPOSE, Server::STEP_ENV_VARS, Server::STEP_DOCKER_CONTAINERS], true)), 'Retry from step', true)
|
||||
->param('migrate', false, new \Utopia\Validator\Boolean(true), 'Run database migration after upgrade', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('swooleResponse')
|
||||
@@ -64,6 +65,7 @@ class Install extends Action
|
||||
string $database,
|
||||
string $installId,
|
||||
?string $retryStep,
|
||||
bool $migrate,
|
||||
Request $request,
|
||||
Response $response,
|
||||
SwooleResponse $swooleResponse,
|
||||
@@ -321,6 +323,28 @@ class Install extends Action
|
||||
}
|
||||
};
|
||||
|
||||
$responseSent = false;
|
||||
$onComplete = function () use ($wantsStream, $swooleResponse, $response, $installId, $state, &$responseSent) {
|
||||
if ($responseSent) {
|
||||
return;
|
||||
}
|
||||
$responseSent = true;
|
||||
$state->updateGlobalLock($installId, Server::STATUS_COMPLETED);
|
||||
if ($wantsStream) {
|
||||
$this->writeSseEvent($swooleResponse, 'done', ['installId' => $installId, 'success' => true]);
|
||||
usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS);
|
||||
$swooleResponse->write(": keepalive\n\n");
|
||||
usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS);
|
||||
$swooleResponse->end();
|
||||
} else {
|
||||
$response->json([
|
||||
'success' => true,
|
||||
'installId' => $installId,
|
||||
'message' => 'Installation completed successfully',
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
$installer->performInstallation(
|
||||
$httpPort ?: $config->getDefaultHttpPort(),
|
||||
$httpsPort ?: $config->getDefaultHttpsPort(),
|
||||
@@ -331,23 +355,12 @@ class Install extends Action
|
||||
$progress,
|
||||
$retryStep,
|
||||
$config->isUpgrade(),
|
||||
$account
|
||||
$account,
|
||||
$onComplete,
|
||||
$migrate,
|
||||
);
|
||||
|
||||
if ($wantsStream) {
|
||||
$this->writeSseEvent($swooleResponse, 'done', ['installId' => $installId, 'success' => true]);
|
||||
usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS);
|
||||
$swooleResponse->write(": keepalive\n\n");
|
||||
usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS);
|
||||
$swooleResponse->end();
|
||||
} else {
|
||||
$response->json([
|
||||
'success' => true,
|
||||
'installId' => $installId,
|
||||
'message' => 'Installation completed successfully',
|
||||
]);
|
||||
}
|
||||
$state->updateGlobalLock($installId, Server::STATUS_COMPLETED);
|
||||
$onComplete();
|
||||
} catch (\Throwable $e) {
|
||||
$this->handleInstallationError($e, $installId, $wantsStream, $response, $swooleResponse, $state);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class View extends Action
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/')
|
||||
->desc('Serve installer UI')
|
||||
->param('step', 1, new Integer(true), 'Step number (1-5)', true)
|
||||
->param('step', 1, new Integer(true), 'Step number (1-6)', true)
|
||||
->param('partial', null, new Nullable(new Text(1, 0)), 'Render partial step only', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
@@ -52,10 +52,13 @@ class View extends Action
|
||||
$defaultEmailCertificates = 'walterobrien@example.com';
|
||||
}
|
||||
|
||||
$step = max(1, min(5, $step));
|
||||
$step = max(1, min(6, $step));
|
||||
if ($isUpgrade && ($step === 2 || $step === 3)) {
|
||||
$step = 4;
|
||||
}
|
||||
if (!$isUpgrade && $step === 6) {
|
||||
$step = 4;
|
||||
}
|
||||
|
||||
$partialFile = $paths['views'] . "/installer/templates/steps/step-{$step}.phtml";
|
||||
if (!is_file($partialFile)) {
|
||||
|
||||
@@ -6,6 +6,7 @@ use Appwrite\Platform\Installer\Http\Installer\Error;
|
||||
use Appwrite\Platform\Installer\Runtime\Config;
|
||||
use Appwrite\Platform\Installer\Runtime\State;
|
||||
use Swoole\Http\Server as SwooleServer;
|
||||
use Swoole\Runtime;
|
||||
use Utopia\Http\Adapter\Swoole\Request;
|
||||
use Utopia\Http\Adapter\Swoole\Response;
|
||||
use Utopia\Http\Adapter\Swoole\Server as SwooleAdapter;
|
||||
@@ -28,6 +29,7 @@ class Server
|
||||
public const string STEP_DOCKER_COMPOSE = 'docker-compose';
|
||||
public const string STEP_DOCKER_CONTAINERS = 'docker-containers';
|
||||
public const string STEP_ACCOUNT_SETUP = 'account-setup';
|
||||
public const string STEP_MIGRATION = 'migration';
|
||||
public const string STEP_SSL_CERTIFICATE = 'ssl-certificate';
|
||||
|
||||
public const string STATUS_IN_PROGRESS = 'in-progress';
|
||||
@@ -129,6 +131,8 @@ class Server
|
||||
|
||||
private function startSwooleServer(string $host, int $port, ?string $readyFile = null): void
|
||||
{
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
|
||||
$this->state->clearStaleLock();
|
||||
|
||||
// Preload static files into memory
|
||||
|
||||
+4
-3
@@ -87,13 +87,14 @@ class Decrement extends Action
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization, User $user): void
|
||||
{
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
|
||||
+4
-3
@@ -87,13 +87,14 @@ class Increment extends Action
|
||||
->inject('usage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization, User $user): void
|
||||
{
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ class Upsert extends Action
|
||||
group: $this->getSDKGroup(),
|
||||
name: self::getName(),
|
||||
description: '/docs/references/databases/upsert-documents.md',
|
||||
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_CREATED,
|
||||
|
||||
+3
-3
@@ -139,7 +139,7 @@ class Create extends Action
|
||||
->inject('eventProcessor')
|
||||
->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, Document $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, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
|
||||
{
|
||||
$data = \is_string($data)
|
||||
? \json_decode($data, true)
|
||||
@@ -183,8 +183,8 @@ class Create extends Action
|
||||
$documents = [$data];
|
||||
}
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($isBulk && !$isAPIKey && !$isPrivilegedUser) {
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE);
|
||||
|
||||
+5
-3
@@ -85,6 +85,7 @@ class Delete extends Action
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -101,12 +102,13 @@ class Delete extends Action
|
||||
Context $usage,
|
||||
TransactionState $transactionState,
|
||||
array $plan,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
User $user
|
||||
): void {
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
|
||||
+4
-3
@@ -72,13 +72,14 @@ class Get extends Action
|
||||
->inject('usage')
|
||||
->inject('transactionState')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Context $usage, TransactionState $transactionState, Authorization $authorization, User $user): void
|
||||
{
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
|
||||
+4
-3
@@ -89,10 +89,11 @@ class Update extends Action
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization): void
|
||||
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization, User $user): void
|
||||
{
|
||||
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
|
||||
|
||||
@@ -102,8 +103,8 @@ class Update extends Action
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
$isAPIKey = User::isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAPIKey = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND, params: [$databaseId]);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user