diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte index 1df280dcef..017e1a03a0 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte @@ -5,7 +5,15 @@ import type { Column, ColumnType } from '$lib/helpers/types'; import { Container } from '$lib/layout'; import { preferences } from '$lib/stores/preferences'; - import { Icon, Layout, Divider, Tooltip } from '@appwrite.io/pink-svelte'; + import { + Icon, + Layout, + Divider, + Tooltip, + Selector, + Typography, + Dialog + } from '@appwrite.io/pink-svelte'; import type { PageProps } from './$types'; import FilePicker from '$lib/components/filePicker.svelte'; import { page } from '$app/state'; @@ -21,7 +29,7 @@ IconUpload, IconDownload } from '@appwrite.io/pink-icons-svelte'; - import { type Models } from '@appwrite.io/console'; + import { OnDuplicate, type Models } from '@appwrite.io/console'; import { sdk } from '$lib/stores/sdk'; import { goto } from '$app/navigation'; import { resolve } from '$app/paths'; @@ -49,7 +57,11 @@ let isRefreshing = $state(false); let showImportJson = $state(false); + let showImportOptions = $state(false); let showCustomColumnsModal = $state(false); + let importOnDuplicate: OnDuplicate = $state(OnDuplicate.Fail); + let pendingFile: Models.File | null = $state(null); + let pendingLocalFile = $state(false); let columnsError: string = $state(null); let spreadsheet: SpreadSheet | null = $state(null); @@ -74,17 +86,28 @@ return queryParam ? `${url}?query=${encodeURIComponent(queryParam)}` : url; } - async function onSelect(file: Models.File, localFile = false) { + function onSelect(file: Models.File, localFile = false) { + pendingFile = file; + pendingLocalFile = localFile; + importOnDuplicate = OnDuplicate.Fail; + showImportOptions = true; + } + + async function startImport() { + if (!pendingFile) return; + + showImportOptions = false; $isCollectionsJsonImportInProgress = true; try { await sdk .forProject(page.params.region, page.params.project) .migrations.createJSONImport({ - bucketId: file.bucketId, - fileId: file.$id, + bucketId: pendingFile.bucketId, + fileId: pendingFile.$id, resourceId: `${page.params.database}:${page.params.collection}`, - internalFile: localFile + internalFile: pendingLocalFile, + onDuplicate: importOnDuplicate }); addNotification({ @@ -101,6 +124,7 @@ }); } finally { $isCollectionsJsonImportInProgress = false; + pendingFile = null; } } @@ -343,6 +367,52 @@ }} /> {/if} + + + + Choose how to handle documents that already exist in this collection. + + + + + Import aborts on the first document with a matching ID. + + + + + Documents with matching IDs will be silently skipped. + + + + + Documents with matching IDs will be updated with the imported data. + + + + + + + + + + + + ([]); @@ -107,17 +120,28 @@ $: disableButton = canShowSuggestionsSheet; - async function onSelect(file: Models.File, localFile = false) { + function onSelect(file: Models.File, localFile = false) { + pendingFile = file; + pendingLocalFile = localFile; + importOnDuplicate = OnDuplicate.Fail; + showImportOptions = true; + } + + async function startImport() { + if (!pendingFile) return; + + showImportOptions = false; $isTablesCsvImportInProgress = true; try { await sdk .forProject(page.params.region, page.params.project) .migrations.createCSVImport({ - bucketId: file.bucketId, - fileId: file.$id, + bucketId: pendingFile.bucketId, + fileId: pendingFile.$id, resourceId: `${page.params.database}:${page.params.table}`, - internalFile: localFile + internalFile: pendingLocalFile, + onDuplicate: importOnDuplicate }); addNotification({ @@ -134,6 +158,7 @@ }); } finally { $isTablesCsvImportInProgress = false; + pendingFile = null; } } @@ -434,6 +459,52 @@ }} /> {/if} + + + + Choose how to handle documents that already exist in this table. + + + + + Migration aborts on the first row with a matching ID. + + + + + Documents with matching IDs will be silently skipped. + + + + + Documents with matching IDs will be updated with the imported data. + + + + + + + + + + + + { resetImportStores(); }; @@ -46,6 +50,15 @@ try { const resources = migrationFormToResources($formData, $provider.provider); + // Gate onDuplicate to Fail when databases isn't selected. The radios + // are only shown when databases.root is checked, but the local value + // persists across toggles — without this gate, deselecting databases + // after picking Overwrite/Skip would silently apply that mode to + // other resource types (users, teams, functions, etc.) on submit. + const importOptions = { + onDuplicate: $formData.databases.root ? importOnDuplicate : OnDuplicate.Fail + }; + switch ($provider.provider) { case 'appwrite': { await sdk @@ -54,7 +67,8 @@ resources: resources as AppwriteMigrationResource[], endpoint: $provider.endpoint, projectId: $provider.projectID, - apiKey: $provider.apiKey + apiKey: $provider.apiKey, + ...importOptions }); await invalidate(Dependencies.MIGRATIONS); @@ -70,7 +84,8 @@ databaseHost: $provider.host, username: $provider.username || 'postgres', password: $provider.password, - port: $provider.port || 5432 + port: $provider.port || 5432, + ...importOptions }); await invalidate(Dependencies.MIGRATIONS); break; @@ -80,7 +95,8 @@ .forProject(page.params.region, page.params.project) .migrations.createFirebaseMigration({ resources: resources as FirebaseMigrationResource[], - serviceAccount: $provider.serviceAccount + serviceAccount: $provider.serviceAccount, + ...importOptions }); await invalidate(Dependencies.MIGRATIONS); break; @@ -95,7 +111,8 @@ adminSecret: $provider.adminSecret, database: $provider.database || $provider.subdomain, username: $provider.username || 'postgres', - password: $provider.password + password: $provider.password, + ...importOptions }); await invalidate(Dependencies.MIGRATIONS); @@ -196,6 +213,46 @@ projectSdk={sdk.forProject(page.params.region, page.params.project)} /> + + {#if $formData.databases.root} +
+ + + + Migration aborts on the first existing resource (database, + table, column, index, or row). + + + + + Existing resources are left untouched. Only resources missing on + the destination are created. + + + + + Existing resources are updated to match the source. Schema drift + and row data are both reconciled. + + + +
+ {/if} {/if}