feat(migration): add project-level resource migrations#186
feat(migration): add project-level resource migrations#186premtsd-code wants to merge 17 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 five new project-level Appwrite resources: auth methods, security policies, protocols, service flags, and RBAC labels. It also bumps the SDK from v23 to v24 to access the
Confidence Score: 5/5Safe to merge; the new export and import paths are isolated singleton handlers with no mutation of shared state beyond the expected project document fields. The destination writes use careful read-modify-write patterns with purgeCachedDocument after each update, auth methods and policies write disjoint key sets into the shared auths attribute, and the error-code cast fix was correctly backfilled. The main concerns are efficiency rather than correctness. src/Migration/Sources/Appwrite.php — the new export helpers each fetch the project document independently; worth consolidating in a follow-up. Important Files Changed
Reviews (6): Last reviewed commit: "Normalize error codes in exportGroupSett..." | 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.
Adds support for migrating project-level Appwrite resources end-to-end.