Merge remote-tracking branch 'origin/1.8.x' into refactor-auth-single-instance

# Conflicts:
#	app/controllers/api/teams.php
#	composer.lock
This commit is contained in:
Jake Barnby
2025-11-12 16:51:21 +13:00
50 changed files with 12177 additions and 3383 deletions
+55
View File
@@ -364,6 +364,61 @@ return [
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('emailCanonical'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 320,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('emailIsFree'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('emailIsDisposable'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('emailIsCorporate'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('emailIsCanonical'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
+1 -1
View File
@@ -2345,7 +2345,7 @@ return [
'$id' => ID::custom('errors'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65535,
'size' => 1_000_000,
'signed' => true,
'required' => true,
'default' => null,
+2 -2
View File
@@ -226,7 +226,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '11.1.0',
'version' => '11.1.1',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
@@ -281,7 +281,7 @@ return [
[
'key' => 'php',
'name' => 'PHP',
'version' => '17.5.0',
'version' => '18.0.1',
'url' => 'https://github.com/appwrite/sdk-for-php',
'package' => 'https://packagist.org/packages/appwrite/appwrite',
'enabled' => true,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+30 -1
View File
@@ -24,6 +24,7 @@ class UseCases
public const ECOMMERCE = 'ecommerce';
public const DOCUMENTATION = 'documentation';
public const BLOG = 'blog';
public const AI = 'artificial intelligence';
}
const TEMPLATE_FRAMEWORKS = [
@@ -970,7 +971,7 @@ return [
'name' => 'TanStack Start starter',
'useCases' => [UseCases::STARTER],
'tagline' => 'Simple TanStack Start application integrated with Appwrite SDK.',
'score' => 6, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'score' => 9, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'screenshotDark' => $url . '/images/sites/templates/starter-for-tanstack-start-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-tanstack-start-light.png',
'frameworks' => [
@@ -1443,4 +1444,32 @@ return [
'providerVersion' => '0.3.*',
'variables' => []
],
[
'key' => 'text-to-speech',
'name' => 'Text-to-speech with ElevenLabs',
'tagline' => 'Next.js app that transforms text into natural, human-like speech using ElevenLabs',
'score' => 10, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [UseCases::AI],
'screenshotDark' => $url . '/images/sites/templates/text-to-speech-dark.png',
'screenshotLight' => $url . '/images/sites/templates/text-to-speech-light.png',
'frameworks' => [
getFramework('NEXTJS', [
'providerRootDirectory' => './nextjs/text-to-speech',
]),
],
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.6.*',
'variables' => [
[
'name' => 'ELEVENLABS_API_KEY',
'description' => 'Your ElevenLabs API key',
'value' => '',
'placeholder' => 'sk_.....',
'required' => true,
'type' => 'password'
],
]
],
];
+83 -7
View File
@@ -20,7 +20,7 @@ use Appwrite\Event\Messaging;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Email as EmailValidator;
use Appwrite\Network\Validator\Redirect;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\SDK\AuthType;
@@ -57,6 +57,7 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Email;
use Utopia\Locale\Locale;
use Utopia\Storage\Validator\FileName;
use Utopia\System\System;
@@ -337,7 +338,7 @@ App::post('/v1/account')
))
->label('abuse-limit', 10)
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary'])
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('request')
@@ -395,6 +396,13 @@ App::post('/v1/account')
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
$password = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
try {
$userId = $userId == 'unique()' ? ID::unique() : $userId;
$user->setAttributes([
@@ -423,7 +431,13 @@ App::post('/v1/account')
'authenticators' => null,
'search' => implode(' ', [$userId, $email, $name]),
'accessedAt' => DateTime::now(),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
]);
$user->removeAttribute('$sequence');
$user = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
try {
@@ -904,7 +918,7 @@ App::post('/v1/account/sessions/email')
))
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},email:{param-email}')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->inject('request')
->inject('response')
@@ -1603,6 +1617,12 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$failureRedirect(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
}
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
try {
$userId = ID::unique();
$user->setAttributes([
@@ -1630,7 +1650,13 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'authenticators' => null,
'search' => implode(' ', [$userId, $email, $name]),
'accessedAt' => DateTime::now(),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
]);
$user->removeAttribute('$sequence');
$userDoc = $authorization->skip(fn () => $dbForProject->createDocument('users', $user));
$dbForProject->createDocument('targets', new Document([
@@ -1701,6 +1727,18 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
if (empty($user->getAttribute('email'))) {
$user->setAttribute('email', $oauth2->getUserEmail($accessToken));
try {
$emailCanonical = new Email($user->getAttribute('email'));
} catch (Throwable) {
$emailCanonical = null;
}
$user->setAttribute('emailCanonical', $emailCanonical?->getCanonical());
$user->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported());
$user->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate());
$user->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable());
$user->setAttribute('emailIsFree', $emailCanonical?->isFree());
}
if (empty($user->getAttribute('name'))) {
@@ -1949,7 +1987,7 @@ App::post('/v1/account/tokens/magic-url')
->label('abuse-limit', 60)
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
->param('userId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars. If the email address has never been used, a new account is created using the provided userId. Otherwise, if the email address is already attached to an account, the user ID is ignored.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
->param('phrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.', true)
->inject('request')
@@ -1996,6 +2034,12 @@ App::post('/v1/account/tokens/magic-url')
$userId = $userId === 'unique()' ? ID::unique() : $userId;
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
$user->setAttributes([
'$id' => $userId,
'$permissions' => [
@@ -2020,6 +2064,11 @@ App::post('/v1/account/tokens/magic-url')
'authenticators' => null,
'search' => implode(' ', [$userId, $email]),
'accessedAt' => DateTime::now(),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
]);
$user->removeAttribute('$sequence');
@@ -2203,7 +2252,7 @@ App::post('/v1/account/tokens/email')
->label('abuse-limit', 10)
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars. If the email address has never been used, a new account is created using the provided userId. Otherwise, if the email address is already attached to an account, the user ID is ignored.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('phrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.', true)
->inject('request')
->inject('response')
@@ -2247,6 +2296,12 @@ App::post('/v1/account/tokens/email')
$userId = $userId === 'unique()' ? ID::unique() : $userId;
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
$user->setAttributes([
'$id' => $userId,
'$permissions' => [
@@ -2269,6 +2324,11 @@ App::post('/v1/account/tokens/email')
'memberships' => null,
'search' => implode(' ', [$userId, $email]),
'accessedAt' => DateTime::now(),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
]);
$user->removeAttribute('$sequence');
@@ -2619,6 +2679,11 @@ App::post('/v1/account/tokens/phone')
'memberships' => null,
'search' => implode(' ', [$userId, $phone]),
'accessedAt' => DateTime::now(),
'emailCanonical' => null,
'emailIsCanonical' => null,
'emailIsCorporate' => null,
'emailIsDisposable' => null,
'emailIsFree' => null,
]);
$user->removeAttribute('$sequence');
@@ -3047,7 +3112,7 @@ App::patch('/v1/account/email')
],
contentType: ContentType::JSON
))
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->inject('requestTimestamp')
->inject('response')
@@ -3083,9 +3148,20 @@ App::patch('/v1/account/email')
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
}
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
$user
->setAttribute('email', $email)
->setAttribute('emailVerification', false) // After this user needs to confirm mail again
->setAttribute('emailCanonical', $emailCanonical?->getCanonical())
->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported())
->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate())
->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable())
->setAttribute('emailIsFree', $emailCanonical?->isFree())
;
if (empty($passwordUpdate)) {
@@ -3323,7 +3399,7 @@ App::post('/v1/account/recovery')
))
->label('abuse-limit', 10)
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['platforms', 'devKey'])
->inject('request')
->inject('response')
+14 -2
View File
@@ -10,7 +10,7 @@ use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Email as EmailValidator;
use Appwrite\Network\Validator\Redirect;
use Appwrite\Platform\Workers\Deletes;
use Appwrite\SDK\AuthType;
@@ -48,6 +48,7 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Email;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
@@ -469,7 +470,7 @@ App::post('/v1/teams/:teamId/memberships')
))
->label('abuse-limit', 10)
->param('teamId', '', new UID(), 'Team ID.')
->param('email', '', new Email(), 'Email of the new team member.', true)
->param('email', '', new EmailValidator(), 'Email of the new team member.', true)
->param('userId', '', new UID(), 'ID of the user to be added to a team.', true)
->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('roles', [], function (Document $project) {
@@ -568,6 +569,12 @@ App::post('/v1/teams/:teamId/memberships')
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
try {
$userId = ID::unique();
$invitee = $authorization->skip(fn () => $dbForProject->createDocument('users', new Document([
@@ -600,6 +607,11 @@ App::post('/v1/teams/:teamId/memberships')
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $name]),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
])));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
+35 -12
View File
@@ -16,7 +16,7 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Email as EmailValidator;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
@@ -49,6 +49,7 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Email;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
@@ -97,6 +98,12 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
}
}
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
$password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null;
$user = new Document([
'$id' => $userId,
@@ -124,6 +131,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $phone, $name]),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
]);
if ($hash === 'plaintext') {
@@ -208,7 +220,7 @@ App::post('/v1/users')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', null, new Email(), 'User email.', true)
->param('email', null, new EmailValidator(), 'User email.', true)
->param('phone', null, new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'Plain text user password. Must be at least 8 chars.', true, ['project', 'passwordsDictionary'])
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
@@ -243,7 +255,7 @@ App::post('/v1/users/bcrypt')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Bcrypt.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -278,7 +290,7 @@ App::post('/v1/users/md5')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using MD5.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -313,7 +325,7 @@ App::post('/v1/users/argon2')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Argon2.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -348,7 +360,7 @@ App::post('/v1/users/sha')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using SHA.')
->param('passwordVersion', '', new WhiteList(['sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']), "Optional SHA version used to hash password. Allowed values are: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512'", true)
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
@@ -390,7 +402,7 @@ App::post('/v1/users/phpass')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or pass the string `ID.unique()`to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using PHPass.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
@@ -425,7 +437,7 @@ App::post('/v1/users/scrypt')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Scrypt.')
->param('passwordSalt', '', new Text(128), 'Optional salt used to hash password.')
->param('passwordCpu', 8, new Integer(), 'Optional CPU cost used to hash password.')
@@ -473,7 +485,7 @@ App::post('/v1/users/scrypt-modified')
]
))
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Scrypt Modified.')
->param('passwordSalt', '', new Text(128), 'Salt used to hash password.')
->param('passwordSaltSeparator', '', new Text(128), 'Salt separator used to hash password.')
@@ -527,7 +539,7 @@ App::post('/v1/users/:userId/targets')
switch ($providerType) {
case 'email':
$validator = new Email();
$validator = new EmailValidator();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
@@ -1402,7 +1414,7 @@ App::patch('/v1/users/:userId/email')
]
))
->param('userId', '', new UID(), 'User ID.')
->param('email', '', new Email(allowEmpty: true), 'User email.')
->param('email', '', new EmailValidator(allowEmpty: true), 'User email.')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
@@ -1437,9 +1449,20 @@ App::patch('/v1/users/:userId/email')
$oldEmail = $user->getAttribute('email');
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
}
$user
->setAttribute('email', $email)
->setAttribute('emailVerification', false)
->setAttribute('emailCanonical', $emailCanonical?->getCanonical())
->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported())
->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate())
->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable())
->setAttribute('emailIsFree', $emailCanonical?->isFree())
;
try {
@@ -1700,7 +1723,7 @@ App::patch('/v1/users/:userId/targets/:targetId')
switch ($providerType) {
case 'email':
$validator = new Email();
$validator = new EmailValidator();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
+2
View File
@@ -271,6 +271,8 @@ const METRIC_SITES_ID_REQUESTS = 'sites.{siteInternalId}.requests';
const METRIC_SITES_ID_INBOUND = 'sites.{siteInternalId}.inbound';
const METRIC_SITES_ID_OUTBOUND = 'sites.{siteInternalId}.outbound';
const METRIC_AVATARS_SCREENSHOTS_GENERATED = 'avatars.screenshotsGenerated';
const METRIC_FUNCTIONS_RUNTIME = 'functions.runtimes.{runtime}';
const METRIC_SITES_FRAMEWORK = 'sites.frameworks.{framework}';
// Resource types
const RESOURCE_TYPE_PROJECTS = 'projects';
Generated
+20 -20
View File
@@ -161,16 +161,16 @@
},
{
"name": "appwrite/php-runtimes",
"version": "0.19.1",
"version": "0.19.2",
"source": {
"type": "git",
"url": "https://github.com/appwrite/runtimes.git",
"reference": "7bd0cc3cb97de625d7b07230bd91b121f88e72ae"
"reference": "e5c142519df5aced37de9c302971c29c079ce3d9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/runtimes/zipball/7bd0cc3cb97de625d7b07230bd91b121f88e72ae",
"reference": "7bd0cc3cb97de625d7b07230bd91b121f88e72ae",
"url": "https://api.github.com/repos/appwrite/runtimes/zipball/e5c142519df5aced37de9c302971c29c079ce3d9",
"reference": "e5c142519df5aced37de9c302971c29c079ce3d9",
"shasum": ""
},
"require": {
@@ -210,9 +210,9 @@
],
"support": {
"issues": "https://github.com/appwrite/runtimes/issues",
"source": "https://github.com/appwrite/runtimes/tree/0.19.1"
"source": "https://github.com/appwrite/runtimes/tree/0.19.2"
},
"time": "2025-05-27T07:12:56+00:00"
"time": "2025-11-11T13:44:44+00:00"
},
{
"name": "beberlei/assert",
@@ -3844,16 +3844,16 @@
},
{
"name": "utopia-php/database",
"version": "4.0.1",
"version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "c34cb23844944ebda952d0f2390bc9d0285f8c12"
"reference": "ce3986e82edc4968fe7eca152c0f5e0006059df9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/c34cb23844944ebda952d0f2390bc9d0285f8c12",
"reference": "c34cb23844944ebda952d0f2390bc9d0285f8c12",
"url": "https://api.github.com/repos/utopia-php/database/zipball/ce3986e82edc4968fe7eca152c0f5e0006059df9",
"reference": "ce3986e82edc4968fe7eca152c0f5e0006059df9",
"shasum": ""
},
"require": {
@@ -3896,9 +3896,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/4.0.1"
"source": "https://github.com/utopia-php/database/tree/4.1.0"
},
"time": "2025-11-05T03:39:09+00:00"
"time": "2025-11-12T03:43:25+00:00"
},
{
"name": "utopia-php/detector",
@@ -5383,16 +5383,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.5.1",
"version": "1.5.3",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "cd712674e34136f706e9170641ed6f4ce160e772"
"reference": "1a7a3b89147aa8c1bde5247f8eeb7e4832c6016d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/cd712674e34136f706e9170641ed6f4ce160e772",
"reference": "cd712674e34136f706e9170641ed6f4ce160e772",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/1a7a3b89147aa8c1bde5247f8eeb7e4832c6016d",
"reference": "1a7a3b89147aa8c1bde5247f8eeb7e4832c6016d",
"shasum": ""
},
"require": {
@@ -5428,9 +5428,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.5.1"
"source": "https://github.com/appwrite/sdk-generator/tree/1.5.3"
},
"time": "2025-11-04T09:55:47+00:00"
"time": "2025-11-10T09:50:41+00:00"
},
{
"name": "doctrine/annotations",
@@ -8897,7 +8897,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@@ -8921,5 +8921,5 @@
"platform-overrides": {
"php": "8.3"
},
"plugin-api-version": "2.2.0"
"plugin-api-version": "2.6.0"
}
@@ -0,0 +1,37 @@
<?php
use Appwrite\Client;
use Appwrite\Services\Avatars;
use Appwrite\Enums\Theme;
use Appwrite\Enums\Timezone;
use Appwrite\Enums\Output;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
->setProject('<YOUR_PROJECT_ID>') // Your project ID
->setSession(''); // The user session to authenticate with
$avatars = new Avatars($client);
$result = $avatars->getScreenshot(
url: 'https://example.com',
headers: [], // optional
viewportWidth: 1, // optional
viewportHeight: 1, // optional
scale: 0.1, // optional
theme: Theme::LIGHT(), // optional
userAgent: '<USER_AGENT>', // optional
fullpage: false, // optional
locale: '<LOCALE>', // optional
timezone: Timezone::AFRICAABIDJAN(), // optional
latitude: -90, // optional
longitude: -180, // optional
accuracy: 0, // optional
touch: false, // optional
permissions: [], // optional
sleep: 0, // optional
width: 0, // optional
height: 0, // optional
quality: -1, // optional
output: Output::JPG() // optional
);
@@ -3,6 +3,7 @@
use Appwrite\Client;
use Appwrite\Services\Databases;
use Appwrite\Enums\RelationshipType;
use Appwrite\Enums\RelationMutate;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Databases;
use Appwrite\Enums\RelationMutate;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Functions;
use Appwrite\Enums\ExecutionMethod;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,7 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Functions;
use Appwrite\Enums\;
use Appwrite\Enums\Runtime;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -14,7 +14,7 @@ $functions = new Functions($client);
$result = $functions->create(
functionId: '<FUNCTION_ID>',
name: '<NAME>',
runtime: ::NODE145(),
runtime: Runtime::NODE145(),
execute: ["any"], // optional
events: [], // optional
schedule: '', // optional
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Functions;
use Appwrite\Enums\DeploymentDownloadType;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Functions;
use Appwrite\Enums\Runtime;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -13,7 +14,7 @@ $functions = new Functions($client);
$result = $functions->update(
functionId: '<FUNCTION_ID>',
name: '<NAME>',
runtime: ::NODE145(), // optional
runtime: Runtime::NODE145(), // optional
execute: ["any"], // optional
events: [], // optional
schedule: '', // optional
@@ -2,7 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Health;
use Appwrite\Enums\;
use Appwrite\Enums\Name;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -12,6 +12,6 @@ $client = (new Client())
$health = new Health($client);
$result = $health->getFailedJobs(
name: ::V1DATABASE(),
name: Name::V1DATABASE(),
threshold: null // optional
);
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Messaging;
use Appwrite\Enums\MessagePriority;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Messaging;
use Appwrite\Enums\SmtpEncryption;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Messaging;
use Appwrite\Enums\MessagePriority;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Messaging;
use Appwrite\Enums\SmtpEncryption;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,8 +2,9 @@
use Appwrite\Client;
use Appwrite\Services\Sites;
use Appwrite\Enums\;
use Appwrite\Enums\;
use Appwrite\Enums\Framework;
use Appwrite\Enums\BuildRuntime;
use Appwrite\Enums\Adapter;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -15,15 +16,15 @@ $sites = new Sites($client);
$result = $sites->create(
siteId: '<SITE_ID>',
name: '<NAME>',
framework: ::ANALOG(),
buildRuntime: ::NODE145(),
framework: Framework::ANALOG(),
buildRuntime: BuildRuntime::NODE145(),
enabled: false, // optional
logging: false, // optional
timeout: 1, // optional
installCommand: '<INSTALL_COMMAND>', // optional
buildCommand: '<BUILD_COMMAND>', // optional
outputDirectory: '<OUTPUT_DIRECTORY>', // optional
adapter: ::STATIC(), // optional
adapter: Adapter::STATIC(), // optional
installationId: '<INSTALLATION_ID>', // optional
fallbackFile: '<FALLBACK_FILE>', // optional
providerRepositoryId: '<PROVIDER_REPOSITORY_ID>', // optional
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Sites;
use Appwrite\Enums\DeploymentDownloadType;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,7 +2,9 @@
use Appwrite\Client;
use Appwrite\Services\Sites;
use Appwrite\Enums\;
use Appwrite\Enums\Framework;
use Appwrite\Enums\BuildRuntime;
use Appwrite\Enums\Adapter;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -14,15 +16,15 @@ $sites = new Sites($client);
$result = $sites->update(
siteId: '<SITE_ID>',
name: '<NAME>',
framework: ::ANALOG(),
framework: Framework::ANALOG(),
enabled: false, // optional
logging: false, // optional
timeout: 1, // optional
installCommand: '<INSTALL_COMMAND>', // optional
buildCommand: '<BUILD_COMMAND>', // optional
outputDirectory: '<OUTPUT_DIRECTORY>', // optional
buildRuntime: ::NODE145(), // optional
adapter: ::STATIC(), // optional
buildRuntime: BuildRuntime::NODE145(), // optional
adapter: Adapter::STATIC(), // optional
fallbackFile: '<FALLBACK_FILE>', // optional
installationId: '<INSTALLATION_ID>', // optional
providerRepositoryId: '<PROVIDER_REPOSITORY_ID>', // optional
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Storage;
use Appwrite\Enums\Compression;
use Appwrite\Permission;
use Appwrite\Role;
@@ -20,7 +21,7 @@ $result = $storage->createBucket(
enabled: false, // optional
maximumFileSize: 1, // optional
allowedFileExtensions: [], // optional
compression: ::NONE(), // optional
compression: Compression::NONE(), // optional
encryption: false, // optional
antivirus: false // optional
);
@@ -2,6 +2,8 @@
use Appwrite\Client;
use Appwrite\Services\Storage;
use Appwrite\Enums\ImageGravity;
use Appwrite\Enums\ImageFormat;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Storage;
use Appwrite\Enums\Compression;
use Appwrite\Permission;
use Appwrite\Role;
@@ -20,7 +21,7 @@ $result = $storage->updateBucket(
enabled: false, // optional
maximumFileSize: 1, // optional
allowedFileExtensions: [], // optional
compression: ::NONE(), // optional
compression: Compression::NONE(), // optional
encryption: false, // optional
antivirus: false // optional
);
@@ -3,6 +3,7 @@
use Appwrite\Client;
use Appwrite\Services\TablesDB;
use Appwrite\Enums\RelationshipType;
use Appwrite\Enums\RelationMutate;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\TablesDB;
use Appwrite\Enums\RelationMutate;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
@@ -2,6 +2,7 @@
use Appwrite\Client;
use Appwrite\Services\Users;
use Appwrite\Enums\PasswordHash;
$client = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
+4
View File
@@ -1,5 +1,9 @@
# Change Log
## 11.1.1
* Fix duplicate `enums` during type generation by prefixing them with table name. For example, `enum MyEnum` will now be generated as `enum MyTableMyEnum` to avoid conflicts.
## 11.1.0
* Add `total` parameter to list queries allowing skipping counting rows in a table for improved performance
+10
View File
@@ -1,5 +1,15 @@
# Change Log
## 18.0.1
* Fix `TablesDB` service to use correct file name
## 18.0.0
* Fix duplicate methods issue (e.g., `updateMFA` and `updateMfa`) causing build and runtime errors
* Add support for `getScreenshot` method to `Avatars` service
* Add `Output`, `Theme` and `Timezone` enums
## 17.5.0
* Add `total` parameter to list queries allowing skipping counting rows in a table for improved performance
Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

+50
View File
@@ -6,6 +6,7 @@ use Appwrite\Migration\Migration;
use Exception;
use Throwable;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict;
@@ -132,6 +133,13 @@ class V23 extends Migration
}
$this->dbForProject->purgeCachedCollection($id);
break;
case 'migrations':
try {
$this->updateMigrateErrorSize();
} catch (\Throwable $th) {
Console::warning("Failed to migration error attribute size in collection {$id}: {$th->getMessage()}");
}
default:
break;
}
@@ -201,4 +209,46 @@ class V23 extends Migration
}
return $document;
}
/**
* Update migration attribute size
* @return void
*/
private function updateMigrateErrorSize(): void
{
if ($this->project->getId() === 'console') {
return;
}
// Read-modify-write from the live schema to avoid overwriting unrelated changes.
$migration = $this->dbForProject->getCollection('migrations');
$attributes = $migration->getAttribute('attributes', []);
$attrsArray = \array_map(fn (Document $doc) => $doc->getArrayCopy(), $attributes);
$errorsIdx = \array_search('errors', \array_column($attrsArray, '$id'));
if ($errorsIdx === false) {
Console::warning("Skipping: 'errors' attribute not found in migrations collection for project {$this->project->getId()}");
return;
}
$desiredSize = 1_000_000;
$migrationAttributes = Config::getParam('collections', [])['projects']['migrations']['attributes'] ?? [];
$migrationIndex = \array_search('errors', \array_column($migrationAttributes, '$id'));
if ($migrationIndex !== false && isset($migrationAttributes[$migrationIndex]['size'])) {
$desiredSize = (int) $migrationAttributes[$migrationIndex]['size'];
}
$currentSize = (int) ($attributes[$errorsIdx]['size'] ?? 0);
if ($currentSize === $desiredSize) {
Console::warning("Skipping: 'errors' attribute already of desired size {$desiredSize} in migrations collection for project {$this->project->getId()}");
return;
}
$attributes[$errorsIdx]['size'] = $desiredSize;
$migration->setAttribute('attributes', $attributes);
$this->dbForProject->updateDocument($migration->getCollection(), $migration->getId(), $migration);
$this->dbForProject->purgeCachedCollection('migrations');
}
}
+11 -8
View File
@@ -259,8 +259,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
}
if ($createRelease) {
Console::execute('git config --global user.email "$GIT_EMAIL"', stdin: '', stdout: '', stderr: '');
$releaseVersion = $language['version'];
$repoName = $language['gitUserName'] . '/' . $language['gitRepoName'];
@@ -429,16 +427,21 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
mkdir -p ' . $target . ' && \
cd ' . $target . ' && \
git init && \
git config core.ignorecase false && \
git config pull.rebase false && \
git remote add origin ' . $gitUrl . ' && \
git fetch origin && \
git checkout ' . $repoBranch . ' || git checkout -b ' . $repoBranch . ' && \
(git checkout -f ' . $repoBranch . ' 2>/dev/null || git checkout -b ' . $repoBranch . ') && \
git pull origin ' . $repoBranch . ' && \
git checkout ' . $gitBranch . ' || git checkout -b ' . $gitBranch . ' && \
git fetch origin ' . $gitBranch . ' || git push -u origin ' . $gitBranch . ' && \
git pull origin ' . $gitBranch . ' && \
find . -mindepth 1 ! -path "./.git*" -delete && \
(git checkout -f ' . $gitBranch . ' 2>/dev/null || git checkout -b ' . $gitBranch . ') && \
(git fetch origin ' . $gitBranch . ' 2>/dev/null || git push -u origin ' . $gitBranch . ') && \
git reset --hard origin/' . $gitBranch . ' 2>/dev/null || true && \
(test -d .github && cp -r .github /tmp/.github-backup-$$ || true) && \
git rm -rf --cached . && \
git clean -fdx -e .git -e .github && \
cp -r ' . $result . '/. ' . $target . '/ && \
git add . && \
(test -d /tmp/.github-backup-$$ && cp -r /tmp/.github-backup-$$/.github . && rm -rf /tmp/.github-backup-$$ || true) && \
git add -A && \
git commit -m "' . $message . '" && \
git push -u origin ' . $gitBranch . '
');
@@ -335,7 +335,11 @@ class StatsResources extends Action
$this->createStatsDocuments($region, str_replace("{resourceType}", RESOURCE_TYPE_FUNCTIONS, METRIC_RESOURCE_TYPE_DEPLOYMENTS), $deployments);
$this->createStatsDocuments($region, str_replace("{resourceType}", RESOURCE_TYPE_FUNCTIONS, METRIC_RESOURCE_TYPE_BUILDS), $deployments);
$this->foreachDocument($dbForProject, 'functions', [], function (Document $function) use ($dbForProject, $region) {
// Count runtimes
$runtimes = [];
$this->foreachDocument($dbForProject, 'functions', [], function (Document $function) use ($dbForProject, $region, &$runtimes) {
$functionDeploymentsStorage = $dbForProject->sum('deployments', 'sourceSize', [
Query::equal('resourceInternalId', [$function->getSequence()]),
Query::equal('resourceType', [RESOURCE_TYPE_FUNCTIONS]),
@@ -364,7 +368,19 @@ class StatsResources extends Action
});
$this->createStatsDocuments($region, str_replace(['{resourceType}','{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS,$function->getSequence()], METRIC_RESOURCE_TYPE_ID_BUILDS_STORAGE), $functionBuildsStorage);
// Runtimes count
$runtime = $function->getAttribute('runtime');
if (!empty($runtime)) {
$runtimes[$runtime] = ($runtimes[$runtime] ?? 0) + 1;
}
});
// Write runtimes counts
foreach ($runtimes as $runtime => $count) {
$this->createStatsDocuments($region, str_replace('{runtime}', $runtime, METRIC_FUNCTIONS_RUNTIME), $count);
}
}
protected function countForSites(Database $dbForProject, string $region)
@@ -385,7 +401,10 @@ class StatsResources extends Action
$this->createStatsDocuments($region, str_replace("{resourceType}", RESOURCE_TYPE_SITES, METRIC_RESOURCE_TYPE_DEPLOYMENTS), $deployments);
$this->createStatsDocuments($region, str_replace("{resourceType}", RESOURCE_TYPE_SITES, METRIC_RESOURCE_TYPE_BUILDS), $deployments);
$this->foreachDocument($dbForProject, 'sites', [], function (Document $site) use ($dbForProject, $region) {
// Count frameworks
$frameworks = [];
$this->foreachDocument($dbForProject, 'sites', [], function (Document $site) use ($dbForProject, $region, &$frameworks) {
$siteDeploymentsStorage = $dbForProject->sum('deployments', 'sourceSize', [
Query::equal('resourceInternalId', [$site->getSequence()]),
Query::equal('resourceType', [RESOURCE_TYPE_SITES]),
@@ -410,7 +429,18 @@ class StatsResources extends Action
]);
$this->createStatsDocuments($region, str_replace(['{resourceType}','{resourceInternalId}'], [RESOURCE_TYPE_SITES,$site->getSequence()], METRIC_RESOURCE_TYPE_ID_BUILDS_STORAGE), $siteBuildsStorage);
// Frameworks count
$framework = $site->getAttribute('framework');
if (!empty($framework)) {
$frameworks[$framework] = ($frameworks[$framework] ?? 0) + 1;
}
});
// Write frameworks counts
foreach ($frameworks as $framework => $count) {
$this->createStatsDocuments($region, str_replace('{framework}', $framework, METRIC_SITES_FRAMEWORK), $count);
}
}
protected function createStatsDocuments(string $region, string $metric, int $value)
@@ -41,6 +41,11 @@ trait AccountBase
$this->assertNotEmpty($response['body']['accessedAt']);
$this->assertArrayHasKey('targets', $response['body']);
$this->assertEquals($email, $response['body']['targets'][0]['identifier']);
$this->assertArrayNotHasKey('emailCanonical', $response['body']);
$this->assertArrayNotHasKey('emailIsFree', $response['body']);
$this->assertArrayNotHasKey('emailIsDisposable', $response['body']);
$this->assertArrayNotHasKey('emailIsCorporate', $response['body']);
$this->assertArrayNotHasKey('emailIsCanonical', $response['body']);
/**
* Test for FAILURE