Skip to content
156 changes: 152 additions & 4 deletions src/lib/stores/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,38 @@ export type MigrationResource =
| AppwriteMigrationResource
| FirebaseMigrationResource
| NHostMigrationResource
| SupabaseMigrationResource;
| SupabaseMigrationResource
| 'platform'
| 'api-key'
| 'project-variable'
| 'webhook'
| 'auth-methods'
| 'protocols'
| 'labels'
| 'services'
| 'policies'
| 'smtp'
| 'rule';

// Appwrite enum is the superset of all provider resources — used as a
// provider-agnostic reference. The addResource guard filters by provider.
export const MigrationResources = AppwriteMigrationResource;
// Project-level singleton resources (Platform, ApiKey, ProjectVariable,
// Webhook, AuthMethods, Protocols, Labels, Services, Policies) are augmented
// locally until @appwrite.io/console SDK is regenerated.
export const MigrationResources = {
...AppwriteMigrationResource,
Platform: 'platform',
ApiKey: 'api-key',
ProjectVariable: 'project-variable',
Webhook: 'webhook',
AuthMethods: 'auth-methods',
Protocols: 'protocols',
Labels: 'labels',
Services: 'services',
Policies: 'policies',
SMTP: 'smtp',
Rule: 'rule'
} as const;

type ProviderResourceMap = {
appwrite: AppwriteMigrationResource[];
Expand Down Expand Up @@ -50,6 +77,35 @@ const initialFormData = {
},
backups: {
root: false
},
authMethods: {
root: false
},
protocols: {
root: false
},
labels: {
root: false
},
services: {
root: false
},
policies: {
root: false
},
smtp: {
root: false
},
customDomains: {
root: false
},
integrations: {
root: false,
apiKeys: false
},
settings: {
root: false,
webhooks: false
}
};

Expand Down Expand Up @@ -88,11 +144,35 @@ export const ResourcesFriendly = {
topic: { singular: 'Topic', plural: 'Topics' },
subscriber: { singular: 'Subscriber', plural: 'Subscribers' },
message: { singular: 'Message', plural: 'Messages' },
'backup-policy': { singular: 'Backup Policy', plural: 'Backup Policies' }
'backup-policy': { singular: 'Backup Policy', plural: 'Backup Policies' },
platform: { singular: 'Platform', plural: 'Platforms' },
'api-key': { singular: 'API Key', plural: 'API Keys' },
'project-variable': { singular: 'Project Variable', plural: 'Project Variables' },
webhook: { singular: 'Webhook', plural: 'Webhooks' },
'auth-methods': { singular: 'Auth method config', plural: 'Auth method config' },
protocols: { singular: 'Protocol config', plural: 'Protocol config' },
labels: { singular: 'Project labels', plural: 'Project labels' },
services: { singular: 'Services config', plural: 'Services config' },
policies: { singular: 'Policies config', plural: 'Policies config' },
smtp: { singular: 'SMTP config', plural: 'SMTP config' },
rule: { singular: 'Custom domain', plural: 'Custom domains' }
};

export const providerResources: ProviderResourceMap = {
appwrite: Object.values(AppwriteMigrationResource),
appwrite: [
...Object.values(AppwriteMigrationResource),
MigrationResources.AuthMethods as AppwriteMigrationResource,
MigrationResources.Protocols as AppwriteMigrationResource,
MigrationResources.Labels as AppwriteMigrationResource,
MigrationResources.Services as AppwriteMigrationResource,
MigrationResources.Policies as AppwriteMigrationResource,
MigrationResources.SMTP as AppwriteMigrationResource,
MigrationResources.Rule as AppwriteMigrationResource,
MigrationResources.Platform as AppwriteMigrationResource,
MigrationResources.ApiKey as AppwriteMigrationResource,
MigrationResources.ProjectVariable as AppwriteMigrationResource,
MigrationResources.Webhook as AppwriteMigrationResource
],
supabase: Object.values(SupabaseMigrationResource),
nhost: Object.values(NHostMigrationResource),
firebase: Object.values(FirebaseMigrationResource)
Expand Down Expand Up @@ -155,6 +235,39 @@ export const migrationFormToResources = <P extends Provider>(
if (formData.backups.root) {
addResource(MigrationResources.Backuppolicy);
}
if (formData.authMethods.root) {
addResource(MigrationResources.AuthMethods);
}
if (formData.protocols.root) {
addResource(MigrationResources.Protocols);
}
if (formData.labels.root) {
addResource(MigrationResources.Labels);
}
if (formData.services.root) {
addResource(MigrationResources.Services);
}
if (formData.policies.root) {
addResource(MigrationResources.Policies);
}
if (formData.smtp.root) {
addResource(MigrationResources.SMTP);
}
if (formData.customDomains.root) {
addResource(MigrationResources.Rule);
}
if (formData.integrations.root) {
addResource(MigrationResources.Platform);
if (formData.integrations.apiKeys) {
addResource(MigrationResources.ApiKey);
}
}
if (formData.settings.root) {
addResource(MigrationResources.ProjectVariable);
if (formData.settings.webhooks) {
addResource(MigrationResources.Webhook);
}
}

return resources as ProviderResourceMap[P];
};
Expand Down Expand Up @@ -234,6 +347,41 @@ export const resourcesToMigrationForm = (resources: MigrationResource[]): Migrat
if (resources.includes(MigrationResources.Backuppolicy)) {
formData.backups.root = true;
}
if (resources.includes(MigrationResources.AuthMethods)) {
formData.authMethods.root = true;
}
if (resources.includes(MigrationResources.Protocols)) {
formData.protocols.root = true;
}
if (resources.includes(MigrationResources.Labels)) {
formData.labels.root = true;
}
if (resources.includes(MigrationResources.Services)) {
formData.services.root = true;
}
if (resources.includes(MigrationResources.Policies)) {
formData.policies.root = true;
}
if (resources.includes(MigrationResources.SMTP)) {
formData.smtp.root = true;
}
if (resources.includes(MigrationResources.Rule)) {
formData.customDomains.root = true;
}
if (resources.includes(MigrationResources.Platform)) {
formData.integrations.root = true;
}
if (resources.includes(MigrationResources.ApiKey)) {
formData.integrations.root = true;
formData.integrations.apiKeys = true;
}
if (resources.includes(MigrationResources.ProjectVariable)) {
formData.settings.root = true;
}
if (resources.includes(MigrationResources.Webhook)) {
formData.settings.root = true;
formData.settings.webhooks = true;
}
Comment on lines +374 to +384
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Round-trip inconsistency: sub-item presence sets root=true, which forward-maps to the parent resource

When resourcesToMigrationForm is called with a list that contains ApiKey but not Platform, it sets formData.integrations.root = true. Then migrationFormToResources always emits Platform when integrations.root is true, so a round-trip silently adds Platform to the resource list. The same applies to Webhook / ProjectVariable in the settings group.

In the current UI flow resourcesToMigrationForm is only called via selectAll() with the full provider list (which always includes both resources), so this is not immediately observable. However, if the function is reused in an edit/re-import flow in the future, migrations could silently acquire unintended resources.


return formData;
};
Expand Down
53 changes: 52 additions & 1 deletion src/routes/(console)/(migration-wizard)/resource-form.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,48 @@
return resources.includes(MigrationResources.Backuppolicy);
}

if (groupKey === 'authMethods') {
return resources.includes(MigrationResources.AuthMethods);
}

if (groupKey === 'protocols') {
return resources.includes(MigrationResources.Protocols);
}

if (groupKey === 'labels') {
return resources.includes(MigrationResources.Labels);
}

if (groupKey === 'services') {
return resources.includes(MigrationResources.Services);
}

if (groupKey === 'policies') {
return resources.includes(MigrationResources.Policies);
}

if (groupKey === 'smtp') {
return resources.includes(MigrationResources.SMTP);
}

if (groupKey === 'customDomains') {
return resources.includes(MigrationResources.Rule);
}

if (groupKey === 'integrations') {
return (
resources.includes(MigrationResources.Platform) ||
resources.includes(MigrationResources.ApiKey)
);
}

if (groupKey === 'settings') {
return (
resources.includes(MigrationResources.ProjectVariable) ||
resources.includes(MigrationResources.Webhook)
);
}

const groupToResource: Record<string, MigrationResource> = {
users: MigrationResources.User,
databases: MigrationResources.Database
Expand All @@ -140,7 +182,16 @@
storage: 'bucket',
sites: 'site',
messaging: 'provider',
backups: 'backup-policy'
backups: 'backup-policy',
authMethods: 'auth-methods',
protocols: 'protocols',
labels: 'labels',
services: 'services',
policies: 'policies',
smtp: 'smtp',
customDomains: 'rule',
integrations: 'platform',
settings: 'project-variable'
};
return map[groupKey] || groupKey;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,35 @@
},
backups: {
root: 'Backup policies'
},
authMethods: {
root: 'Auth methods'
},
protocols: {
root: 'Protocols'
},
labels: {
root: 'Project labels'
},
services: {
root: 'Services'
},
policies: {
root: 'Security policies'
},
smtp: {
root: 'Custom SMTP'
},
customDomains: {
root: 'Custom domains'
},
integrations: {
root: 'Platforms',
apiKeys: 'Include API keys'
},
settings: {
root: 'Project variables',
webhooks: 'Include webhooks'
}
};

Expand All @@ -62,6 +91,36 @@
},
backups: {
root: 'Import all backup policies'
},
authMethods: {
root: 'Import the project auth method flags (email/password, magic URL, JWT, phone, etc.)'
},
protocols: {
root: 'Import the project protocol flags (REST / GraphQL / WebSocket)'
},
labels: {
root: 'Import the project-level RBAC label array'
},
services: {
root: 'Import the project service enable/disable flags (Account, Databases, Functions, GraphQL, etc.)'
},
policies: {
root: 'Import the project security policies (password rules, session behavior, user limits, membership privacy)'
},
smtp: {
root: 'Import the project custom SMTP configuration (the password is not exposed by the SDK and stays on the destination)'
},
customDomains: {
root: 'Import manually-added custom-domain proxy rules (API, function, site, redirect). Auto-generated `.appwrite.network` rules are skipped — they are recreated by parent Function/Site migration.'
},
integrations: {
root: 'Import all platforms (web, Flutter, iOS, Android, etc.)',
apiKeys: 'Import all API keys with their scopes and expiration'
},
settings: {
root: 'Import all project-level variables (secret values are not exposed by the SDK and will be migrated empty)',
webhooks:
'Import all webhooks (signing secrets are not exposed by the SDK; the destination regenerates a fresh signature key for each)'
}
};

Expand Down