Skip to content

feat(migration): add project-level resource migrations#186

Open
premtsd-code wants to merge 23 commits into
add-webhook-migrationfrom
add-policies-migration
Open

feat(migration): add project-level resource migrations#186
premtsd-code wants to merge 23 commits into
add-webhook-migrationfrom
add-policies-migration

Conversation

@premtsd-code
Copy link
Copy Markdown
Contributor

@premtsd-code premtsd-code commented May 20, 2026

Adds support for migrating project-level Appwrite resources end-to-end.

Singleton resource per project carrying the 7 auth-method flags
(email/password, magic URL, email OTP, anonymous, invites, JWT, phone).
Source reads via raw GET /v1/project (no SDK get() method exposed);
destination flips each flag via Project::updateAuthMethod().

Renames destination $project (string) -> $projectId so $project can
hold the Project SDK service, matching the source-side convention.
Singleton settings resource carrying REST/GraphQL/WebSocket flags.
Source reads via raw GET /v1/project; destination flips each via
Project::updateProtocol(). Lives in GROUP_SETTINGS.
Singleton settings resource carrying the project's RBAC label array.
Source reads via raw GET /v1/project; destination overwrites via
Project::updateLabels() (wholesale replace).
Singleton settings resource carrying 17 per-service enable/disable flags
(Account, Avatars, Databases, TablesDB, Locale, Health, Project, Storage,
Teams, Users, VCS, Sites, Functions, Proxy, GraphQL, Migrations, Messaging).
Source reads via raw GET /v1/project; destination flips each via
Project::updateService().
The SDK Client's endpoint already includes /v1; calling $this->call('GET',
'/v1/project') produced http://host/v1/v1/project and 404'd. Existing
'/health/version' caller already follows the prefix-less convention.

Affects all five project-singleton sources (AuthMethods, Policies,
Protocols, Labels, Services).
passwordHistory / sessionsLimit / userLimit treat 0 as "disabled" in the
source's project document, but the server policy validators reject 0 —
they accept a positive int or null. Disabled policies were round-tripping
as 0 and the destination rejected the update with:

  Invalid `total` param: Value must be a valid range between 1 and 5,000
  or null

Coerce 0 -> null at the SDK call site so the disabled state preserves
correctly across the migration.
Replace 9 SDK setter calls (updatePasswordHistoryPolicy, etc.) with a
single dbForPlatform->updateDocument('projects', ...) write. Matches
the convention used by Webhook / Platform / ProjectVariable / ApiKey
destinations — every other project-singleton resource already writes
directly to the project document instead of going through the API.

Drops two server-side workarounds along the way:
- SDK setters return MODEL_PROJECT, which carries cloud's empty
  billingLimits as {} that strict-typed SDKs reject ("Missing required
  field bandwidth"). Direct DB write never deserializes a Project,
  so the bug is irrelevant.
- PasswordHistory / SessionLimit / UserLimit endpoints reject total: 0
  even though 0 is the storage convention for "disabled" (see response
  model description). Direct DB write writes 0 straight to storage,
  bypassing the validator mismatch.

Note: cloud spec fix (appwrite-labs/cloud#4068) and validator fix
(separate appwrite/appwrite PR for Range(0, ...)) are still worth
landing — they help any other SDK consumer hitting the same paths —
but the migration no longer depends on either.
The platform 'projects' collection has document-level permissions
restricted to team-owner roles. The migration worker has no team
context, so the updateDocument call was being silently rejected by
the authorization validator — the migration reported success because
no exception bubbled up, but the destination project's auths attribute
was left unchanged.

Match the upstream Policies/Labels controllers' pattern by wrapping the
write in $dbForPlatform->getAuthorization()->skip(...).
- composer.json: appwrite/appwrite ^23 -> ^24
- composer.lock: appwrite/appwrite 23.1.0 -> 24.1.0
- AuthMethod  -> ProjectAuthMethodId
- ProtocolId  -> ProjectProtocolId
- ServiceId   -> ProjectServiceId

24.1.0 brings the BillingLimits nullable + Project.consoleAccessedAt
fix that was blocking the policies migration, and adds Project::get
and Project::getPolicy methods used by the source-side refactor.
The /v1/project response doesn't expose per-policy fields at the top
level — they live inside the project document's `auths` attribute
which the public Project response model omits.

Switch from a raw `GET /v1/project` call to per-policy SDK methods
(Project::getPolicy(ProjectPolicyId::*)) which return typed policy
models (PolicyPasswordHistory, PolicySessionAlert, etc.). Each of the
9 sub-policies maps to one method call.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 20, 2026

Greptile Summary

This PR adds end-to-end migration support for five project-level Appwrite resource types — AuthMethods, Policies, Protocols, Labels, and Services — and renames the GROUP_SETTINGS group to GROUP_PROJECTS across the codebase. The SDK dependency is bumped from 23.* to 24.* to gain Project::get(), getPolicy(), and the new enum types (ProjectAuthMethodId, ProjectPolicyId, ProjectProtocolId) required by the new export methods.

  • Five new resource classes are introduced (AuthMethods, Policies, Protocols, Labels, Services), each following the existing singleton-resource pattern with fromArray/jsonSerialize/getName; destination handlers write directly to the platform DB via read-merge-write to avoid SDK validation constraints on zero values.
  • GROUP_SETTINGS is fully replaced by GROUP_PROJECTS; Webhook::getGroup() and ProjectVariable::getGroup() are corrected to GROUP_INTEGRATIONS and GROUP_PROJECTS respectively; Transfer::ALL_PUBLIC_RESOURCES and GROUP_AUTH_RESOURCES are extended with the new types.

Confidence Score: 5/5

Safe to merge; the new resource classes, group mappings, and destination write paths are internally consistent and existing error-handling conventions are followed correctly throughout.

The group rename is applied uniformly across all source adapters, the destination dispatcher, and the Transfer constants. The direct DB writes in createAuthMethods and createPolicies correctly use a read-merge-write pattern over disjoint key sets, so the two sequential writes cannot clobber each other. No correctness bugs were found; the two flagged items are a missing entry in a test mock's getSupportedResources() list and redundant HTTP calls in the source export helpers.

No files require special attention beyond the noted redundant project->get() calls in the source and the missing types in MockSource::getSupportedResources().

Important Files Changed

Filename Overview
src/Migration/Destinations/Appwrite.php Adds destination handlers for AuthMethods, Policies, Protocols, Labels, and Services; all use read-merge-write against the platform DB with authorization skipped; property rename from $project (string) to $projectId is applied consistently throughout the file.
src/Migration/Sources/Appwrite.php Adds export logic for AuthMethods, Policies, Protocols, Labels, and Services; exportGroupAuth/exportGroupProjects correctly dispatch each type; exportServices/exportLabels/exportProtocols each issue a redundant project->get() call that could be collapsed to one.
src/Migration/Transfer.php Renames GROUP_SETTINGS to GROUP_PROJECTS, adds TYPE_AUTH_METHODS and TYPE_POLICIES to GROUP_AUTH_RESOURCES and ALL_PUBLIC_RESOURCES, and defines GROUP_PROJECTS_RESOURCES with the four new project-level types; all group mappings in extractServices() are updated.
src/Migration/Resource.php Adds six new type constants and includes them all in the ALL_RESOURCES list.
src/Migration/Resources/Auth/AuthMethods.php New singleton resource for project auth-method flags; implements fromArray/jsonSerialize/getName correctly; group returns GROUP_AUTH matching the dispatcher.
src/Migration/Resources/Auth/Policies.php New singleton resource for 13 policy fields across password, session, user-limit, and membership-privacy categories; field names and serialization keys are internally consistent.
tests/Migration/Unit/Adapters/MockSource.php exportGroupProjects() correctly updated; however TYPE_AUTH_METHODS and TYPE_POLICIES are missing from getSupportedResources(), creating an inconsistency between what the mock can export and what it advertises.
composer.json Bumps appwrite/appwrite SDK dependency from 23.* to 24.* to gain Project::get(), getPolicy(), and the new enum types used by the new export methods.

Reviews (10): Last reviewed commit: "Revert Protocols loop refactor (review f..." | Re-trigger Greptile

Comment thread src/Migration/Sources/Appwrite.php Outdated
Comment thread src/Migration/Sources/Appwrite.php Outdated
…rvices/Labels

Replaces 4 raw 'GET /v1/project' HTTP calls with the typed Project model
from SDK 24.1.0. Each resource iterates the typed authMethods/protocols/
services arrays (each containing typed ProjectAuthMethod/ProjectProtocol/
ProjectService models with id+enabled fields) and builds an id->enabled
lookup keyed by the corresponding Project*Id enum.

Brings the source side fully in line with policies: all 5 settings
resources now read via the SDK only, no raw HTTP calls remain.
Comment thread src/Migration/Destinations/Appwrite.php
Comment thread src/Migration/Destinations/Appwrite.php Outdated
Unifies the destination side: Protocols, Labels, Services, AuthMethods,
and Policies now all write to the project document directly via
dbForPlatform->updateDocument(...) wrapped in getAuthorization()->skip().

Each resource bundles its fields into ONE document write instead of N
SDK round-trips:
  - Protocols  3 SDK calls -> 1 document update
  - Services  17 SDK calls -> 1 document update
  - AuthMethods 7 SDK calls -> 1 document update
  - Labels      1 SDK call  -> 1 document update (with array_unique dedupe)
  - Policies (unchanged: was already direct DB)

Field mapping mirrors the upstream server handlers:
  - Protocols   -> project.apis (map)
  - Services    -> project.services (map)
  - Labels      -> project.labels (array, deduped)
  - AuthMethods -> project.auths (map; keys from app/config/auth.php)
  - Policies    -> project.auths (map; shares same attribute as AuthMethods)

AuthMethods and Policies both read-then-merge the auths map so they
coexist when both ship in the same migration.
@premtsd-code premtsd-code changed the base branch from main to add-webhook-migration May 20, 2026 11:32
Existing private/protected migration functions carry at most a one-line
description (most have just @throws). The recent docblocks I added were
over-explaining what the code already says.

Kept only the two non-obvious WHYs:
  - createAuthMethods: storage keys differ from SDK enum values; shares
    the auths map with createPolicies.
  - createPolicies: SDK setters reject 0 even though 0 = disabled in
    storage.
Comment on lines 296 to 301
Resource::TYPE_PROJECT_VARIABLE,
Resource::TYPE_WEBHOOK,
Resource::TYPE_PROTOCOLS,
Resource::TYPE_LABELS,
Resource::TYPE_SERVICES,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's move variables and protocols, labels, and services into their own "projects" group. Webhooks can go with integrations. Let's also prefix them all like TYPE_PROJECT_*

Comment thread src/Migration/Destinations/Appwrite.php Outdated
Comment on lines +3233 to +3249
$services[(string) ProjectServiceId::ACCOUNT()] = $resource->getAccount();
$services[(string) ProjectServiceId::AVATARS()] = $resource->getAvatars();
$services[(string) ProjectServiceId::DATABASES()] = $resource->getDatabases();
$services[(string) ProjectServiceId::TABLESDB()] = $resource->getTablesdb();
$services[(string) ProjectServiceId::LOCALE()] = $resource->getLocale();
$services[(string) ProjectServiceId::HEALTH()] = $resource->getHealth();
$services[(string) ProjectServiceId::PROJECT()] = $resource->getProject();
$services[(string) ProjectServiceId::STORAGE()] = $resource->getStorage();
$services[(string) ProjectServiceId::TEAMS()] = $resource->getTeams();
$services[(string) ProjectServiceId::USERS()] = $resource->getUsers();
$services[(string) ProjectServiceId::VCS()] = $resource->getVcs();
$services[(string) ProjectServiceId::SITES()] = $resource->getSites();
$services[(string) ProjectServiceId::FUNCTIONS()] = $resource->getFunctions();
$services[(string) ProjectServiceId::PROXY()] = $resource->getProxy();
$services[(string) ProjectServiceId::GRAPHQL()] = $resource->getGraphql();
$services[(string) ProjectServiceId::MIGRATIONS()] = $resource->getMigrations();
$services[(string) ProjectServiceId::MESSAGING()] = $resource->getMessaging();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's loop the enum elements and build the resource method name as a string so we don't need to extend for new services

Comment on lines +1700 to +1721
$services = new ServicesResource(
$this->projectId,
$byId[(string) ProjectServiceId::ACCOUNT()] ?? true,
$byId[(string) ProjectServiceId::AVATARS()] ?? true,
$byId[(string) ProjectServiceId::DATABASES()] ?? true,
$byId[(string) ProjectServiceId::TABLESDB()] ?? true,
$byId[(string) ProjectServiceId::LOCALE()] ?? true,
$byId[(string) ProjectServiceId::HEALTH()] ?? true,
$byId[(string) ProjectServiceId::PROJECT()] ?? true,
$byId[(string) ProjectServiceId::STORAGE()] ?? true,
$byId[(string) ProjectServiceId::TEAMS()] ?? true,
$byId[(string) ProjectServiceId::USERS()] ?? true,
$byId[(string) ProjectServiceId::VCS()] ?? true,
$byId[(string) ProjectServiceId::SITES()] ?? true,
$byId[(string) ProjectServiceId::FUNCTIONS()] ?? true,
$byId[(string) ProjectServiceId::PROXY()] ?? true,
$byId[(string) ProjectServiceId::GRAPHQL()] ?? true,
$byId[(string) ProjectServiceId::MIGRATIONS()] ?? true,
$byId[(string) ProjectServiceId::MESSAGING()] ?? true,
createdAt: $project->createdAt,
updatedAt: $project->updatedAt,
);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same here, let's build in a loop instead

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants