feat(migrations): expose Fail/Skip/Overwrite radios for import flows

Reworks the import-options UI in three places to match the new
OnDuplicate enum (Fail / Skip / Overwrite) shipped by the cloud worker
and migration package:

- Settings → Migrations wizard (Appwrite source): three-option radio
  group with Fail pre-selected. Wording reflects that overwrite/skip
  apply to the entire resource tree (databases, tables, columns,
  indexes, rows), not just rows.
- Tables (TablesDB) CSV import: same three-option radio replacing
  the prior two checkboxes. Wording stays document-centric since CSV
  import is row-only.
- Collections (DocumentsDB) JSON import: previously imported with no
  onDuplicate (silent default to fail). Now opens the same Import
  options dialog with Fail/Skip/Overwrite radios.

Also bumps @appwrite.io/console SDK to https://pkg.vc/-/@appwrite/
@appwrite.io/console@341620a so the OnDuplicate enum exports match
the merged backend (post 1.9.x).

Renamed OnDuplicate.Upsert -> OnDuplicate.Overwrite throughout the
console (the enum was renamed in upstream PR #11910 / migration 1.9.7;
the prior reference compiled to undefined and silently defaulted the
backend to Fail).
This commit is contained in:
Prem Palanisamy
2026-05-07 12:15:03 +01:00
parent 6c154449b1
commit 8dfd91b7d3
5 changed files with 155 additions and 53 deletions
+2 -2
View File
@@ -6,7 +6,7 @@
"name": "@appwrite/console",
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@8f00f95",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@341620a",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3",
"@appwrite.io/pink-legacy": "^1.0.3",
@@ -124,7 +124,7 @@
"@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="],
"@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@8f00f95", { "dependencies": { "json-bigint": "1.0.0" } }],
"@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@341620a", { "dependencies": { "json-bigint": "1.0.0" } }],
"@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="],
+1 -1
View File
@@ -20,7 +20,7 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@8f00f95",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@341620a",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3",
"@appwrite.io/pink-legacy": "^1.0.3",
@@ -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}
<Dialog title="Import options" bind:open={showImportOptions}>
<Layout.Stack gap="l">
<Typography.Text variant="m-400">
Choose how to handle documents that already exist in this collection.
</Typography.Text>
<Layout.Stack gap="m">
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Fail}
label="Fail on duplicate (default)">
<svelte:fragment slot="description">
Import aborts on the first document with a matching ID.
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Skip}
label="Skip existing documents">
<svelte:fragment slot="description">
Documents with matching IDs will be silently skipped.
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Overwrite}
label="Overwrite existing documents">
<svelte:fragment slot="description">
Documents with matching IDs will be updated with the imported data.
</svelte:fragment>
</Selector.Radio>
</Layout.Stack>
</Layout.Stack>
<svelte:fragment slot="footer">
<Layout.Stack direction="row" gap="s" justifyContent="flex-end">
<Button text on:click={() => (showImportOptions = false)}>Cancel</Button>
<Button on:click={startImport}>Start import</Button>
</Layout.Stack>
</svelte:fragment>
</Dialog>
<Modal
title="Custom columns"
bind:error={columnsError}
@@ -8,7 +8,16 @@
import { Container } from '$lib/layout';
import { preferences } from '$lib/stores/preferences';
import { canWriteTables, canWriteRows } from '$lib/stores/roles';
import { Dialog, Icon, Layout, Divider, Selector, Tooltip, Typography, Link } from '@appwrite.io/pink-svelte';
import {
Dialog,
Icon,
Layout,
Divider,
Selector,
Tooltip,
Typography,
Link
} from '@appwrite.io/pink-svelte';
import type { PageData } from './$types';
import {
tableColumns,
@@ -34,7 +43,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 CreateRow from '$database/table-[table]/rows/create.svelte';
import { onDestroy } from 'svelte';
import { isCloud } from '$lib/system';
@@ -57,8 +66,7 @@
let isRefreshing = false;
let showImportCSV = false;
let showImportOptions = false;
let importOverwrite = false;
let importSkip = false;
let importOnDuplicate: OnDuplicate = OnDuplicate.Fail;
let pendingFile: Models.File | null = null;
let pendingLocalFile = false;
@@ -115,8 +123,7 @@
function onSelect(file: Models.File, localFile = false) {
pendingFile = file;
pendingLocalFile = localFile;
importOverwrite = false;
importSkip = false;
importOnDuplicate = OnDuplicate.Fail;
showImportOptions = true;
}
@@ -134,8 +141,7 @@
fileId: pendingFile.$id,
resourceId: `${page.params.database}:${page.params.table}`,
internalFile: pendingLocalFile,
overwrite: importOverwrite,
skip: importSkip
onDuplicate: importOnDuplicate
});
addNotification({
@@ -459,24 +465,36 @@
Choose how to handle documents that already exist in this table.
</Typography.Text>
<Layout.Stack gap="m">
<Selector.Checkbox
<Selector.Radio
size="s"
checked={importOverwrite}
on:change={(e) => {
importOverwrite = e.detail;
if (e.detail) importSkip = false;
}}
label="Overwrite existing documents"
description="Documents with matching IDs will be updated with the imported data." />
<Selector.Checkbox
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Fail}
label="Fail on duplicate (default)">
<svelte:fragment slot="description">
Migration aborts on the first row with a matching ID.
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
checked={importSkip}
on:change={(e) => {
importSkip = e.detail;
if (e.detail) importOverwrite = false;
}}
label="Skip existing documents"
description="Documents with matching IDs will be silently skipped." />
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Skip}
label="Skip existing documents">
<svelte:fragment slot="description">
Documents with matching IDs will be silently skipped.
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Overwrite}
label="Overwrite existing documents">
<svelte:fragment slot="description">
Documents with matching IDs will be updated with the imported data.
</svelte:fragment>
</Selector.Radio>
</Layout.Stack>
</Layout.Stack>
<svelte:fragment slot="footer">
@@ -11,6 +11,7 @@
AppwriteMigrationResource,
FirebaseMigrationResource,
NHostMigrationResource,
OnDuplicate,
SupabaseMigrationResource
} from '@appwrite.io/console';
import { started } from '../stores';
@@ -39,8 +40,7 @@
import { capitalize } from '$lib/helpers/string';
import { page } from '$app/state';
let importOverwrite = false;
let importSkip = false;
let importOnDuplicate: OnDuplicate = OnDuplicate.Fail;
const onExit = () => {
resetImportStores();
@@ -51,8 +51,7 @@
const resources = migrationFormToResources($formData, $provider.provider);
const importOptions = {
overwrite: importOverwrite,
skip: importSkip
onDuplicate: importOnDuplicate
};
switch ($provider.provider) {
@@ -213,24 +212,39 @@
{#if $formData.databases.root}
<Fieldset legend="Import options">
<Layout.Stack gap="m">
<Selector.Checkbox
<Selector.Radio
size="s"
checked={importOverwrite}
on:change={(e) => {
importOverwrite = e.detail;
if (e.detail) importSkip = false;
}}
label="Overwrite existing documents"
description="Documents with matching IDs will be updated with the imported data." />
<Selector.Checkbox
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Fail}
label="Fail on duplicate (default)">
<svelte:fragment slot="description">
Migration aborts on the first existing resource (database,
table, column, index, or row).
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
checked={importSkip}
on:change={(e) => {
importSkip = e.detail;
if (e.detail) importOverwrite = false;
}}
label="Skip existing documents"
description="Documents with matching IDs will be silently skipped." />
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Skip}
label="Skip existing resources">
<svelte:fragment slot="description">
Existing resources are left untouched. Only resources missing on
the destination are created.
</svelte:fragment>
</Selector.Radio>
<Selector.Radio
size="s"
bind:group={importOnDuplicate}
name="importOnDuplicate"
value={OnDuplicate.Overwrite}
label="Overwrite existing resources">
<svelte:fragment slot="description">
Existing resources are updated to match the source. Schema drift
and row data are both reconciled.
</svelte:fragment>
</Selector.Radio>
</Layout.Stack>
</Fieldset>
{/if}