Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions app/config/collections/platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,17 @@
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('onboarding'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65536,
'signed' => true,
'required' => false,
'default' => [],
'array' => false,
'filters' => ['json'],
],
Comment on lines +346 to +355

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 No database migration for the new onboarding attribute

The onboarding attribute is added to the platform.php schema definition, but this PR does not include a corresponding migration class (e.g., V25.php). Existing deployments that upgrade will not have the onboarding column in their projects collection. Any attempt by the shutdown hook or the Update endpoint to call updateDocument('projects', …, new Document(['onboarding' => …])) on an upgraded installation will fail at the database layer. New fresh installs are unaffected because platform.php is consumed at collection-creation time, not on upgrade.

[
'$id' => 'status',
'type' => Database::VAR_STRING,
Expand Down
30 changes: 30 additions & 0 deletions app/config/onboarding.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/**
* Project onboarding: each stage maps to an SDK method key (namespace + method name, same as Appwrite\SDK\Method).
* The sdk index is built once for O(1) lookup in the API shutdown hook.
*/
$stages = [
[
'id' => 'create_database',
'sdk' => 'databases.create',
],
[
'id' => 'create_bucket',
'sdk' => 'storage.createBucket',
],
[
'id' => 'create_function',
'sdk' => 'functions.create',
],
];

$sdkIndex = [];
foreach ($stages as $stage) {
$sdkIndex[$stage['sdk']] = $stage['id'];
}

return [
'stages' => $stages,
'sdkIndex' => $sdkIndex,
];
2 changes: 2 additions & 0 deletions app/config/roles.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@
'tokens.write',
'schedules.read',
'schedules.write',
'stages.read',
'stages.write',
];

return [
Expand Down
6 changes: 6 additions & 0 deletions app/config/scopes/project.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@
'schedules.write' => [
'description' => 'Access to create, update, and delete your project\'s schedules',
],
'stages.read' => [
'description' => 'Access to read your project\'s stages',
],
'stages.write' => [
'description' => 'Access to update your project\'s stages',
],
'migrations.read' => [
'description' => 'Access to read your project\'s migrations',
],
Expand Down
62 changes: 61 additions & 1 deletion app/controllers/shared/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -800,16 +800,76 @@
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('authorization')
->inject('timelimit')
->inject('eventProcessor')
->inject('bus')
->inject('apiKey')
->inject('mode')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Database $dbForPlatform, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {

$responsePayload = $response->getPayload();

/**
* Persist completed stage when the route matches a configured SDK method (stored on project as `onboarding`: stageId → row).
*/
if ($response->getStatusCode() >= 200 && $response->getStatusCode() < 300 && $project->getId() !== 'console') {
$sdkLabel = $utopia->getRoute()?->getLabel('sdk', false);
$stageId = null;
if ($sdkLabel !== false && $sdkLabel !== null) {
$sdkIndex = Config::getParam('onboarding', [])['sdkIndex'] ?? [];
foreach ($sdkLabel instanceof Method ? [$sdkLabel] : (\is_array($sdkLabel) ? $sdkLabel : []) as $sdkMethod) {
if ($sdkMethod instanceof Method && isset($sdkIndex[$k = $sdkMethod->getNamespace() . '.' . $sdkMethod->getMethodName()])) {
$stageId = $sdkIndex[$k];
break;
}
}
}
if ($stageId !== null) {
$byStageId = $project->getAttribute('onboarding', []);
if (! \is_array($byStageId)) {
$byStageId = [];
}
$done = \is_array($byStageId[$stageId] ?? null) ? ($byStageId[$stageId]['status'] ?? '') : '';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functional Medium

After a client skips stage create_database via PATCH /v1/projects/{projectId}/stages/create_database, the next successful POST /v1/databases still overwrites that skipped row to completed, breaking the cross-endpoint contract that skipped stages remain skipped.

Before marking a route-derived stage complete, reload the latest project document from dbForPlatform and honor any existing skipped status, or make the stage update endpoint and shutdown hook use a shared compare-and-swap helper to prevent stale in-memory $project data from overriding prior writes.

Prompt for AI assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert php developer with deep knowledge of security, performance, and best practices.

### Context

File: app/controllers/shared/api.php
Lines: 834-834
Issue Type: functional-medium
Severity: medium

Issue Description:
After a client skips stage `create_database` via `PATCH /v1/projects/{projectId}/stages/create_database`, the next successful `POST /v1/databases` still overwrites that skipped row to `completed`, breaking the cross-endpoint contract that skipped stages remain skipped.

Current Code:
$done = \is_array($byStageId[$stageId] ?? null) ? ($byStageId[$stageId]['status'] ?? '') : '';
if ($done !== ONBOARDING_STATUS_COMPLETED && $done !== ONBOARDING_STATUS_SKIPPED) {
    $actorType = ($apiKey !== null && $apiKey->getRole() === User::ROLE_APPS)
        ? match ($apiKey->getType()) {
            API_KEY_ACCOUNT => ACTIVITY_TYPE_KEY_ACCOUNT,

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow php best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira

if ($done !== ONBOARDING_STATUS_COMPLETED && $done !== ONBOARDING_STATUS_SKIPPED) {
$actorType = ($apiKey !== null && $apiKey->getRole() === User::ROLE_APPS)
? match ($apiKey->getType()) {
API_KEY_ACCOUNT => ACTIVITY_TYPE_KEY_ACCOUNT,
API_KEY_ORGANIZATION => ACTIVITY_TYPE_KEY_ORGANIZATION,
API_KEY_STANDARD, API_KEY_DYNAMIC => ACTIVITY_TYPE_KEY_PROJECT,
default => ACTIVITY_TYPE_KEY_PROJECT,
}
: (! $user->isEmpty()
? ($mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER)
: ACTIVITY_TYPE_GUEST);
$byStageId[$stageId] = [

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functional High

After PATCH /v1/projects/{projectId}/stages/create_database with {"skip":true}, a later successful databases.create call still overwrites that skipped stage to completed because this hook updates the stale $project snapshot instead of reloading onboarding state first.

Before computing $done and writing completion, reload the current project document from dbForPlatform (or perform a conditional/merge update) so an already-skipped stage is not reverted by a later request using an older injected $project instance.

Prompt for AI assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert php developer with deep knowledge of security, performance, and best practices.

### Context

File: app/controllers/shared/api.php
Lines: 846-846
Issue Type: functional-high
Severity: high

Issue Description:
After PATCH /v1/projects/{projectId}/stages/create_database with {"skip":true}, a later successful databases.create call still overwrites that skipped stage to completed because this hook updates the stale $project snapshot instead of reloading onboarding state first.

Current Code:
$byStageId[$stageId] = [
    'status' => ONBOARDING_STATUS_COMPLETED,
    'at' => DateTime::now(),
    'actorType' => $actorType,
];

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow php best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira

'status' => ONBOARDING_STATUS_COMPLETED,
'at' => DateTime::now(),
'actorType' => $actorType,
];
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'onboarding' => $byStageId,
])));

$queueForRealtime->reset();
$queueForRealtime
->setProject($project)
->setSubscribers(['console'])
->setEvent('projects.[projectId].stages.[stageId].complete')
->setParam('projectId', $project->getId())
->setParam('stageId', $stageId)
->setPayload([
'stageId' => $stageId,
'status' => ONBOARDING_STATUS_COMPLETED,
'at' => $byStageId[$stageId]['at'],
'actorType' => $actorType,
])
->trigger();
}
}
}

if (! empty($queueForEvents->getEvent())) {
if (empty($queueForEvents->getPayload())) {
$queueForEvents->setPayload($responsePayload);
Expand Down
1 change: 1 addition & 0 deletions app/init/configs.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
Config::load('organizationScopes', __DIR__ . '/../config/scopes/organization.php', $configAdapter);
Config::load('accountScopes', __DIR__ . '/../config/scopes/account.php', $configAdapter);
Config::load('services', __DIR__ . '/../config/services.php', $configAdapter); // List of services
Config::load('onboarding', __DIR__ . '/../config/onboarding.php', $configAdapter); // Project onboarding stages → routes
Config::load('variables', __DIR__ . '/../config/variables.php', $configAdapter); // List of env variables
Config::load('regions', __DIR__ . '/../config/regions.php', $configAdapter); // List of available regions
Config::load('avatar-browsers', __DIR__ . '/../config/avatars/browsers.php', $configAdapter);
Expand Down
6 changes: 6 additions & 0 deletions app/init/constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@
const ACTIVITY_TYPE_KEY_ACCOUNT = 'keyAccount';
const ACTIVITY_TYPE_KEY_ORGANIZATION = 'keyOrganization';

/**
* Project onboarding stage status (stored per stage id under project.onboarding JSON).
*/
const ONBOARDING_STATUS_COMPLETED = 'completed';
const ONBOARDING_STATUS_SKIPPED = 'skipped';

/**
* MFA
*/
Expand Down
3 changes: 3 additions & 0 deletions app/init/models.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
use Appwrite\Utopia\Response\Model\Session;
use Appwrite\Utopia\Response\Model\Site;
use Appwrite\Utopia\Response\Model\Specification;
use Appwrite\Utopia\Response\Model\Stage;
use Appwrite\Utopia\Response\Model\Subscriber;
use Appwrite\Utopia\Response\Model\Table;
use Appwrite\Utopia\Response\Model\Target;
Expand Down Expand Up @@ -212,6 +213,7 @@
Response::setModel(new BaseList('Status List', Response::MODEL_HEALTH_STATUS_LIST, 'statuses', Response::MODEL_HEALTH_STATUS));
Response::setModel(new BaseList('Rule List', Response::MODEL_PROXY_RULE_LIST, 'rules', Response::MODEL_PROXY_RULE));
Response::setModel(new BaseList('Schedules List', Response::MODEL_SCHEDULE_LIST, 'schedules', Response::MODEL_SCHEDULE));
Response::setModel(new BaseList('Stages List', Response::MODEL_STAGE_LIST, 'stages', Response::MODEL_STAGE, false, false));
Response::setModel(new BaseList('Locale codes list', Response::MODEL_LOCALE_CODE_LIST, 'localeCodes', Response::MODEL_LOCALE_CODE));
Response::setModel(new BaseList('Provider list', Response::MODEL_PROVIDER_LIST, 'providers', Response::MODEL_PROVIDER));
Response::setModel(new BaseList('Message list', Response::MODEL_MESSAGE_LIST, 'messages', Response::MODEL_MESSAGE));
Expand Down Expand Up @@ -373,6 +375,7 @@
Response::setModel(new Specification());
Response::setModel(new Rule());
Response::setModel(new Schedule());
Response::setModel(new Stage());
Response::setModel(new TemplateSMS());
Response::setModel(new TemplateEmail());
Response::setModel(new ConsoleVariables());
Expand Down
1 change: 1 addition & 0 deletions docs/references/projects/list-stages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Get the onboarding stages for the current project, including each stage’s SDK method key and status (for example pending, completed, or skipped).
1 change: 1 addition & 0 deletions docs/references/projects/update-stage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update an onboarding stage for the current project. Use this endpoint to skip a stage or leave it unchanged without performing the related API action.
150 changes: 150 additions & 0 deletions src/Appwrite/Platform/Modules/Projects/Http/Stages/Update.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php

namespace Appwrite\Platform\Modules\Projects\Http\Stages;

use Appwrite\Auth\Key;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;

class Update extends Action
{
use HTTP;

public static function getName(): string
{
return 'updateStage';
}

public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/projects/:projectId/stages/:stageId')
->desc('Update stage')
->groups(['api', 'projects'])
->label('scope', 'stages.write')
->label('audits.event', 'stages.update')
->label('audits.resource', 'project/{request.projectId}')
->label('sdk', new Method(
namespace: 'projects',
group: 'stages',
name: 'updateStage',
description: '/docs/references/projects/update-stage.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_STAGE,
)
],
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('stageId', '', new Text(64), 'Stage ID.')
->param('skip', true, new Boolean(), 'Mark the stage as skipped.', true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The skip parameter defaults to true, meaning a PATCH request sent with no body will silently skip the stage. This is a destructive default; callers who forget to include the body (or who intend a no-op read) will permanently mark stages as skipped. Defaulting to false preserves the current status unless the caller explicitly opts in.

Suggested change
->param('skip', true, new Boolean(), 'Mark the stage as skipped.', true)
->param('skip', false, new Boolean(), 'Mark the stage as skipped.', true)

->inject('response')
->inject('dbForPlatform')
->inject('apiKey')
->inject('user')
->inject('mode')
->callback($this->action(...));
}

public function action(string $projectId, string $stageId, bool $skip, Response $response, Database $dbForPlatform, ?Key $apiKey, User $user, string $mode): void
{
$project = $dbForPlatform->getDocument('projects', $projectId);

if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}

$definition = $this->getStageDefinition($stageId);

$byStageId = $project->getAttribute('onboarding', []);
if (! \is_array($byStageId)) {
$byStageId = [];
}

$row = \is_array($byStageId[$stageId] ?? null) ? $byStageId[$stageId] : null;

if ($skip) {
$prev = \is_array($row) ? ($row['status'] ?? '') : '';
if ($prev !== ONBOARDING_STATUS_COMPLETED) {
$byStageId[$stageId] = [

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functional Medium

After a client skips a stage via PATCH /v1/projects/{projectId}/stages/{stageId}, the later successful API call for that stage (for example creating a database) never flips it to completed because the shutdown hook in app/controllers/shared/api.php explicitly refuses to overwrite skipped status.

Suggested change
$byStageId[$stageId] = [
Either allow the shutdown hook to transition skipped -> completed when the mapped SDK action succeeds, or persist a distinct "dismissed" state for manual skips that does not block later automatic completion.
Prompt for AI assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert php developer with deep knowledge of security, performance, and best practices.

### Context

File: src/Appwrite/Platform/Modules/Projects/Http/Stages/Update.php
Lines: 85-85
Issue Type: functional-medium
Severity: medium

Issue Description:
After a client skips a stage via PATCH /v1/projects/{projectId}/stages/{stageId}, the later successful API call for that stage (for example creating a database) never flips it to completed because the shutdown hook in app/controllers/shared/api.php explicitly refuses to overwrite skipped status.

Current Code:
$byStageId[$stageId] = [
    'status' => ONBOARDING_STATUS_SKIPPED,
    'at' => DateTime::now(),
    'actorType' => $this->resolveActorType($apiKey, $user, $mode),
];

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow php best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira

'status' => ONBOARDING_STATUS_SKIPPED,
'at' => DateTime::now(),
'actorType' => $this->resolveActorType($apiKey, $user, $mode),
];
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('onboarding', $byStageId));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functional Medium

After a stage is skipped, a successful API call that matches that stage (for example PATCH /v1/projects/{id}/stages/create_database with skip=true followed by creating a database) will never mark it completed because the shutdown hook in app/controllers/shared/api.php now treats skipped as terminal.

Change the shared API hook to only preserve COMPLETED as terminal, or add explicit logic here to let real route completion overwrite SKIPPED with COMPLETED when the mapped SDK action later succeeds.

Prompt for AI assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert php developer with deep knowledge of security, performance, and best practices.

### Context

File: src/Appwrite/Platform/Modules/Projects/Http/Stages/Update.php
Lines: 90-90
Issue Type: functional-medium
Severity: medium

Issue Description:
After a stage is skipped, a successful API call that matches that stage (for example PATCH /v1/projects/{id}/stages/create_database with skip=true followed by creating a database) will never mark it completed because the shutdown hook in app/controllers/shared/api.php now treats skipped as terminal.

Current Code:
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('onboarding', $byStageId));

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow php best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira

Comment on lines +82 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security High

This update reads and writes the project without an atomic compare or transaction, so a concurrent successful stage completion can be overwritten with skipped; re-read under a lock or perform a conditional update that preserves completed.

Suggested fix
        if ($skip) {
            $project = $dbForPlatform->getDocument('projects', $projectId);
            $byStageId = $project->getAttribute('onboarding', []);
            if (! \is_array($byStageId)) {
                $byStageId = [];
            }

            $row = \is_array($byStageId[$stageId] ?? null) ? $byStageId[$stageId] : null;
            $prev = \is_array($row) ? ($row['status'] ?? '') : '';
            if ($prev !== ONBOARDING_STATUS_COMPLETED) {
                $byStageId[$stageId] = [
                    'status' => ONBOARDING_STATUS_SKIPPED,
                    'at' => DateTime::now(),
                    'actorType' => $this->resolveActorType($apiKey, $user, $mode),
                ];
                $project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
                    'onboarding' => $byStageId,
                ]));
            }
        }
Prompt for AI assistance

Copy the prompt below and paste it into ChatGPT, Claude, or any LLM:

You are an expert php developer with deep knowledge of security, performance, and best practices.

### Context

File: src/Appwrite/Platform/Modules/Projects/Http/Stages/Update.php
Lines: 82-90
Issue Type: security-high
Severity: high

Issue Description:
This update reads and writes the project without an atomic compare or transaction, so a concurrent successful stage completion can be overwritten with `skipped`; re-read under a lock or perform a conditional update that preserves `completed`.

Current Code:
        if ($skip) {
            $prev = \is_array($row) ? ($row['status'] ?? '') : '';
            if ($prev !== ONBOARDING_STATUS_COMPLETED) {
                $byStageId[$stageId] = [
                    'status' => ONBOARDING_STATUS_SKIPPED,
                    'at' => DateTime::now(),
                    'actorType' => $this->resolveActorType($apiKey, $user, $mode),
                ];
                $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('onboarding', $byStageId));

---

### Instructions

1. Fix the issue described above
2. Maintain the exact indentation and code style from the original
3. Follow php best practices and language-specific idioms
4. Ensure the fix addresses the root cause, not just the symptoms
5. Add brief inline comments explaining the fix if needed

### Constraints

- Do not change functionality beyond fixing the identified issue
- Preserve existing variable names and function signatures unless they are part of the problem
- Ensure the fix is production-ready

---


Like Dislike Create Issue Jira

$byStageId = $project->getAttribute('onboarding', []);
$row = \is_array($byStageId[$stageId] ?? null) ? $byStageId[$stageId] : null;
}
}

$response->dynamic(new Document($this->formatStageRow($definition, $row)), Response::MODEL_STAGE);
}

/**
* @return array{id: string, sdk: string}
*/
private function getStageDefinition(string $stageId): array
{
foreach (Config::getParam('onboarding', [])['stages'] ?? [] as $definition) {
if (($definition['id'] ?? '') === $stageId) {
return $definition;
}
}

throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Unknown stage ID: ' . $stageId);
}

/**
* @param array<string, mixed>|null $row
* @return array<string, mixed>
*/
private function formatStageRow(array $definition, ?array $row): array
{
$stageId = $definition['id'];
$status = \is_array($row) ? ($row['status'] ?? null) : null;
$at = \is_array($row) ? ($row['at'] ?? '') : '';
$actorType = \is_array($row) ? ($row['actorType'] ?? '') : '';

return [
'id' => $stageId,
'sdk' => $definition['sdk'] ?? '',
'status' => $status ?? 'pending',
'at' => $at,
'actorType' => $actorType,
];
}

private function resolveActorType(?Key $apiKey, User $user, string $mode): string
{
if ($apiKey !== null && $apiKey->getRole() === User::ROLE_APPS) {
return match ($apiKey->getType()) {
API_KEY_ACCOUNT => ACTIVITY_TYPE_KEY_ACCOUNT,
API_KEY_ORGANIZATION => ACTIVITY_TYPE_KEY_ORGANIZATION,
API_KEY_STANDARD, API_KEY_DYNAMIC => ACTIVITY_TYPE_KEY_PROJECT,
default => ACTIVITY_TYPE_KEY_PROJECT,
};
}

if (! $user->isEmpty()) {
return $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER;
}

return ACTIVITY_TYPE_GUEST;
}
}
Loading