feat(migration): add project-level resource migrations#186
feat(migration): add project-level resource migrations#186premtsd-code wants to merge 25 commits into
Conversation
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 SummaryThis PR adds end-to-end migration support for project-level Appwrite resources — auth methods, security policies, protocols, labels, and service enable/disable flags — and upgrades the Appwrite PHP SDK dependency from v23 to v24. The "settings" transfer group is renamed to "projects" throughout, and five new singleton resource classes are introduced alongside their corresponding source exporters and destination writers.
Confidence Score: 4/5Safe to merge after confirming the GROUP_SETTINGS removal is intentional for this release and that downstream callers have been updated. The removal of the public constant Transfer::GROUP_SETTINGS without a deprecation alias breaks any external caller that references it by name, and any persisted migration state that stored the group string 'settings' will hit the default-throw path on resume. Everything else — the new resource classes, the destination write logic, and the $project → $projectId rename — looks correct and internally consistent. src/Migration/Transfer.php (GROUP_SETTINGS removal) and src/Migration/Destinations/Appwrite.php (protocol storage-key alignment in createProtocols) Important Files Changed
Reviews (11): Last reviewed commit: "Register auth-methods, policies, protoco..." | Re-trigger Greptile |
…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.
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.
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.
| Resource::TYPE_PROJECT_VARIABLE, | ||
| Resource::TYPE_WEBHOOK, | ||
| Resource::TYPE_PROTOCOLS, | ||
| Resource::TYPE_LABELS, | ||
| Resource::TYPE_SERVICES, | ||
|
|
There was a problem hiding this comment.
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_*
| $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(); |
There was a problem hiding this comment.
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
| $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, | ||
| ); |
There was a problem hiding this comment.
Same here, let's build in a loop instead
…policies-migration
…TYPE_PROJECT_* prefix
Adds support for migrating project-level Appwrite resources end-to-end.