mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch '1.9.x' into feat-dedicated-db
This commit is contained in:
@@ -474,19 +474,26 @@ Http::delete('/v1/account')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForDeletes')
|
||||
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes) {
|
||||
->inject('authorization')
|
||||
->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes, Authorization $authorization) {
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
// get all memberships
|
||||
$memberships = $user->getAttribute('memberships', []);
|
||||
foreach ($memberships as $membership) {
|
||||
// prevent deletion if at least one active membership
|
||||
if ($membership->getAttribute('confirm', false)) {
|
||||
throw new Exception(Exception::USER_DELETION_PROHIBITED);
|
||||
if (!$membership->getAttribute('confirm', false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$team = $dbForProject->getDocument('teams', $membership->getAttribute('teamId'));
|
||||
if ($team->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Team is left as-is — we don't promote non-owner members to owner.
|
||||
// Orphan teams are cleaned up later by Cloud's inactive project cleanup.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -117,7 +117,9 @@ use Appwrite\Utopia\Response\Model\Project;
|
||||
use Appwrite\Utopia\Response\Model\Provider;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepository;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryFrameworkList;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntimeList;
|
||||
use Appwrite\Utopia\Response\Model\ResourceToken;
|
||||
use Appwrite\Utopia\Response\Model\Row;
|
||||
use Appwrite\Utopia\Response\Model\Rule;
|
||||
@@ -190,8 +192,8 @@ Response::setModel(new BaseList('Site Templates List', Response::MODEL_TEMPLATE_
|
||||
Response::setModel(new BaseList('Functions List', Response::MODEL_FUNCTION_LIST, 'functions', Response::MODEL_FUNCTION));
|
||||
Response::setModel(new BaseList('Function Templates List', Response::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', Response::MODEL_TEMPLATE_FUNCTION));
|
||||
Response::setModel(new BaseList('Installations List', Response::MODEL_INSTALLATION_LIST, 'installations', Response::MODEL_INSTALLATION));
|
||||
Response::setModel(new BaseList('Framework Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, 'frameworkProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK));
|
||||
Response::setModel(new BaseList('Runtime Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, 'runtimeProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME));
|
||||
Response::setModel(new ProviderRepositoryFrameworkList());
|
||||
Response::setModel(new ProviderRepositoryRuntimeList());
|
||||
Response::setModel(new BaseList('Branches List', Response::MODEL_BRANCH_LIST, 'branches', Response::MODEL_BRANCH));
|
||||
Response::setModel(new BaseList('Frameworks List', Response::MODEL_FRAMEWORK_LIST, 'frameworks', Response::MODEL_FRAMEWORK));
|
||||
Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, 'runtimes', Response::MODEL_RUNTIME));
|
||||
|
||||
@@ -120,7 +120,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_SMTP_HOST
|
||||
- _APP_SMTP_PORT
|
||||
- _APP_SMTP_SECURE
|
||||
@@ -256,7 +255,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_USAGE_STATS
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
@@ -287,7 +285,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-webhooks:
|
||||
@@ -315,7 +312,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -356,7 +352,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_STORAGE_DEVICE
|
||||
- _APP_STORAGE_S3_ACCESS_KEY
|
||||
- _APP_STORAGE_S3_SECRET
|
||||
@@ -416,7 +411,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-builds:
|
||||
@@ -453,7 +447,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_VCS_GITHUB_APP_NAME
|
||||
- _APP_VCS_GITHUB_PRIVATE_KEY
|
||||
@@ -529,7 +522,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
|
||||
appwrite-worker-executions:
|
||||
@@ -592,7 +584,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_FUNCTIONS_TIMEOUT
|
||||
- _APP_SITES_TIMEOUT
|
||||
- _APP_COMPUTE_BUILD_TIMEOUT
|
||||
@@ -630,7 +621,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -673,7 +663,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_SMS_FROM
|
||||
- _APP_SMS_PROVIDER
|
||||
@@ -734,7 +723,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
|
||||
@@ -773,7 +761,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_CACHE
|
||||
@@ -806,7 +793,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -839,7 +825,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -871,7 +856,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
@@ -907,7 +891,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
appwrite-task-scheduler-executions:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
@@ -936,7 +919,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
appwrite-task-scheduler-messages:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
@@ -965,7 +947,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_DB_ADAPTER
|
||||
|
||||
<?php if ($enableAssistant): ?>
|
||||
appwrite-assistant:
|
||||
@@ -1068,13 +1049,12 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
image: mongo:8.2.5
|
||||
container_name: appwrite-mongodb
|
||||
<<: *x-logging
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-mongodb:/data/db
|
||||
- appwrite-mongodb-keyfile:/data/keyfile
|
||||
ports:
|
||||
- "27017:27017"
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=root
|
||||
- MONGO_INITDB_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
|
||||
@@ -1205,7 +1185,6 @@ volumes:
|
||||
<?php elseif ($dbService === 'mongodb'): ?>
|
||||
appwrite-mongodb:
|
||||
appwrite-mongodb-keyfile:
|
||||
appwrite-mongodb-config:
|
||||
<?php endif; ?>
|
||||
appwrite-redis:
|
||||
appwrite-cache:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Protocols\Status;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
@@ -33,8 +34,8 @@ class Update extends Action
|
||||
->desc('Update project protocol status')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('event', 'protocols.[protocol].update')
|
||||
->label('audits.event', 'project.protocols.[protocol].update')
|
||||
->label('event', 'protocols.[protocolId].update')
|
||||
->label('audits.event', 'project.protocols.[protocolId].update')
|
||||
->label('audits.resource', 'project.protocols/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
@@ -57,6 +58,7 @@ class Update extends Action
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -66,7 +68,8 @@ class Update extends Action
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
Event $queueForEvents,
|
||||
): void {
|
||||
$protocols = $project->getAttribute('apis', []);
|
||||
$protocols[$protocolId] = $enabled;
|
||||
@@ -75,6 +78,8 @@ class Update extends Action
|
||||
'apis' => $protocols,
|
||||
])));
|
||||
|
||||
$queueForEvents->setParam('protocolId', $protocolId);
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\Services\Status;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
@@ -33,8 +34,8 @@ class Update extends Action
|
||||
->desc('Update project service status')
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'project.write')
|
||||
->label('event', 'services.[service].update')
|
||||
->label('audits.event', 'project.services.[service].update')
|
||||
->label('event', 'services.[serviceId].update')
|
||||
->label('audits.event', 'project.services.[serviceId].update')
|
||||
->label('audits.resource', 'project.services/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
@@ -57,6 +58,7 @@ class Update extends Action
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -66,7 +68,8 @@ class Update extends Action
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization
|
||||
Authorization $authorization,
|
||||
Event $queueForEvents
|
||||
): void {
|
||||
$services = $project->getAttribute('services', []);
|
||||
$services[$serviceId] = $enabled;
|
||||
@@ -75,6 +78,8 @@ class Update extends Action
|
||||
'services' => $services,
|
||||
])));
|
||||
|
||||
$queueForEvents->setParam('serviceId', $serviceId);
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@ class Delete extends Action
|
||||
if ($team->getAttribute('userInternalId') === $membership->getAttribute('userInternalId')) {
|
||||
$membership = $dbForProject->findOne('memberships', [
|
||||
Query::equal('teamInternalId', [$team->getSequence()]),
|
||||
Query::equal('confirm', [true]),
|
||||
]);
|
||||
|
||||
if (!$membership->isEmpty()) {
|
||||
|
||||
+1
@@ -307,6 +307,7 @@ class Create extends Action
|
||||
];
|
||||
}
|
||||
|
||||
$output->setAttribute('type', $type);
|
||||
$output->setAttribute('variables', $variables);
|
||||
|
||||
$response->dynamic($output, $type === 'framework' ? Response::MODEL_DETECTION_FRAMEWORK : Response::MODEL_DETECTION_RUNTIME);
|
||||
|
||||
@@ -313,6 +313,7 @@ class XList extends Action
|
||||
}, $repos);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'type' => $type,
|
||||
$type === 'framework' ? 'frameworkProviderRepositories' : 'runtimeProviderRepositories' => $repos,
|
||||
'total' => $total,
|
||||
]), ($type === 'framework') ? Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST : Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST);
|
||||
|
||||
@@ -263,6 +263,182 @@ abstract class Format
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Model> $models
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
protected function getDiscriminator(array $models, string $refPrefix): ?array
|
||||
{
|
||||
if (\count($models) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$candidateKeys = \array_keys($models[0]->conditions);
|
||||
|
||||
foreach (\array_slice($models, 1) as $model) {
|
||||
$candidateKeys = \array_values(\array_intersect($candidateKeys, \array_keys($model->conditions)));
|
||||
}
|
||||
|
||||
if (empty($candidateKeys)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($candidateKeys as $key) {
|
||||
$mapping = [];
|
||||
$isValid = true;
|
||||
|
||||
foreach ($models as $model) {
|
||||
$rules = $model->getRules();
|
||||
$condition = $model->conditions[$key] ?? null;
|
||||
|
||||
if (!isset($rules[$key]) || ($rules[$key]['required'] ?? false) !== true) {
|
||||
$isValid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!\is_array($condition)) {
|
||||
if (!\is_scalar($condition)) {
|
||||
$isValid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
$values = [$condition];
|
||||
} else {
|
||||
if ($condition === []) {
|
||||
$isValid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
$values = $condition;
|
||||
$hasInvalidValue = false;
|
||||
|
||||
foreach ($values as $value) {
|
||||
if (!\is_scalar($value)) {
|
||||
$hasInvalidValue = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasInvalidValue) {
|
||||
$isValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($rules[$key]['enum']) && \is_array($rules[$key]['enum'])) {
|
||||
$values = \array_values(\array_filter(
|
||||
$values,
|
||||
fn (mixed $value) => \in_array($value, $rules[$key]['enum'], true)
|
||||
));
|
||||
}
|
||||
|
||||
if ($values === []) {
|
||||
$isValid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
$ref = $refPrefix . $model->getType();
|
||||
|
||||
foreach ($values as $value) {
|
||||
$mappingKey = \is_bool($value) ? ($value ? 'true' : 'false') : (string) $value;
|
||||
|
||||
if (isset($mapping[$mappingKey]) && $mapping[$mappingKey] !== $ref) {
|
||||
$isValid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
$mapping[$mappingKey] = $ref;
|
||||
}
|
||||
|
||||
if (!$isValid) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isValid || $mapping === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return [
|
||||
'propertyName' => $key,
|
||||
'mapping' => $mapping,
|
||||
];
|
||||
}
|
||||
|
||||
// Single-key failed — try compound discriminator
|
||||
return $this->getCompoundDiscriminator($models, $refPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Model> $models
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function getCompoundDiscriminator(array $models, string $refPrefix): ?array
|
||||
{
|
||||
$allKeys = [];
|
||||
foreach ($models as $model) {
|
||||
foreach (\array_keys($model->conditions) as $key) {
|
||||
if (!\in_array($key, $allKeys, true)) {
|
||||
$allKeys[] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($allKeys) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$primaryKey = $allKeys[0];
|
||||
$primaryMapping = [];
|
||||
$compoundMapping = [];
|
||||
|
||||
foreach ($models as $model) {
|
||||
$rules = $model->getRules();
|
||||
$conditions = [];
|
||||
|
||||
foreach ($model->conditions as $key => $condition) {
|
||||
if (!isset($rules[$key]) || ($rules[$key]['required'] ?? false) !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!\is_scalar($condition)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$conditions[$key] = \is_bool($condition) ? ($condition ? 'true' : 'false') : (string) $condition;
|
||||
}
|
||||
|
||||
if (empty($conditions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ref = $refPrefix . $model->getType();
|
||||
$compoundMapping[$ref] = $conditions;
|
||||
|
||||
// Best-effort single-key mapping — last model with this value wins (fallback)
|
||||
if (isset($conditions[$primaryKey])) {
|
||||
$primaryMapping[$conditions[$primaryKey]] = $ref;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify compound uniqueness
|
||||
$seen = [];
|
||||
foreach ($compoundMapping as $conditions) {
|
||||
$sig = \json_encode($conditions, JSON_THROW_ON_ERROR);
|
||||
if (isset($seen[$sig])) {
|
||||
return null;
|
||||
}
|
||||
$seen[$sig] = true;
|
||||
}
|
||||
|
||||
return \array_filter([
|
||||
'propertyName' => $primaryKey,
|
||||
'mapping' => !empty($primaryMapping) ? $primaryMapping : null,
|
||||
'x-propertyNames' => $allKeys,
|
||||
'x-mapping' => $compoundMapping,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getRequestEnumName(string $service, string $method, string $param): ?string
|
||||
{
|
||||
/* `$service` is `$namespace` */
|
||||
|
||||
@@ -316,9 +316,10 @@ class OpenAPI3 extends Format
|
||||
'description' => $modelDescription,
|
||||
'content' => [
|
||||
$produces => [
|
||||
'schema' => [
|
||||
'oneOf' => \array_map(fn ($m) => ['$ref' => '#/components/schemas/' . $m->getType()], $model)
|
||||
],
|
||||
'schema' => \array_filter([
|
||||
'oneOf' => \array_map(fn ($m) => ['$ref' => '#/components/schemas/' . $m->getType()], $model),
|
||||
'discriminator' => $this->getDiscriminator($model, '#/components/schemas/'),
|
||||
]),
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -900,18 +901,30 @@ class OpenAPI3 extends Format
|
||||
$rule['type'] = ($rule['type']) ? $rule['type'] : 'none';
|
||||
|
||||
if (\is_array($rule['type'])) {
|
||||
$resolvedModels = \array_map(function (string $type) {
|
||||
foreach ($this->models as $model) {
|
||||
if ($model->getType() === $type) {
|
||||
return $model;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException("Unresolved model '{$type}'. Ensure the model is registered.");
|
||||
}, $rule['type']);
|
||||
|
||||
if ($rule['array']) {
|
||||
$items = [
|
||||
$items = \array_filter([
|
||||
'anyOf' => \array_map(function ($type) {
|
||||
return ['$ref' => '#/components/schemas/' . $type];
|
||||
}, $rule['type'])
|
||||
];
|
||||
}, $rule['type']),
|
||||
'discriminator' => $this->getDiscriminator($resolvedModels, '#/components/schemas/'),
|
||||
]);
|
||||
} else {
|
||||
$items = [
|
||||
$items = \array_filter([
|
||||
'oneOf' => \array_map(function ($type) {
|
||||
return ['$ref' => '#/components/schemas/' . $type];
|
||||
}, $rule['type'])
|
||||
];
|
||||
}, $rule['type']),
|
||||
'discriminator' => $this->getDiscriminator($resolvedModels, '#/components/schemas/'),
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$items = [
|
||||
|
||||
@@ -322,11 +322,12 @@ class Swagger2 extends Format
|
||||
}
|
||||
$temp['responses'][(string)$response->getCode() ?? '500'] = [
|
||||
'description' => $modelDescription,
|
||||
'schema' => [
|
||||
'schema' => \array_filter([
|
||||
'x-oneOf' => \array_map(function ($m) {
|
||||
return ['$ref' => '#/definitions/' . $m->getType()];
|
||||
}, $model)
|
||||
],
|
||||
}, $model),
|
||||
'x-discriminator' => $this->getDiscriminator($model, '#/definitions/'),
|
||||
]),
|
||||
];
|
||||
} else {
|
||||
// Response definition using one type
|
||||
@@ -880,14 +881,27 @@ class Swagger2 extends Format
|
||||
$rule['type'] = ($rule['type']) ?: 'none';
|
||||
|
||||
if (\is_array($rule['type'])) {
|
||||
$resolvedModels = \array_map(function (string $type) {
|
||||
foreach ($this->models as $model) {
|
||||
if ($model->getType() === $type) {
|
||||
return $model;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException("Unresolved model '{$type}'. Ensure the model is registered.");
|
||||
}, $rule['type']);
|
||||
$xDiscriminator = $this->getDiscriminator($resolvedModels, '#/definitions/');
|
||||
|
||||
if ($rule['array']) {
|
||||
$items = [
|
||||
'x-anyOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type'])
|
||||
];
|
||||
$items = \array_filter([
|
||||
'x-anyOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type']),
|
||||
'x-discriminator' => $xDiscriminator,
|
||||
]);
|
||||
} else {
|
||||
$items = [
|
||||
'x-oneOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type'])
|
||||
];
|
||||
$items = \array_filter([
|
||||
'x-oneOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type']),
|
||||
'x-discriminator' => $xDiscriminator,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$items = [
|
||||
|
||||
@@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class AlgoArgon2 extends Model
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'argon2',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// No options if imported. If hashed by Appwrite, following configuration is available:
|
||||
|
||||
@@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class AlgoBcrypt extends Model
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'bcrypt',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// No options, because this can only be imported, and verifying doesnt require any configuration
|
||||
|
||||
@@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class AlgoMd5 extends Model
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'md5',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// No options, because this can only be imported, and verifying doesnt require any configuration
|
||||
|
||||
@@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class AlgoPhpass extends Model
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'phpass',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// No options, because this can only be imported, and verifying doesnt require any configuration
|
||||
|
||||
@@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class AlgoScrypt extends Model
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'scrypt',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
|
||||
@@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class AlgoScryptModified extends Model
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'scryptMod',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
|
||||
@@ -7,6 +7,10 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class AlgoSha extends Model
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'sha',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// No options, because this can only be imported, and verifying doesnt require any configuration
|
||||
|
||||
@@ -7,9 +7,16 @@ use Appwrite\Utopia\Response\Model;
|
||||
|
||||
abstract class Detection extends Model
|
||||
{
|
||||
public function __construct()
|
||||
public function __construct(string $type)
|
||||
{
|
||||
$this
|
||||
->addRule('type', [
|
||||
'type' => self::TYPE_ENUM,
|
||||
'description' => 'Repository detection type.',
|
||||
'default' => $type,
|
||||
'example' => $type,
|
||||
'enum' => [$type],
|
||||
])
|
||||
->addRule('variables', [
|
||||
'type' => Response::MODEL_DETECTION_VARIABLE,
|
||||
'description' => 'Environment variables found in .env files',
|
||||
|
||||
@@ -8,7 +8,11 @@ class DetectionFramework extends Detection
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->conditions = [
|
||||
'type' => 'framework',
|
||||
];
|
||||
|
||||
parent::__construct('framework');
|
||||
|
||||
$this
|
||||
->addRule('framework', [
|
||||
|
||||
@@ -8,7 +8,11 @@ class DetectionRuntime extends Detection
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->conditions = [
|
||||
'type' => 'runtime',
|
||||
];
|
||||
|
||||
parent::__construct('runtime');
|
||||
|
||||
$this
|
||||
->addRule('runtime', [
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
|
||||
class ProviderRepositoryFrameworkList extends BaseList
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'framework',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(
|
||||
'Framework Provider Repositories List',
|
||||
Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST,
|
||||
'frameworkProviderRepositories',
|
||||
Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK
|
||||
);
|
||||
|
||||
$this->addRule('type', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Provider repository list type.',
|
||||
'default' => 'framework',
|
||||
'example' => 'framework',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
|
||||
class ProviderRepositoryRuntimeList extends BaseList
|
||||
{
|
||||
public array $conditions = [
|
||||
'type' => 'runtime',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(
|
||||
'Runtime Provider Repositories List',
|
||||
Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST,
|
||||
'runtimeProviderRepositories',
|
||||
Response::MODEL_PROVIDER_REPOSITORY_RUNTIME
|
||||
);
|
||||
|
||||
$this->addRule('type', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Provider repository list type.',
|
||||
'default' => 'runtime',
|
||||
'example' => 'runtime',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,12 @@ class AccountConsoleClientTest extends Scope
|
||||
use ProjectConsole;
|
||||
use SideClient;
|
||||
|
||||
public function testDeleteAccount(): void
|
||||
/**
|
||||
* Test that account deletion succeeds even with active team memberships.
|
||||
* When the user is the sole owner and only member of a team, the team
|
||||
* should be cleaned up automatically.
|
||||
*/
|
||||
public function testDeleteAccountWithMembership(): void
|
||||
{
|
||||
$email = uniqid() . 'user@localhost.test';
|
||||
$password = 'password';
|
||||
@@ -46,7 +51,7 @@ class AccountConsoleClientTest extends Scope
|
||||
|
||||
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
|
||||
|
||||
// create team
|
||||
// Create team — user becomes sole owner and only member
|
||||
$team = $this->client->call(Client::METHOD_POST, '/teams', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
@@ -58,7 +63,51 @@ class AccountConsoleClientTest extends Scope
|
||||
]);
|
||||
$this->assertEquals($team['headers']['status-code'], 201);
|
||||
|
||||
$teamId = $team['body']['$id'];
|
||||
// Account deletion should succeed even with active membership
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that account deletion works when the user has no team memberships.
|
||||
*/
|
||||
public function testDeleteAccountWithoutMembership(): void
|
||||
{
|
||||
$email = uniqid() . 'user@localhost.test';
|
||||
$password = 'password';
|
||||
$name = 'User Name';
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'userId' => ID::unique(),
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'name' => $name,
|
||||
]);
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 201);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]), [
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 201);
|
||||
|
||||
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
@@ -67,27 +116,7 @@ class AccountConsoleClientTest extends Scope
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 400);
|
||||
|
||||
// DELETE TEAM
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/teams/' . $teamId, array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
$this->assertEquals($response['headers']['status-code'], 204);
|
||||
|
||||
$this->assertEventually(function () use ($session) {
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
|
||||
]));
|
||||
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
}, 10_000, 500);
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testSessionAlert(): void
|
||||
|
||||
Reference in New Issue
Block a user