From 06b91e988a5b685619afe4bba606cb5893a5147c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 28 Jan 2026 00:14:32 +1300 Subject: [PATCH 01/56] Add dedicated databases UI and SDK --- src/lib/actions/analytics.ts | 13 +- .../components/domains/viewLogsModal.svelte | 4 +- src/lib/helpers/faker.ts | 11 +- src/lib/helpers/object.ts | 2 +- src/lib/helpers/search.ts | 16 +- src/lib/sdk/dedicatedDatabases.ts | 172 +++++++ src/lib/stores/preferences.ts | 21 +- src/lib/stores/sdk.ts | 2 + .../databases/(assets)/dark/dedicated-db.svg | 12 + .../(assets)/dark/prisma-postgres.svg | 8 + .../databases/(assets)/dedicated-db.svg | 12 + .../databases/(assets)/prisma-postgres.svg | 8 + .../databases/create/+page.svelte | 244 +++++++++- .../(entity)/helpers/sdk.ts | 67 ++- .../(entity)/helpers/terminology.ts | 24 +- .../(entity)/views/create.svelte | 10 +- .../database-[database]/+layout.svelte | 56 ++- .../databases/database-[database]/+layout.ts | 50 +- .../database-[database]/+page.svelte | 135 +++--- .../databases/database-[database]/+page.ts | 27 +- .../backups/createPolicy.svelte | 8 +- .../backups/policyPresets.svelte | 32 ++ .../editor/extensions/duplicates.ts | 2 + .../(components)/editor/view.svelte | 43 +- .../database-[database]/connectModal.svelte | 183 ++++++++ .../dedicatedOverview.svelte | 441 ++++++++++++++++++ .../database-[database]/header.svelte | 15 +- .../database-[database]/subNavigation.svelte | 166 ++++--- .../table-[table]/+layout.svelte | 7 +- .../table-[table]/spreadsheet.svelte | 50 +- .../databases/empty.svelte | 45 +- 31 files changed, 1622 insertions(+), 264 deletions(-) create mode 100644 src/lib/sdk/dedicatedDatabases.ts create mode 100644 src/routes/(console)/project-[region]-[project]/databases/(assets)/dark/dedicated-db.svg create mode 100644 src/routes/(console)/project-[region]-[project]/databases/(assets)/dark/prisma-postgres.svg create mode 100644 src/routes/(console)/project-[region]-[project]/databases/(assets)/dedicated-db.svg create mode 100644 src/routes/(console)/project-[region]-[project]/databases/(assets)/prisma-postgres.svg create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/policyPresets.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/connectModal.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/dedicatedOverview.svelte diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 5cdd782621..376253d4cd 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -75,13 +75,12 @@ export function trackEvent(name: string, data: object = null): void { } } -export function trackError(exception: Error, event: Submit): void { - if (exception instanceof AppwriteException && exception.type && event) { - trackEvent(Submit.Error, { - type: exception.type, - form: event - }); - } +export function trackError(exception: Error, event?: Submit): void { + if (!(exception instanceof AppwriteException) || !exception.type) return; + + const data: Record = { type: exception.type }; + if (event) data.form = event; + trackEvent(Submit.Error, data); } export function trackPageView(path: string) { diff --git a/src/lib/components/domains/viewLogsModal.svelte b/src/lib/components/domains/viewLogsModal.svelte index 3f558c71a0..9d53b088a5 100644 --- a/src/lib/components/domains/viewLogsModal.svelte +++ b/src/lib/components/domains/viewLogsModal.svelte @@ -36,8 +36,8 @@ domainId: domain.$id }); } - } catch { - // Ignore error + } catch (e) { + console.warn('[viewLogsModal] Failed to update nameservers:', e?.message ?? e); } try { diff --git a/src/lib/helpers/faker.ts b/src/lib/helpers/faker.ts index 07d9ade073..6474b77523 100644 --- a/src/lib/helpers/faker.ts +++ b/src/lib/helpers/faker.ts @@ -73,9 +73,14 @@ export async function generateFields( ]); } - case 'documentsdb': /* doesn't need any fields */ - case 'vectordb': /* vector embeddings + metadata defined at collection creation */ { - /* no individual field creation needed */ + /** + * Schema-less database types that don't require individual field creation: + * - documentsdb: Flexible document structure without predefined schema + * - vectordb: Vector embeddings and metadata are defined at collection creation + * @returns Empty array since no individual field creation is needed + */ + case 'documentsdb': + case 'vectordb': { return []; } } diff --git a/src/lib/helpers/object.ts b/src/lib/helpers/object.ts index ce28b5a6e3..2e26e49bf7 100644 --- a/src/lib/helpers/object.ts +++ b/src/lib/helpers/object.ts @@ -68,7 +68,7 @@ export function parseIfString(value: unknown): unknown { export function areObjectsSame( objectOne: T, objectTwo: T, - method: 'recursive' | 'stringify' = 'stringify' + method: 'recursive' | 'stringify' = 'recursive' ): boolean { if (method === 'recursive') { return deepEqual(objectOne, objectTwo); diff --git a/src/lib/helpers/search.ts b/src/lib/helpers/search.ts index 66f662bf31..2d19291e85 100644 --- a/src/lib/helpers/search.ts +++ b/src/lib/helpers/search.ts @@ -1,5 +1,10 @@ import type { Models } from '@appwrite.io/console'; +/** Minimum documents needed for reliable key frequency analysis */ +const MIN_SAMPLE_SIZE = 5; +/** Maximum documents to sample for performance */ +const DOC_SAMPLE_LIMIT = 5; + type FuzzySearchOptions = { limit?: number; minOccurrences?: number | null; @@ -12,17 +17,20 @@ export function fuzzySearchKeys( documents: Models.Document[], options: FuzzySearchOptions = {} ): string[] | null { - if (!documents || documents.length < 5) { + if (!documents || documents.length < MIN_SAMPLE_SIZE) { return null; } const { minOccurrences = 2, limit } = options; const attributeCount = new Map(); - const threshold = minOccurrences === null ? 5 : Math.max(2, Math.min(minOccurrences, 5)); + const threshold = + minOccurrences === null + ? MIN_SAMPLE_SIZE + : Math.max(2, Math.min(minOccurrences, MIN_SAMPLE_SIZE)); - // Process only first 5 documents - const docLimit = Math.min(5, documents.length); + // Process only first DOC_SAMPLE_LIMIT documents + const docLimit = Math.min(DOC_SAMPLE_LIMIT, documents.length); for (let docIndex = 0; docIndex < docLimit; docIndex++) { const document = documents[docIndex]; diff --git a/src/lib/sdk/dedicatedDatabases.ts b/src/lib/sdk/dedicatedDatabases.ts new file mode 100644 index 0000000000..20442e3974 --- /dev/null +++ b/src/lib/sdk/dedicatedDatabases.ts @@ -0,0 +1,172 @@ +import type { Client } from '@appwrite.io/console'; + +export type DedicatedDatabase = { + $id: string; + $createdAt: string; + $updatedAt: string; + name: string; + engine: 'postgres' | 'mysql' | 'mariadb'; + version: string; + type: 'shared' | 'dedicated'; + region: string; + tier: string; + backend: 'prisma' | 'appwrite'; + cpu: number; + memory: number; + storage: number; + storageClass: string; + hostname: string; + port: number; + status: 'provisioning' | 'ready' | 'failed' | 'deleting' | 'deleted'; + containerStatus: 'inactive' | 'starting' | 'running' | null; + projectId: string; + highAvailability: boolean; + haReplicaCount: number; + haSyncMode: string | null; + networkMaxConnections: number; + networkIdleTimeoutSeconds: number; + networkIPAllowlist: string[]; + idleTimeoutMinutes: number | null; + backupEnabled: boolean; + backupPitr: boolean; + backupCron: string; + backupRetentionDays: number; + metricsEnabled: boolean; + error?: string; +}; + +export type DedicatedDatabaseList = { + total: number; + databases: DedicatedDatabase[]; +}; + +export type DedicatedDatabaseCredentials = { + username: string; + password: string; + host: string; + port: number; + database: string; + connectionString: string; +}; + +export type CreateDedicatedDatabaseParams = { + databaseId: string; + name: string; + engine?: 'postgres' | 'mysql' | 'mariadb'; + version?: string; + region?: string; + type?: 'shared' | 'dedicated'; + tier?: string; + backend: 'prisma' | 'appwrite'; + cpu?: number; + memory?: number; + storage?: number; + storageClass?: string; + highAvailability?: boolean; + haReplicaCount?: number; + haSyncMode?: string; + networkMaxConnections?: number; + networkIdleTimeoutSeconds?: number; + networkIPAllowlist?: string[]; + idleTimeoutMinutes?: number; + backupEnabled?: boolean; + backupPitr?: boolean; + backupSchedule?: string; + backupRetentionDays?: number; + metricsEnabled?: boolean; +}; + +export class DedicatedDatabases { + client: Client; + + constructor(client: Client) { + this.client = client; + } + + async create(params: CreateDedicatedDatabaseParams): Promise { + const path = `/compute/databases`; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'POST', + uri, + { + 'content-type': 'application/json' + }, + { + databaseId: params.databaseId, + name: params.name, + engine: params.engine ?? 'postgres', + version: params.version, + region: params.region ?? 'fra', + type: params.type ?? 'shared', + tier: params.tier ?? 'starter', + backend: params.backend, + cpu: params.cpu, + memory: params.memory, + storage: params.storage, + storageClass: params.storageClass, + highAvailability: params.highAvailability, + haReplicaCount: params.haReplicaCount, + haSyncMode: params.haSyncMode, + networkMaxConnections: params.networkMaxConnections, + networkIdleTimeoutSeconds: params.networkIdleTimeoutSeconds, + networkIPAllowlist: params.networkIPAllowlist, + idleTimeoutMinutes: params.idleTimeoutMinutes, + backupEnabled: params.backupEnabled, + backupPitr: params.backupPitr, + backupCron: params.backupSchedule, + backupRetentionDays: params.backupRetentionDays, + metricsEnabled: params.metricsEnabled + } + ); + } + + async get(databaseId: string): Promise { + const path = `/compute/databases/${databaseId}`; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call('GET', uri, { + 'content-type': 'application/json' + }); + } + + async list(queries: string[] = [], search?: string): Promise { + const path = `/compute/databases`; + const params: Record = {}; + if (queries.length > 0) params.queries = queries; + if (search) params.search = search; + + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'GET', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async delete(params: { databaseId: string }): Promise { + const path = `/compute/databases/${params.databaseId}`; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call('DELETE', uri, { + 'content-type': 'application/json' + }); + } + + async getCredentials(databaseId: string): Promise { + const path = `/compute/databases/${databaseId}/credentials`; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call('GET', uri, { + 'content-type': 'application/json' + }); + } + + async coldStart(databaseId: string): Promise { + const path = `/compute/databases/${databaseId}/cold-start`; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call('POST', uri, { + 'content-type': 'application/json' + }); + } +} diff --git a/src/lib/stores/preferences.ts b/src/lib/stores/preferences.ts index 0c56fd9b50..311e4bd440 100644 --- a/src/lib/stores/preferences.ts +++ b/src/lib/stores/preferences.ts @@ -218,14 +218,11 @@ function createPreferences() { }), // `databaseType` fallback for legacy cases. - deleteEntityDetails: async ( - orgId: string, - entityId: string, - databaseType: string = 'tables' - ) => { + deleteEntityDetails: async (orgId: string, entityId: string, databaseType?: string) => { + const dbType = databaseType ?? 'tables'; // remove from account preferences const removeCustomTableColumns = updateAndSync((n) => { - n = ensureObjectProperty(n, databaseType); + n = ensureObjectProperty(n, dbType); delete n.tables[entityId]; return n; }); @@ -235,9 +232,9 @@ function createPreferences() { delete teamPreferences?.columnWidths?.[entityId + '#columns']; delete teamPreferences?.columnWidths?.[entityId + '#indexes']; - if (teamPreferences.displayNames?.[databaseType]?.[entityId]) { + if (teamPreferences.displayNames?.[dbType]?.[entityId]) { // new structure - delete teamPreferences?.displayNames?.[databaseType]?.[entityId]; + delete teamPreferences?.displayNames?.[dbType]?.[entityId]; } else { // legacy structure delete teamPreferences?.displayNames?.[entityId]; @@ -253,9 +250,9 @@ function createPreferences() { loadTeamPrefs: loadTeamPreferences, - getDisplayNames: (entityId: string, databaseType: string = null) => { + getDisplayNames: (entityId: string, databaseType?: string) => { let names = teamPreferences?.displayNames?.[entityId]; - if (databaseType) { + if (databaseType != null) { names = teamPreferences?.displayNames?.[databaseType]?.[entityId]; } @@ -266,10 +263,10 @@ function createPreferences() { orgId: string, entityId: string, displayNames: TeamPreferences['names'], - databaseType: string = null + databaseType?: string ) => { teamPreferences = ensureObjectProperty(teamPreferences, 'displayNames'); - if (!databaseType) { + if (databaseType == null) { // legacy! teamPreferences.displayNames[entityId] = displayNames; } else { diff --git a/src/lib/stores/sdk.ts b/src/lib/stores/sdk.ts index bfe73f6080..ccd3bccbb1 100644 --- a/src/lib/stores/sdk.ts +++ b/src/lib/stores/sdk.ts @@ -29,6 +29,7 @@ import { } from '@appwrite.io/console'; import { Billing } from '../sdk/billing'; import { Sources } from '$lib/sdk/sources'; +import { DedicatedDatabases } from '$lib/sdk/dedicatedDatabases'; import { REGION_FRA, REGION_NYC, @@ -139,6 +140,7 @@ const sdkForProject = { sites: new Sites(clientProject), tablesDB: new TablesDB(clientProject), documentsDB: new DocumentsDB(clientProject), + dedicatedDatabases: new DedicatedDatabases(clientProject), console: new Console(clientProject) // for suggestions API }; diff --git a/src/routes/(console)/project-[region]-[project]/databases/(assets)/dark/dedicated-db.svg b/src/routes/(console)/project-[region]-[project]/databases/(assets)/dark/dedicated-db.svg new file mode 100644 index 0000000000..5e07417621 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/(assets)/dark/dedicated-db.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + Dedicated Database + diff --git a/src/routes/(console)/project-[region]-[project]/databases/(assets)/dark/prisma-postgres.svg b/src/routes/(console)/project-[region]-[project]/databases/(assets)/dark/prisma-postgres.svg new file mode 100644 index 0000000000..5748b0f2db --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/(assets)/dark/prisma-postgres.svg @@ -0,0 +1,8 @@ + + + + + + + Prisma Postgres + diff --git a/src/routes/(console)/project-[region]-[project]/databases/(assets)/dedicated-db.svg b/src/routes/(console)/project-[region]-[project]/databases/(assets)/dedicated-db.svg new file mode 100644 index 0000000000..8cdf27a121 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/(assets)/dedicated-db.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + Dedicated Database + diff --git a/src/routes/(console)/project-[region]-[project]/databases/(assets)/prisma-postgres.svg b/src/routes/(console)/project-[region]-[project]/databases/(assets)/prisma-postgres.svg new file mode 100644 index 0000000000..7cdc40d6da --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/(assets)/prisma-postgres.svg @@ -0,0 +1,8 @@ + + + + + + + Prisma Postgres + diff --git a/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte index 57d80762e0..92aff1e142 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte @@ -1,7 +1,7 @@ @@ -194,11 +328,51 @@ + {#if showRegionSelect} +
+ + {#if showEngineSelect} + + {/if} + + + + {#if showTierSelect} + + + + {/if} + +
+ {/if} +
- {#if isCloud} - {@render cloudBackupOptions()} - {:else} - {@render selfHostedBackupOptions()} + {#if backupSystem === 'appwrite'} + {#if isCloud} + {@render cloudBackupOptions()} + {:else} + {@render selfHostedBackupOptions()} + {/if} + {:else if backupSystem === 'prisma'} + {@render prismaBackupOptions()} + {:else if backupSystem === 'dedicated'} + {@render dedicatedBackupOptions()} {/if}
@@ -271,6 +445,46 @@ {/snippet} +{#snippet prismaBackupOptions()} + + + Prisma Postgres automatically creates daily snapshots with 30-day retention. Backups are + managed by Prisma and cannot be customized. + + +{/snippet} + +{#snippet dedicatedBackupOptions()} + + + + {#if backupEnabled} + + + + + {#if backupPitr} + + PITR allows you to restore your database to any point within the retention + window using WAL archiving. This provides more granular recovery options but + increases storage usage. + + {/if} + {/if} + +{/snippet} + {#snippet selectDatabaseType()} {#each databaseTypes as databaseType} @@ -283,7 +497,7 @@ value={databaseType.type} title={databaseType.title} imageRadius="s" - icon={databaseType.type === 'documentsdb' ? Mongo : undefined}> + icon={databaseType.icon}> {databaseType.subtitle} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts index 13f8da3bc4..e44690aaec 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts @@ -12,14 +12,30 @@ import { } from './terminology'; import type { Models } from '@appwrite.io/console'; +export type DedicatedDatabaseParams = { + databaseId: string; + name: string; + enabled?: boolean; + engine?: 'postgres' | 'mysql' | 'mariadb'; + region?: string; + tier?: string; + highAvailability?: boolean; + backupEnabled?: boolean; + backupSchedule?: string; + backupRetentionDays?: number; + backupPitr?: boolean; +}; + export type DatabaseSdkResult = { create: ( type: DatabaseType, - params: { - databaseId: string; - name: string; - enabled?: boolean; - } + params: + | { + databaseId: string; + name: string; + enabled?: boolean; + } + | DedicatedDatabaseParams ) => Promise; list: (params: { queries?: string[]; search?: string }) => Promise; createEntity: (params: { @@ -96,7 +112,12 @@ export function useDatabaseSdk( region = regionOrPage?.params?.region || ''; project = regionOrPage?.params?.project || ''; } else { - type = databaseType!; + if (!databaseType) { + throw new Error( + 'databaseType is required when passing string parameters to useDatabaseSdk' + ); + } + type = databaseType; region = regionOrPage as string; project = projectOrTerminology as string; } @@ -113,6 +134,37 @@ export function useDatabaseSdk( case 'documentsdb': { return await baseSdk.documentsDB.create(params); } + case 'prismapostgres': { + // Prisma databases are created via the compute/databases endpoint + // with backend: 'prisma' + const prismaParams = params as DedicatedDatabaseParams; + return await baseSdk.dedicatedDatabases.create({ + databaseId: prismaParams.databaseId, + name: prismaParams.name, + backend: 'prisma', + engine: 'postgres', + region: prismaParams.region, + tier: prismaParams.tier + }); + } + case 'dedicateddb': { + // Dedicated databases are created via the compute/databases endpoint + // with backend: 'appwrite' + const dedicatedParams = params as DedicatedDatabaseParams; + return await baseSdk.dedicatedDatabases.create({ + databaseId: dedicatedParams.databaseId, + name: dedicatedParams.name, + backend: 'appwrite', + engine: dedicatedParams.engine, + region: dedicatedParams.region, + tier: dedicatedParams.tier, + highAvailability: dedicatedParams.highAvailability, + backupEnabled: dedicatedParams.backupEnabled, + backupSchedule: dedicatedParams.backupSchedule, + backupRetentionDays: dedicatedParams.backupRetentionDays, + backupPitr: dedicatedParams.backupPitr + }); + } case 'vectordb': throw new Error('Database type not supported yet'); default: @@ -212,6 +264,9 @@ export function useDatabaseSdk( return await baseSdk.tablesDB.delete(params); case 'documentsdb': return await baseSdk.documentsDB.delete(params); + case 'prismapostgres': + case 'dedicateddb': + return await baseSdk.dedicatedDatabases.delete(params); case 'vectordb': throw new Error('Database type not supported yet'); default: diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/terminology.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/terminology.ts index 005a4ca69d..264255cb43 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/terminology.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/terminology.ts @@ -1,14 +1,20 @@ import type { Page } from '@sveltejs/kit'; import { capitalize, plural } from '$lib/helpers/string'; -import { AppwriteException, type Models } from '@appwrite.io/console'; +import type { Models } from '@appwrite.io/console'; import type { Attributes, Collection, Columns, Table } from '$database/store'; import type { Term, TerminologyResult, TerminologyShape } from '$database/(entity)/helpers/types'; type BaseTerminology = typeof baseTerminology; type ImplementedDBTypes = Omit; -export type DatabaseType = 'legacy' | 'tablesdb' | 'documentsdb' | 'vectordb'; +export type DatabaseType = + | 'legacy' + | 'tablesdb' + | 'documentsdb' + | 'vectordb' + | 'prismapostgres' + | 'dedicateddb'; export type RecordType = ImplementedDBTypes[keyof ImplementedDBTypes]['record']; @@ -60,7 +66,17 @@ export const baseTerminology = { field: 'attribute', record: 'document' }, - vectordb: {} + vectordb: {}, + prismapostgres: { + entity: 'table', + field: 'column', + record: 'row' + }, + dedicateddb: { + entity: 'table', + field: 'column', + record: 'row' + } } as const; const createTerm = (singular: string, pluralForm: string): Term => { @@ -145,7 +161,7 @@ export function useTerminology(pageOrType: Page | DatabaseType): TerminologyResu : pageOrType; if (!type) { // strict check because this should always be available! - throw new AppwriteException('Database type is required', 500); + throw new Error('Database type is required for terminology lookup'); } const dbTerminologies = terminologyData[type] || {}; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/create.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/create.svelte index df2ddb2137..eec6a4f781 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/create.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/create.svelte @@ -91,6 +91,14 @@ show = false; } + /** + * Converts string to valid Appwrite ID format matching backend rules: + * - Lowercase alphanumeric characters, hyphens, underscores, and dots only + * - Cannot start with a hyphen + * - Cannot end with a dot + * - Consecutive underscores collapsed to single underscore + * - Maximum 36 characters + */ function toIdFormat(str: string): string { return str .toLowerCase() @@ -99,7 +107,7 @@ .replace(/^-+/, '') .replace(/\.+$/, '') .replace(/_{2,}/g, '_') - .slice(0, 36); // max length + .slice(0, 36); } $effect(() => { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte index 8625ffc703..8ea1f2a732 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte @@ -30,6 +30,10 @@ const { databaseSdk, terminology } = getTerminologies(); + // Check if this is a dedicated database type + $: isDedicatedType = + terminology.type === 'prismapostgres' || terminology.type === 'dedicateddb'; + $: $registerCommands([ { label: 'Create table', @@ -42,7 +46,8 @@ } }, keys: page.url.pathname.endsWith(databaseId) ? ['c'] : ['c', 'c'], - disabled: page.url.pathname.includes('table-') || !$canWriteTables, + // Disable for dedicated databases - they don't have tables/collections + disabled: page.url.pathname.includes('table-') || !$canWriteTables || isDedicatedType, group: 'databases', icon: IconPlus }, @@ -79,7 +84,8 @@ disabled: !isCloud || !$currentPlan?.backupsEnabled }, { - label: 'Go to tables', + // For dedicated DBs, show "Go to overview" instead of "Go to tables" + label: isDedicatedType ? 'Go to overview' : 'Go to tables', callback() { goto( `${base}/project-${page.params.region}-${project}/databases/database-${databaseId}` @@ -133,11 +139,16 @@ addSubPanel(TablesPanel); }, group: 'databases', - rank: -1 + rank: -1, + // Disable for dedicated databases + disabled: isDedicatedType } ]); - $registerSearchers(tablesSearcher); + // Only register table searcher for non-dedicated databases + if (!isDedicatedType) { + $registerSearchers(tablesSearcher); + } $: $updateCommandGroupRanks({ tables: 10 }); @@ -172,23 +183,26 @@ - + +{#if !isDedicatedType} + - - {@const records = terminology.record.lower.singular} - - - Select how many sample {records} to generate for testing. This won't delete or replace any - existing {records}. - + + {@const records = terminology.record.lower.singular} + + + Select how many sample {records} to generate for testing. This won't delete or replace + any existing {records}. + - - - - - - - + - - + + + + + + + + +{/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.ts index 11adb874c6..fe611d2fa6 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.ts @@ -4,16 +4,60 @@ import type { LayoutLoad } from './$types'; import { Dependencies } from '$lib/constants'; import Breadcrumbs from './breadcrumbs.svelte'; import SubNavigation from './subNavigation.svelte'; +import type { DedicatedDatabase, DedicatedDatabaseCredentials } from '$lib/sdk/dedicatedDatabases'; +import type { Models } from '@appwrite.io/console'; + +type DatabaseWithType = Models.Database & { + type?: string; +}; + +function isDedicatedDatabaseType(type: string | undefined): boolean { + return type === 'prismapostgres' || type === 'dedicateddb'; +} export const load: LayoutLoad = async ({ params, depends }) => { depends(Dependencies.DATABASE); - const database = await sdk.forProject(params.region, params.project).tablesDB.get({ - databaseId: params.database - }); + const projectSdk = sdk.forProject(params.region, params.project); + + // Try to get from tablesDB first (handles legacy, tablesdb, documentsdb) + let database: DatabaseWithType | DedicatedDatabase; + let dedicatedDatabase: DedicatedDatabase | null = null; + let credentials: DedicatedDatabaseCredentials | null = null; + + try { + database = await projectSdk.tablesDB.get({ + databaseId: params.database + }); + } catch { + // If not found in tablesDB, try dedicated databases + database = await projectSdk.dedicatedDatabases.get(params.database); + dedicatedDatabase = database as DedicatedDatabase; + } + + // If it's a dedicated database type, fetch additional details + const dbType = database.type as string | undefined; + if (isDedicatedDatabaseType(dbType) && !dedicatedDatabase) { + try { + dedicatedDatabase = await projectSdk.dedicatedDatabases.get(params.database); + } catch { + // Fallback - dedicated details not available + } + } + + // Fetch credentials for dedicated databases + if (dedicatedDatabase) { + try { + credentials = await projectSdk.dedicatedDatabases.getCredentials(params.database); + } catch { + // Credentials not available yet (e.g., still provisioning) + } + } return { database, + dedicatedDatabase, + credentials, header: Header, breadcrumbs: Breadcrumbs, subNavigation: SubNavigation diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+page.svelte index 865cdbeff3..0f6870e1f4 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+page.svelte @@ -14,6 +14,7 @@ import { resolveRoute } from '$lib/stores/navigation'; import { getTerminologies } from '$database/(entity)'; import { withPath } from '$lib/stores/navigation.js'; + import DedicatedOverview from './dedicatedOverview.svelte'; const { data }: PageProps = $props(); @@ -49,78 +50,82 @@ }); - - - - - +{#if data.isDedicatedType && data.dedicatedDatabase} + +{:else} + + + + + - - + + - {#if $canWriteTables} - - {/if} + {#if $canWriteTables} + + {/if} + - - {#if data.entities.total} - {#if data.view === 'grid'} - - {:else} - - {/if} + {#if data.entities.total} + {#if data.view === 'grid'} + + {:else} +
+ {/if} - - {:else if data.search} - - - - {:else} - -
- - {emptyPageText} + + {:else if data.search} + + + + {:else} + +
+ + {emptyPageText} - - + + - {#if $canWriteTables} - - {/if} - - -
-
- {/if} - + {#if $canWriteTables} + + {/if} + +
+
+
+ {/if} + +{/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/dedicatedOverview.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/dedicatedOverview.svelte new file mode 100644 index 0000000000..80af11f4c1 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/dedicatedOverview.svelte @@ -0,0 +1,441 @@ + + + + + + Status + + + + + Database Status + + + + {#if database.containerStatus} + + Container + + + {/if} + + + {#if database.error} + + {database.error} + + {/if} + +
+

Created: {toLocaleDateTime(database.$createdAt)}

+

Last updated: {toLocaleDateTime(database.$updatedAt)}

+
+
+
+ + {#if database.containerStatus === 'inactive'} + + {/if} + + +
+ + + {#if database.status === 'ready' && credentials} + + Connection Settings + Use these credentials to connect to your database directly. + + + + + + + + +
+ +
+ + + +
+
+ + +
+
+ + + +
+ {:else if database.status === 'provisioning'} + + Connection Settings + + + + Your database is being set up. Connection details will be available once + provisioning is complete. + + + + + + + + + + {/if} + + + + Resources + + + + + Engine + {getEngineDisplayName(database.engine)} + {database.version} + + + Tier + {database.tier.charAt(0).toUpperCase() + + database.tier.slice(1)} + + + CPU + {cpuDisplay} + + + Memory + {memoryDisplay} + + + Storage + {storageDisplay} + + + Storage Class + {database.storageClass} + + + + + + + + + High Availability + + + + + Status + + + {#if database.highAvailability} + + Replica Count + {database.haReplicaCount} + + {#if database.haSyncMode} + + Sync Mode + {database.haSyncMode} + + {/if} + {/if} + + + + + + + + Network + + + + + Max Connections + {database.networkMaxConnections} + + + Idle Timeout + {database.networkIdleTimeoutSeconds}s + + {#if database.idleTimeoutMinutes} + + Sleep After Idle + {database.idleTimeoutMinutes} min + + {/if} + + + {#if database.networkIPAllowlist?.length > 0} + + IP Allowlist + + {#each database.networkIPAllowlist as ip} + {ip} + {/each} + + + {/if} + + + + + + + Backups + + + + + Status + + + {#if database.backupEnabled} + + Point-in-Time Recovery + + + + Schedule + {database.backupCron} + + + Retention + {database.backupRetentionDays} days + + {/if} + + + + +
+ + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/header.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/header.svelte index 1d3ded6c0d..5311b5904f 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/header.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/header.svelte @@ -5,7 +5,7 @@ import { isTabSelected } from '$lib/helpers/load'; import { canWriteDatabases } from '$lib/stores/roles'; import { resolveRoute, withPath } from '$lib/stores/navigation'; - import { useTerminology } from '$database/(entity)'; + import { useTerminology, type DatabaseType } from '$database/(entity)'; import { isSmallViewport } from '$lib/stores/viewport'; const terminology = useTerminology(page); @@ -20,13 +20,20 @@ page.params ); + // Check if this is a dedicated database type + const isDedicatedType = $derived( + (database?.type as DatabaseType) === 'prismapostgres' || + (database?.type as DatabaseType) === 'dedicateddb' + ); + const tabs = $derived( [ { href: baseDatabasePath, - title: terminology.entity.title.plural, - event: terminology.entity.lower.plural, - hasChildren: true + // For dedicated DBs, show "Overview" instead of Tables/Collections + title: isDedicatedType ? 'Overview' : terminology.entity.title.plural, + event: isDedicatedType ? 'overview' : terminology.entity.lower.plural, + hasChildren: !isDedicatedType }, { href: withPath(baseDatabasePath, '/backups'), diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/subNavigation.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/subNavigation.svelte index 011dc9a88a..66a32416a0 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/subNavigation.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/subNavigation.svelte @@ -42,6 +42,11 @@ const terminology = useTerminology(page); const databaseSdk = useDatabaseSdk(page, terminology); + // Check if this is a dedicated database type + const isDedicatedType = $derived( + terminology.type === 'prismapostgres' || terminology.type === 'dedicateddb' + ); + const entityTypePlural = terminology.entity.lower.plural; const entityTypeSingular = terminology.entity.lower.singular; @@ -78,6 +83,12 @@ ); async function loadEntities() { + // Don't load entities for dedicated databases - they don't have tables/collections + if (isDedicatedType) { + loading = false; + return; + } + try { entities = await databaseSdk.listEntities({ databaseId: page.params.database, @@ -113,83 +124,86 @@ {data.database?.name} -
- {#if loading} -
    - {#each Array(2) as _} - -
  • -
    - -
    -
  • + + {#if !isDedicatedType} +
    + {#if loading} +
      + {#each Array(2) as _} + +
    • +
      + +
      +
    • +
      + {/each} +
    + {:else if entities?.total} +
      + {#each sortedEntities as entity, index} + {@const isFirst = index === 0} + {@const isSelected = entityId === entity.$id} + {@const isLast = index === sortedEntities.length - 1} + {@const href = withPath( + databaseBaseRoute, + `/${entityTypeSingular}-${entity.$id}` + )} + + +
    • + + + {entity.name} + +
    • +
      + {/each} +
    + {:else} +
    + +
    +
    + No {entityTypePlural} yet
    - {/each} -
- {:else if entities?.total} -
    - {#each sortedEntities as entity, index} - {@const isFirst = index === 0} - {@const isSelected = entityId === entity.$id} - {@const isLast = index === sortedEntities.length - 1} - {@const href = withPath( - databaseBaseRoute, - `/${entityTypeSingular}-${entity.$id}` - )} - - -
  • - - - {entity.name} - -
  • -
    - {/each} -
- {:else} -
- -
-
- No {entityTypePlural} yet -
-
- {/if} - - - - - -
+ + {/if} + + + + + + + {/if}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte index 52e5533ecd..af7688b5ef 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte @@ -161,8 +161,13 @@ label: 'Create row', keys: page.url.pathname.endsWith(table?.$id) ? ['r'] : ['r', 'd'], callback: () => { - if (table.fields) { + if (table.fields?.length > 0) { $showRowCreateSheet.show = true; + } else { + addNotification({ + type: 'warning', + message: 'Cannot create rows: table has no fields' + }); } }, icon: IconPlus, diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte index 505ff8df0b..07dcd899ad 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte @@ -398,7 +398,8 @@ async function handleDelete() { showDelete = false; - let hadErrors = false; + let successCount = 0; + let failedCount = 0; try { if (selectedRowForDelete) { @@ -407,6 +408,7 @@ tableId, rowId: selectedRowForDelete }); + successCount = 1; } else { if (selectedRows.length) { const hasAnyRelationships = table.fields.some(isRelationship) ?? false; @@ -418,28 +420,23 @@ if (hasAnyRelationships) { for (const batch of chunks(selectedRows)) { - try { - await Promise.all( - batch.map((rowId) => - tablesSDK.deleteRow({ - databaseId, - tableId, - rowId - }) - ) - ); - } catch (e) { - hadErrors = true; - // ignore but keep proceeding! + const results = await Promise.allSettled( + batch.map((rowId) => + tablesSDK.deleteRow({ + databaseId, + tableId, + rowId + }) + ) + ); + for (const result of results) { + if (result.status === 'fulfilled') { + successCount++; + } else { + failedCount++; + } } } - - if (hadErrors) { - addNotification({ - type: 'error', - message: 'Some rows could not be deleted' - }); - } } else { for (const batch of chunks(selectedRows, 100)) { await tablesSDK.deleteRows({ @@ -447,6 +444,7 @@ tableId, queries: [Query.equal('$id', batch)] }); + successCount += batch.length; } } } @@ -455,11 +453,15 @@ await invalidate(Dependencies.ROWS); trackEvent(Click.DatabaseRowDelete); - if (!hadErrors) { - // error is already shown above! + if (failedCount > 0) { + addNotification({ + type: 'warning', + message: `${successCount} row${successCount !== 1 ? 's' : ''} deleted, ${failedCount} failed` + }); + } else if (successCount > 0) { addNotification({ type: 'success', - message: `${selectedRows.length ? selectedRows.length : 1} row${selectedRows.length > 1 ? 's' : ''} deleted` + message: `${successCount} row${successCount !== 1 ? 's' : ''} deleted` }); } diff --git a/src/routes/(console)/project-[region]-[project]/databases/empty.svelte b/src/routes/(console)/project-[region]-[project]/databases/empty.svelte index c7a3ab274d..45e12b795c 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/empty.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/empty.svelte @@ -14,6 +14,12 @@ import DocumentsDB from './(assets)/documents-db.svg'; import DocumentsDBDark from './(assets)/dark/documents-db.svg'; + import PrismaPostgres from './(assets)/prisma-postgres.svg'; + import PrismaPostgresDark from './(assets)/dark/prisma-postgres.svg'; + + import DedicatedDB from './(assets)/dedicated-db.svg'; + import DedicatedDBDark from './(assets)/dark/dedicated-db.svg'; + import { isSmallViewport } from '$lib/stores/viewport'; import type { DatabaseType } from '$database/(entity)'; @@ -29,6 +35,8 @@ const mongoDbImage = $derived(isDark ? MongoDBDark : MongoDB); const tablesDbImage = $derived(isDark ? TablesDBDark : TablesDB); const documentsDbImage = $derived(isDark ? DocumentsDBDark : DocumentsDB); + const prismaPostgresImage = $derived(isDark ? PrismaPostgresDark : PrismaPostgres); + const dedicatedDbImage = $derived(isDark ? DedicatedDBDark : DedicatedDB); {#if $isSmallViewport} @@ -66,12 +74,32 @@ subtitle: 'Store flexible data without a fixed schema. Best for unstructured data and simple querying.', image: documentsDbImage, - footer: true + footerType: 'mongodb' + })} + + + {@render databaseTypeCard({ + type: 'prismapostgres', + title: 'Prisma Postgres', + subtitle: + 'Managed PostgreSQL with direct connections. Best for high-performance SQL workloads.', + image: prismaPostgresImage, + footerType: 'prisma' + })} + + + {@render databaseTypeCard({ + type: 'dedicateddb', + title: 'DedicatedDB', + subtitle: + 'Always-on dedicated database instances with high availability. Best for production workloads.', + image: dedicatedDbImage, + footerType: 'appwrite' })} {/snippet} -{#snippet databaseTypeCard({ type, title, subtitle, image = undefined, footer = false })} +{#snippet databaseTypeCard({ type, title, subtitle, image = undefined, footerType = undefined })} - {#if footer} + {#if footerType === 'mongodb'} Powered by - mongo-db artwork + {:else if footerType === 'prisma'} + Powered by + prisma artwork + {:else if footerType === 'appwrite'} + Powered by Appwrite {/if} From 679ea26e3af6036efea20c72d2633845fc547eb5 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 28 Jan 2026 00:14:48 +1300 Subject: [PATCH 02/56] Update database collection and table views for dedicated DB support --- .../database-[database]/(entity)/helpers/sdk.ts | 13 +++++++------ .../collection-[collection]/+layout.ts | 8 ++++++-- .../collection-[collection]/indexes/+page.svelte | 3 ++- .../collection-[collection]/spreadsheet.svelte | 8 ++++++-- .../database-[database]/table-[table]/+layout.ts | 8 ++++++-- .../table-[table]/spreadsheet.svelte | 4 +--- 6 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts index e44690aaec..32af9b0b90 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts @@ -138,20 +138,20 @@ export function useDatabaseSdk( // Prisma databases are created via the compute/databases endpoint // with backend: 'prisma' const prismaParams = params as DedicatedDatabaseParams; - return await baseSdk.dedicatedDatabases.create({ + return (await baseSdk.dedicatedDatabases.create({ databaseId: prismaParams.databaseId, name: prismaParams.name, backend: 'prisma', engine: 'postgres', region: prismaParams.region, tier: prismaParams.tier - }); + })) as unknown as Models.Database; } case 'dedicateddb': { // Dedicated databases are created via the compute/databases endpoint // with backend: 'appwrite' const dedicatedParams = params as DedicatedDatabaseParams; - return await baseSdk.dedicatedDatabases.create({ + return (await baseSdk.dedicatedDatabases.create({ databaseId: dedicatedParams.databaseId, name: dedicatedParams.name, backend: 'appwrite', @@ -163,7 +163,7 @@ export function useDatabaseSdk( backupSchedule: dedicatedParams.backupSchedule, backupRetentionDays: dedicatedParams.backupRetentionDays, backupPitr: dedicatedParams.backupPitr - }); + })) as unknown as Models.Database; } case 'vectordb': throw new Error('Database type not supported yet'); @@ -257,7 +257,7 @@ export function useDatabaseSdk( } }, - async delete(params) { + async delete(params): Promise<{}> { switch (type ?? params.databaseType) { case 'legacy': /* databases api */ case 'tablesdb': @@ -266,7 +266,8 @@ export function useDatabaseSdk( return await baseSdk.documentsDB.delete(params); case 'prismapostgres': case 'dedicateddb': - return await baseSdk.dedicatedDatabases.delete(params); + await baseSdk.dedicatedDatabases.delete(params); + return {}; case 'vectordb': throw new Error('Database type not supported yet'); default: diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.ts index c1f38e5bab..fefafdd8c9 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.ts @@ -1,13 +1,17 @@ import Header from './header.svelte'; import type { LayoutLoad } from './$types'; import { Dependencies } from '$lib/constants'; -import { Breadcrumbs, useDatabaseSdk } from '$database/(entity)'; +import { Breadcrumbs, useDatabaseSdk, type DatabaseType } from '$database/(entity)'; export const load: LayoutLoad = async ({ params, depends, parent }) => { const { database } = await parent(); depends(Dependencies.COLLECTION); - const databaseSdk = useDatabaseSdk(params.region, params.project, database.type); + const databaseSdk = useDatabaseSdk( + params.region, + params.project, + database.type as DatabaseType + ); const collection = await databaseSdk.getEntity({ databaseId: params.database, diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/indexes/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/indexes/+page.svelte index db6e52dfe9..4e88363711 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/indexes/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/indexes/+page.svelte @@ -4,6 +4,7 @@ import type { PageProps } from './$types'; import { type CreateIndexesCallbackType, + type DatabaseType, Indexes, EmptySheet, EmptySheetCards @@ -46,7 +47,7 @@ {#snippet emptyIndexesSheetView(toggle)} - + {#snippet actions()} { const { database } = await parent(); depends(Dependencies.TABLE); - const databaseSdk = useDatabaseSdk(params.region, params.project, database.type); + const databaseSdk = useDatabaseSdk( + params.region, + params.project, + database.type as DatabaseType + ); const table = await databaseSdk.getEntity({ databaseId: params.database, diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte index 07dcd899ad..efec69e38d 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte @@ -169,7 +169,7 @@ const systemColumns = new Set(['$id', 'actions']); const validColumnKeys = new Set([ - ...$table.columns.map((col) => col.key), + ...$columns.map((col) => col.key), '$createdAt' /* allowed for reordering */, '$updatedAt' /* allowed for reordering */ ]); @@ -811,8 +811,6 @@ } - - {#key $spreadsheetRenderKey} Date: Thu, 29 Jan 2026 21:54:04 +1300 Subject: [PATCH 03/56] Update types --- .../databases/create/+page.svelte | 18 ++++---- .../(entity)/helpers/sdk.ts | 41 ++++++++++++------- .../(entity)/helpers/terminology.ts | 8 ++-- .../database-[database]/+layout.svelte | 2 +- .../databases/database-[database]/+layout.ts | 2 +- .../databases/database-[database]/+page.ts | 2 +- .../{error.svelte => error-bar.svelte} | 0 .../(components)/sonners/index.ts | 2 +- .../database-[database]/header.svelte | 4 +- .../database-[database]/subNavigation.svelte | 2 +- .../databases/empty.svelte | 4 +- 11 files changed, 49 insertions(+), 36 deletions(-) rename src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/{error.svelte => error-bar.svelte} (100%) diff --git a/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte index 92aff1e142..2e11b85f6c 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte @@ -72,13 +72,13 @@ icon: Mongo }, { - type: 'prismapostgres', + type: 'prisma', title: 'Prisma Postgres', subtitle: 'Managed PostgreSQL with direct connections. Best for high-performance SQL workloads.' }, { - type: 'dedicateddb', + type: 'dedicated', title: 'DedicatedDB', subtitle: 'Always-on dedicated instances with high availability. Best for production workloads.' @@ -116,9 +116,9 @@ let highAvailability = $state(false); // Helper to check database type capabilities - const showRegionSelect = $derived(type === 'prismapostgres' || type === 'dedicateddb'); - const showTierSelect = $derived(type === 'dedicateddb'); - const showEngineSelect = $derived(type === 'dedicateddb'); + const showRegionSelect = $derived(type === 'prisma' || type === 'dedicated'); + const showTierSelect = $derived(type === 'dedicated'); + const showEngineSelect = $derived(type === 'dedicated'); // Backup system varies by database type const backupSystem = $derived.by(() => { @@ -126,9 +126,9 @@ case 'tablesdb': case 'documentsdb': return 'appwrite'; - case 'prismapostgres': + case 'prisma': return 'prisma'; - case 'dedicateddb': + case 'dedicated': return 'dedicated'; default: return 'appwrite'; @@ -236,14 +236,14 @@ let database: Models.Database; const databaseSdk = useDatabaseSdk(page.params.region, page.params.project); - if (type === 'prismapostgres') { + if (type === 'prisma') { database = await databaseSdk.create(type, { databaseId, name: databaseName, region: selectedRegion, tier: selectedTier } as DedicatedDatabaseParams); - } else if (type === 'dedicateddb') { + } else if (type === 'dedicated') { database = await databaseSdk.create(type, { databaseId, name: databaseName, diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts index 32af9b0b90..058f14ed81 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts @@ -112,11 +112,6 @@ export function useDatabaseSdk( region = regionOrPage?.params?.region || ''; project = regionOrPage?.params?.project || ''; } else { - if (!databaseType) { - throw new Error( - 'databaseType is required when passing string parameters to useDatabaseSdk' - ); - } type = databaseType; region = regionOrPage as string; project = projectOrTerminology as string; @@ -134,7 +129,7 @@ export function useDatabaseSdk( case 'documentsdb': { return await baseSdk.documentsDB.create(params); } - case 'prismapostgres': { + case 'prisma': { // Prisma databases are created via the compute/databases endpoint // with backend: 'prisma' const prismaParams = params as DedicatedDatabaseParams; @@ -147,7 +142,7 @@ export function useDatabaseSdk( tier: prismaParams.tier })) as unknown as Models.Database; } - case 'dedicateddb': { + case 'dedicated': { // Dedicated databases are created via the compute/databases endpoint // with backend: 'appwrite' const dedicatedParams = params as DedicatedDatabaseParams; @@ -192,7 +187,9 @@ export function useDatabaseSdk( async createEntity(params) { switch (type ?? params.databaseType) { case 'legacy': /* databases api */ - case 'tablesdb': { + case 'tablesdb': + case 'prisma': + case 'dedicated': { const table = await baseSdk.tablesDB.createTable({ ...params, tableId: params.entityId @@ -217,7 +214,9 @@ export function useDatabaseSdk( async listEntities(params) { switch (type ?? params.databaseType) { case 'legacy': /* databases api */ - case 'tablesdb': { + case 'tablesdb': + case 'prisma': + case 'dedicated': { const { total, tables } = await baseSdk.tablesDB.listTables(params); return { total, entities: tables.map(toSupportiveEntity) }; } @@ -236,7 +235,9 @@ export function useDatabaseSdk( async getEntity(params) { switch (type ?? params.databaseType) { case 'legacy': /* databases api */ - case 'tablesdb': { + case 'tablesdb': + case 'prisma': + case 'dedicated': { const table = await baseSdk.tablesDB.getTable({ databaseId: params.databaseId, tableId: params.entityId @@ -264,8 +265,8 @@ export function useDatabaseSdk( return await baseSdk.tablesDB.delete(params); case 'documentsdb': return await baseSdk.documentsDB.delete(params); - case 'prismapostgres': - case 'dedicateddb': + case 'prisma': + case 'dedicated': await baseSdk.dedicatedDatabases.delete(params); return {}; case 'vectordb': @@ -279,6 +280,8 @@ export function useDatabaseSdk( switch (type ?? params.databaseType) { case 'legacy': /* databases api */ case 'tablesdb': + case 'prisma': + case 'dedicated': return await baseSdk.tablesDB.deleteTable({ databaseId: params.databaseId, tableId: params.entityId @@ -299,6 +302,8 @@ export function useDatabaseSdk( switch (type ?? params.databaseType) { case 'legacy': /* databases api */ case 'tablesdb': + case 'prisma': + case 'dedicated': return await baseSdk.tablesDB.createRow({ databaseId: params.databaseId, tableId: params.entityId, @@ -325,6 +330,8 @@ export function useDatabaseSdk( switch (type ?? params.databaseType) { case 'legacy': /* databases api */ case 'tablesdb': + case 'prisma': + case 'dedicated': return await baseSdk.tablesDB.updateRow({ databaseId: params.databaseId, tableId: params.entityId, @@ -351,6 +358,8 @@ export function useDatabaseSdk( switch (type ?? params.databaseType) { case 'legacy': /* databases api */ case 'tablesdb': + case 'prisma': + case 'dedicated': return await baseSdk.tablesDB.updateRow({ databaseId: params.databaseId, tableId: params.entityId, @@ -374,7 +383,9 @@ export function useDatabaseSdk( async deleteRecord(params) { switch (type ?? params.databaseType) { case 'legacy': /* databases api */ - case 'tablesdb': { + case 'tablesdb': + case 'prisma': + case 'dedicated': { const row = await baseSdk.tablesDB.deleteRow({ databaseId: params.databaseId, tableId: params.entityId, @@ -400,7 +411,9 @@ export function useDatabaseSdk( async deleteRecords(params) { switch (type ?? params.databaseType) { case 'legacy': /* databases api */ - case 'tablesdb': { + case 'tablesdb': + case 'prisma': + case 'dedicated': { const { total, rows } = await baseSdk.tablesDB.deleteRows({ databaseId: params.databaseId, tableId: params.entityId, diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/terminology.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/terminology.ts index 264255cb43..6896fde2c6 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/terminology.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/terminology.ts @@ -13,8 +13,8 @@ export type DatabaseType = | 'tablesdb' | 'documentsdb' | 'vectordb' - | 'prismapostgres' - | 'dedicateddb'; + | 'prisma' + | 'dedicated'; export type RecordType = ImplementedDBTypes[keyof ImplementedDBTypes]['record']; @@ -67,12 +67,12 @@ export const baseTerminology = { record: 'document' }, vectordb: {}, - prismapostgres: { + prisma: { entity: 'table', field: 'column', record: 'row' }, - dedicateddb: { + dedicated: { entity: 'table', field: 'column', record: 'row' diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte index 8ea1f2a732..1fc25c2555 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte @@ -32,7 +32,7 @@ // Check if this is a dedicated database type $: isDedicatedType = - terminology.type === 'prismapostgres' || terminology.type === 'dedicateddb'; + terminology.type === 'prisma' || terminology.type === 'dedicated'; $: $registerCommands([ { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.ts index fe611d2fa6..5674ef1846 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.ts @@ -12,7 +12,7 @@ type DatabaseWithType = Models.Database & { }; function isDedicatedDatabaseType(type: string | undefined): boolean { - return type === 'prismapostgres' || type === 'dedicateddb'; + return type === 'prisma' || type === 'dedicated'; } export const load: LayoutLoad = async ({ params, depends }) => { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+page.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+page.ts index e770cd97e3..c98835bf7b 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+page.ts @@ -11,7 +11,7 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) => const databaseType = database.type as DatabaseType; // For dedicated databases, we don't fetch entities (tables/collections) - const isDedicatedType = databaseType === 'prismapostgres' || databaseType === 'dedicateddb'; + const isDedicatedType = databaseType === 'prisma' || databaseType === 'dedicated'; if (isDedicatedType) { return { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/error.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/error-bar.svelte similarity index 100% rename from src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/error.svelte rename to src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/error-bar.svelte diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/index.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/index.ts index cc1daf60ad..138a51a54d 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/index.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/index.ts @@ -1,3 +1,3 @@ export { default as Save } from './save.svelte'; -export { default as Error } from './error.svelte'; +export { default as Error } from './error-bar.svelte'; export { default as Suggestions } from './suggestions.svelte'; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/header.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/header.svelte index 5311b5904f..cd4eb3afc5 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/header.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/header.svelte @@ -22,8 +22,8 @@ // Check if this is a dedicated database type const isDedicatedType = $derived( - (database?.type as DatabaseType) === 'prismapostgres' || - (database?.type as DatabaseType) === 'dedicateddb' + (database?.type as DatabaseType) === 'prisma' || + (database?.type as DatabaseType) === 'dedicated' ); const tabs = $derived( diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/subNavigation.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/subNavigation.svelte index 66a32416a0..936632daef 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/subNavigation.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/subNavigation.svelte @@ -44,7 +44,7 @@ // Check if this is a dedicated database type const isDedicatedType = $derived( - terminology.type === 'prismapostgres' || terminology.type === 'dedicateddb' + terminology.type === 'prisma' || terminology.type === 'dedicated' ); const entityTypePlural = terminology.entity.lower.plural; diff --git a/src/routes/(console)/project-[region]-[project]/databases/empty.svelte b/src/routes/(console)/project-[region]-[project]/databases/empty.svelte index 45e12b795c..504114d375 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/empty.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/empty.svelte @@ -79,7 +79,7 @@ {@render databaseTypeCard({ - type: 'prismapostgres', + type: 'prisma', title: 'Prisma Postgres', subtitle: 'Managed PostgreSQL with direct connections. Best for high-performance SQL workloads.', @@ -89,7 +89,7 @@ {@render databaseTypeCard({ - type: 'dedicateddb', + type: 'dedicated', title: 'DedicatedDB', subtitle: 'Always-on dedicated database instances with high availability. Best for production workloads.', From fcfb3a8bf91f2fb5d501706ac73780ff87017ee0 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 30 Jan 2026 01:33:27 +1300 Subject: [PATCH 04/56] Show connection properly --- src/lib/sdk/dedicatedDatabases.ts | 10 +- .../(entity)/helpers/sdk.ts | 57 ++- .../databases/database-[database]/+layout.ts | 13 +- .../database-[database]/+page.svelte | 2 +- .../databases/database-[database]/+page.ts | 8 +- .../database-[database]/connectModal.svelte | 25 +- .../dedicatedOverview.svelte | 482 ++++++++++-------- .../databases/store.ts | 9 +- 8 files changed, 340 insertions(+), 266 deletions(-) diff --git a/src/lib/sdk/dedicatedDatabases.ts b/src/lib/sdk/dedicatedDatabases.ts index 20442e3974..9930778347 100644 --- a/src/lib/sdk/dedicatedDatabases.ts +++ b/src/lib/sdk/dedicatedDatabases.ts @@ -16,10 +16,12 @@ export type DedicatedDatabase = { storage: number; storageClass: string; hostname: string; - port: number; - status: 'provisioning' | 'ready' | 'failed' | 'deleting' | 'deleted'; - containerStatus: 'inactive' | 'starting' | 'running' | null; - projectId: string; + connectionPort: number; + connectionUser: string; + connectionPassword: string; + connectionString: string; + status: 'provisioning' | 'ready' | 'inactive' | 'paused' | 'failed' | 'deleted' | 'restoring' | 'scaling'; + containerStatus: 'inactive' | 'starting' | 'running' | 'active' | null; highAvailability: boolean; haReplicaCount: number; haSyncMode: string | null; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts index 058f14ed81..fac292c65d 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts @@ -187,15 +187,16 @@ export function useDatabaseSdk( async createEntity(params) { switch (type ?? params.databaseType) { case 'legacy': /* databases api */ - case 'tablesdb': - case 'prisma': - case 'dedicated': { + case 'tablesdb': { const table = await baseSdk.tablesDB.createTable({ ...params, tableId: params.entityId }); return toSupportiveEntity(table); } + case 'prisma': + case 'dedicated': + throw new Error('External databases do not support entity creation via Appwrite'); case 'documentsdb': { const table = await baseSdk.documentsDB.createCollection({ ...params, @@ -214,12 +215,15 @@ export function useDatabaseSdk( async listEntities(params) { switch (type ?? params.databaseType) { case 'legacy': /* databases api */ - case 'tablesdb': - case 'prisma': - case 'dedicated': { + case 'tablesdb': { const { total, tables } = await baseSdk.tablesDB.listTables(params); return { total, entities: tables.map(toSupportiveEntity) }; } + case 'prisma': + case 'dedicated': { + // External databases don't have entities managed by Appwrite + return { total: 0, entities: [] }; + } case 'documentsdb': { const { total, collections } = await baseSdk.documentsDB.listCollections(params); @@ -235,15 +239,16 @@ export function useDatabaseSdk( async getEntity(params) { switch (type ?? params.databaseType) { case 'legacy': /* databases api */ - case 'tablesdb': - case 'prisma': - case 'dedicated': { + case 'tablesdb': { const table = await baseSdk.tablesDB.getTable({ databaseId: params.databaseId, tableId: params.entityId }); return toSupportiveEntity(table); } + case 'prisma': + case 'dedicated': + throw new Error('External databases do not support entity retrieval via Appwrite'); case 'documentsdb': { const table = await baseSdk.documentsDB.getCollection({ databaseId: params.databaseId, @@ -280,12 +285,13 @@ export function useDatabaseSdk( switch (type ?? params.databaseType) { case 'legacy': /* databases api */ case 'tablesdb': - case 'prisma': - case 'dedicated': return await baseSdk.tablesDB.deleteTable({ databaseId: params.databaseId, tableId: params.entityId }); + case 'prisma': + case 'dedicated': + throw new Error('External databases do not support entity deletion via Appwrite'); case 'documentsdb': return await baseSdk.documentsDB.deleteCollection({ databaseId: params.databaseId, @@ -302,8 +308,6 @@ export function useDatabaseSdk( switch (type ?? params.databaseType) { case 'legacy': /* databases api */ case 'tablesdb': - case 'prisma': - case 'dedicated': return await baseSdk.tablesDB.createRow({ databaseId: params.databaseId, tableId: params.entityId, @@ -311,6 +315,9 @@ export function useDatabaseSdk( data: params.data, permissions: params.permissions }); + case 'prisma': + case 'dedicated': + throw new Error('External databases do not support record creation via Appwrite'); case 'documentsdb': return await baseSdk.documentsDB.createDocument({ databaseId: params.databaseId, @@ -330,8 +337,6 @@ export function useDatabaseSdk( switch (type ?? params.databaseType) { case 'legacy': /* databases api */ case 'tablesdb': - case 'prisma': - case 'dedicated': return await baseSdk.tablesDB.updateRow({ databaseId: params.databaseId, tableId: params.entityId, @@ -339,6 +344,9 @@ export function useDatabaseSdk( data: params.data, permissions: params.permissions }); + case 'prisma': + case 'dedicated': + throw new Error('External databases do not support record updates via Appwrite'); case 'documentsdb': return await baseSdk.documentsDB.upsertDocument({ databaseId: params.databaseId, @@ -358,14 +366,15 @@ export function useDatabaseSdk( switch (type ?? params.databaseType) { case 'legacy': /* databases api */ case 'tablesdb': - case 'prisma': - case 'dedicated': return await baseSdk.tablesDB.updateRow({ databaseId: params.databaseId, tableId: params.entityId, rowId: params.recordId, permissions: params.permissions }); + case 'prisma': + case 'dedicated': + throw new Error('External databases do not support permission updates via Appwrite'); case 'documentsdb': return await baseSdk.documentsDB.upsertDocument({ databaseId: params.databaseId, @@ -383,9 +392,7 @@ export function useDatabaseSdk( async deleteRecord(params) { switch (type ?? params.databaseType) { case 'legacy': /* databases api */ - case 'tablesdb': - case 'prisma': - case 'dedicated': { + case 'tablesdb': { const row = await baseSdk.tablesDB.deleteRow({ databaseId: params.databaseId, tableId: params.entityId, @@ -393,6 +400,9 @@ export function useDatabaseSdk( }); return toSupportiveRecord(row); } + case 'prisma': + case 'dedicated': + throw new Error('External databases do not support record deletion via Appwrite'); case 'documentsdb': { const document = await baseSdk.documentsDB.deleteDocument({ databaseId: params.databaseId, @@ -411,9 +421,7 @@ export function useDatabaseSdk( async deleteRecords(params) { switch (type ?? params.databaseType) { case 'legacy': /* databases api */ - case 'tablesdb': - case 'prisma': - case 'dedicated': { + case 'tablesdb': { const { total, rows } = await baseSdk.tablesDB.deleteRows({ databaseId: params.databaseId, tableId: params.entityId, @@ -421,6 +429,9 @@ export function useDatabaseSdk( }); return { total, records: rows.map(toSupportiveRecord) }; } + case 'prisma': + case 'dedicated': + throw new Error('External databases do not support bulk record deletion via Appwrite'); case 'documentsdb': { const { total, documents } = await baseSdk.documentsDB.deleteDocuments({ databaseId: params.databaseId, diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.ts index 5674ef1846..efee99629c 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.ts @@ -4,7 +4,7 @@ import type { LayoutLoad } from './$types'; import { Dependencies } from '$lib/constants'; import Breadcrumbs from './breadcrumbs.svelte'; import SubNavigation from './subNavigation.svelte'; -import type { DedicatedDatabase, DedicatedDatabaseCredentials } from '$lib/sdk/dedicatedDatabases'; +import type { DedicatedDatabase } from '$lib/sdk/dedicatedDatabases'; import type { Models } from '@appwrite.io/console'; type DatabaseWithType = Models.Database & { @@ -23,7 +23,6 @@ export const load: LayoutLoad = async ({ params, depends }) => { // Try to get from tablesDB first (handles legacy, tablesdb, documentsdb) let database: DatabaseWithType | DedicatedDatabase; let dedicatedDatabase: DedicatedDatabase | null = null; - let credentials: DedicatedDatabaseCredentials | null = null; try { database = await projectSdk.tablesDB.get({ @@ -45,19 +44,9 @@ export const load: LayoutLoad = async ({ params, depends }) => { } } - // Fetch credentials for dedicated databases - if (dedicatedDatabase) { - try { - credentials = await projectSdk.dedicatedDatabases.getCredentials(params.database); - } catch { - // Credentials not available yet (e.g., still provisioning) - } - } - return { database, dedicatedDatabase, - credentials, header: Header, breadcrumbs: Breadcrumbs, subNavigation: SubNavigation diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+page.svelte index 0f6870e1f4..cce344d1d5 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+page.svelte @@ -51,7 +51,7 @@ {#if data.isDedicatedType && data.dedicatedDatabase} - + {:else} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+page.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+page.ts index c98835bf7b..e4c26b4868 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+page.ts @@ -5,7 +5,7 @@ import { CARD_LIMIT, Dependencies } from '$lib/constants'; import { type DatabaseType, useDatabaseSdk } from '$database/(entity)'; export const load: PageLoad = async ({ params, url, route, depends, parent }) => { - const { database, dedicatedDatabase, credentials } = await parent(); + const { database, dedicatedDatabase } = await parent(); depends(Dependencies.TABLES); const databaseType = database.type as DatabaseType; @@ -21,8 +21,7 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) => view: View.Grid, entities: { total: 0, entities: [] }, isDedicatedType: true, - dedicatedDatabase, - credentials + dedicatedDatabase }; } @@ -46,7 +45,6 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) => view, entities, isDedicatedType: false, - dedicatedDatabase: null, - credentials: null + dedicatedDatabase: null }; }; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/connectModal.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/connectModal.svelte index 013b560683..72894a0b6f 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/connectModal.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/connectModal.svelte @@ -5,20 +5,15 @@ import { IconDuplicate } from '@appwrite.io/pink-icons-svelte'; import { copy } from '$lib/helpers/copy'; import { addNotification } from '$lib/stores/notifications'; - import type { - DedicatedDatabase, - DedicatedDatabaseCredentials - } from '$lib/sdk/dedicatedDatabases'; + import type { DedicatedDatabase } from '$lib/sdk/dedicatedDatabases'; let { show = $bindable(false), database, - credentials, connectionCommand }: { show: boolean; database: DedicatedDatabase; - credentials: DedicatedDatabaseCredentials | null; connectionCommand: string; } = $props(); @@ -48,8 +43,8 @@ } async function copyConnectionString() { - if (!credentials) return; - const success = await copy(credentials.connectionString); + if (!database.connectionString) return; + const success = await copy(database.connectionString); if (success) { addNotification({ type: 'success', @@ -91,8 +86,8 @@ Use this URI to connect from your application or database client. - {#if credentials} - + {#if database.connectionString} + {/if} @@ -116,21 +111,19 @@
Host - {credentials?.host ?? '-'} + {database.hostname || '-'}
Port - {credentials?.port ?? '-'} + {database.connectionPort || '-'}
Database - {credentials?.database ?? '-'} + postgres
Username - {credentials?.username ?? '-'} + {database.connectionUser || '-'}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/dedicatedOverview.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/dedicatedOverview.svelte index 80af11f4c1..24ffde4499 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/dedicatedOverview.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/dedicatedOverview.svelte @@ -1,7 +1,7 @@ - - + + Status - - - Database Status - - - - {#if database.containerStatus} - - Container - - + + + {capitalizeFirst(database.status)} + + + {#if database.containerStatus && !isPrisma} + + Container: {capitalizeFirst(database.containerStatus)} + {/if} + + + {database.region.toUpperCase()} + {#if database.error} @@ -194,14 +199,18 @@ {/if} -
-

Created: {toLocaleDateTime(database.$createdAt)}

-

Last updated: {toLocaleDateTime(database.$updatedAt)}

-
+ + + Created {toLocaleDateTime(database.$createdAt)} + + + Updated {toLocaleDateTime(database.$updatedAt)} + +
- {#if database.containerStatus === 'inactive'} + {#if database.containerStatus === 'inactive' && !isPrisma} @@ -213,43 +222,72 @@
- - {#if database.status === 'ready' && credentials} + + {#if database.status === 'ready' && hasConnectionDetails} - Connection Settings - Use these credentials to connect to your database directly. + Connection + Use these credentials to connect to your database. - - - - - - - -
- -
- - - -
+ +
+ + (connectionTab = 'direct')} + active={connectionTab === 'direct'}> + Direct Connection + + (connectionTab = 'string')} + active={connectionTab === 'string'}> + Connection String + + +
- + {#if connectionTab === 'direct'} + + + + +
+ +
+ + + +
+
+
+ + {:else} + + + + + Terminal Command + + + + + {/if}
- - - {:else if database.status === 'provisioning'} - Connection Settings + Connection - + Your database is being set up. Connection details will be available once provisioning is complete. @@ -264,53 +302,69 @@ {/if} - + Resources + Your database configuration and allocated resources. - - - - Engine - {getEngineDisplayName(database.engine)} - {database.version} - - - Tier - {database.tier.charAt(0).toUpperCase() + - database.tier.slice(1)} - - - CPU - {cpuDisplay} - - - Memory - {memoryDisplay} - - - Storage - {storageDisplay} - - - Storage Class - {database.storageClass} - - - + + + + Engine + + + {getEngineDisplayName(database.engine)} {database.version} + + + + + Tier + + + {capitalizeFirst(database.tier)} + + + + + Backend + + + {capitalizeFirst(database.backend)} + + + + + CPU + + {cpuDisplay} + + + + Memory + + {memoryDisplay} + + + + Storage + + {storageDisplay} + + - - - High Availability - - - + + {#if !isPrisma} + + High Availability + Configure replicas and failover settings for your database. + + - Status + + Status + {#if database.highAvailability} - Replica Count - {database.haReplicaCount} + + Replicas + + + {database.haReplicaCount} + {#if database.haSyncMode} - Sync Mode - {database.haSyncMode} + + Sync Mode + + + {capitalizeFirst(database.haSyncMode)} + {/if} {/if} - - - + + + {/if} - - - Network - - - - - Max Connections - {database.networkMaxConnections} - - - Idle Timeout - {database.networkIdleTimeoutSeconds}s - - {#if database.idleTimeoutMinutes} + + {#if !isPrisma} + + Network + Connection limits and network configuration. + + + - Sleep After Idle - {database.idleTimeoutMinutes} min + + Max Connections + + + {database.networkMaxConnections} + - {/if} - + + + Idle Timeout + + + {database.networkIdleTimeoutSeconds}s + + + {#if database.idleTimeoutMinutes} + + + Sleep After Idle + + + {database.idleTimeoutMinutes} min + + + {/if} + - {#if database.networkIPAllowlist?.length > 0} - - IP Allowlist + {#if database.networkIPAllowlist?.length > 0} - {#each database.networkIPAllowlist as ip} - {ip} - {/each} + + IP Allowlist + + + {#each database.networkIPAllowlist as ip} + + {/each} + - - {/if} - - - + {/if} + + + + {/if} - + Backups + Automatic backup and point-in-time recovery settings. - - + + + + Automatic Backups + + + + {#if database.backupEnabled} - Status + + Point-in-Time Recovery + + content={database.backupPitr ? 'Enabled' : 'Disabled'} /> - {#if database.backupEnabled} - - Point-in-Time Recovery - - - - Schedule - {database.backupCron} - - - Retention - {database.backupRetentionDays} days - - {/if} - - + + + Schedule + + {database.backupCron} + + + + Retention + + + {database.backupRetentionDays} days + + + {/if} + - - diff --git a/src/routes/(console)/project-[region]-[project]/databases/store.ts b/src/routes/(console)/project-[region]-[project]/databases/store.ts index d503891d35..5c57731f01 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/store.ts @@ -23,8 +23,15 @@ export const columns = writable( ] ); -export function getDatabaseTypeTitle(database: Models.Database) { +export function getDatabaseTypeTitle(database: Models.Database & { engine?: string }) { switch (database.type as DatabaseType) { + case 'prisma': + return 'Prisma Postgres'; + case 'dedicated': { + const engine = database.engine || 'postgres'; + const engineName = engine === 'postgres' ? 'PostgreSQL' : engine === 'mysql' ? 'MySQL' : engine; + return `Dedicated ${engineName}`; + } default: case 'legacy': case 'tablesdb': From da0cc2b5000c0d69d08fe5564dcb88f4ea2f106d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 12 Mar 2026 21:23:19 +1300 Subject: [PATCH 05/56] (feat): Expand dedicated databases SDK with full CRUD operations --- src/lib/sdk/dedicatedDatabases.ts | 991 +++++++++++++++++++++++++++--- 1 file changed, 913 insertions(+), 78 deletions(-) diff --git a/src/lib/sdk/dedicatedDatabases.ts b/src/lib/sdk/dedicatedDatabases.ts index 9930778347..f06c437cfd 100644 --- a/src/lib/sdk/dedicatedDatabases.ts +++ b/src/lib/sdk/dedicatedDatabases.ts @@ -1,39 +1,135 @@ import type { Client } from '@appwrite.io/console'; +// ── Enums ────────────────────────────────────────────────────────────────── + +export type DatabaseEngine = 'postgres' | 'mysql' | 'mariadb' | 'mongodb'; +export type DatabaseTypeValue = 'shared' | 'dedicated'; +export type DatabaseBackend = 'prisma' | 'appwrite' | 'edge'; +export type DatabaseStatusValue = + | 'provisioning' + | 'ready' + | 'active' + | 'inactive' + | 'paused' + | 'failed' + | 'deleted' + | 'restoring' + | 'scaling'; +export type ContainerStatusValue = + | 'inactive' + | 'starting' + | 'running' + | 'active' + | 'spinning_down' + | 'freezing' + | null; +export type StorageClass = 'ssd' | 'nvme' | 'hdd'; +export type BackupType = 'full' | 'incremental' | 'wal'; +export type BackupStatusValue = 'pending' | 'running' | 'completed' | 'failed' | 'verified'; +export type BackupStorageProvider = 's3' | 'gcs' | 'azure'; +export type RestorationType = 'backup' | 'pitr'; +export type RestorationStatusValue = 'pending' | 'running' | 'completed' | 'failed'; +export type HASyncMode = 'async' | 'sync' | 'quorum'; +export type ReplicaRole = 'primary' | 'standby' | 'readReplica'; +export type MaintenanceDay = 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'; +export type DataResidency = 'eu' | 'us' | 'apac' | 'global'; +export type KeyManagement = 'appwriteKms' | 'customerManaged'; +export type UpgradePolicy = 'autoMinor' | 'manual' | 'scheduled'; +export type PoolerMode = 'transaction' | 'session'; +export type ConnectionRole = 'readonly' | 'readwrite'; + +export type Capability = + | 'pitr' + | 'ha' + | 'coldStart' + | 'pause' + | 'scaling' + | 'storageScaling' + | 'backupCreate' + | 'backupRestore' + | 'backupVerification' + | 'connections' + | 'usageMetrics' + | 'versionUpgrade' + | 'maintenanceWindow' + | 'extensions' + | 'connectionPooler' + | 'ipAllowlist' + | 'slowQueryLog' + | 'auditLog' + | 'credentialRotation' + | 'failover' + | 'crossRegionFailover' + | 'multiRegionReplica' + | 'backupOffCluster' + | 'performanceInsights'; + +// ── Response Types ───────────────────────────────────────────────────────── + export type DedicatedDatabase = { $id: string; $createdAt: string; $updatedAt: string; + projectId: string; name: string; - engine: 'postgres' | 'mysql' | 'mariadb'; + engine: DatabaseEngine; version: string; - type: 'shared' | 'dedicated'; + type: DatabaseTypeValue; region: string; tier: string; - backend: 'prisma' | 'appwrite'; + backend: DatabaseBackend; cpu: number; memory: number; storage: number; - storageClass: string; + storageClass: StorageClass; + maxStorageGb: number; hostname: string; connectionPort: number; connectionUser: string; connectionPassword: string; connectionString: string; - status: 'provisioning' | 'ready' | 'inactive' | 'paused' | 'failed' | 'deleted' | 'restoring' | 'scaling'; - containerStatus: 'inactive' | 'starting' | 'running' | 'active' | null; + status: DatabaseStatusValue; + externalIP: string; + internalIP: string; + containerStatus: ContainerStatusValue; + lastActivityAt: string; + idleUntil: string; + idleTimeoutMinutes: number | null; highAvailability: boolean; haReplicaCount: number; - haSyncMode: string | null; + haSyncMode: HASyncMode | null; networkMaxConnections: number; networkIdleTimeoutSeconds: number; networkIPAllowlist: string[]; - idleTimeoutMinutes: number | null; + networkPublicTcp: boolean; backupEnabled: boolean; backupPitr: boolean; backupCron: string; backupRetentionDays: number; + pitrRetentionDays: number; + storageAutoscaling: boolean; + storageAutoscalingThresholdPercent: number; + storageAutoscalingMaxGb: number; + maintenanceWindowDay: MaintenanceDay; + maintenanceWindowHourUtc: number; + maintenanceWindowDurationMinutes: number; + maintenanceUpgradePolicy: UpgradePolicy; metricsEnabled: boolean; + metricsSlowQueryLogThresholdMs: number; + metricsTraceSampleRate: number; + securityEncryptionAtRest: boolean; + securityKeyManagement: KeyManagement; + securityKeyRotationDays: number; + securityCMKKeyId: string; + securityAuditLogEnabled: boolean; + securityLogRetentionDays: number; + securityDataResidency: DataResidency; + sqlApiEnabled: boolean; + sqlApiAllowedStatements: string[]; + sqlApiMaxBytes: number; + sqlApiMaxRows: number; + sqlApiTimeoutSeconds: number; + lastMetricsPollAt: number; error?: string; }; @@ -43,30 +139,259 @@ export type DedicatedDatabaseList = { }; export type DedicatedDatabaseCredentials = { - username: string; - password: string; + $id: string; host: string; port: number; + username: string; + password: string; database: string; + engine: DatabaseEngine; + ssl: boolean; connectionString: string; }; +export type DatabaseConnection = { + $id: string; + username: string; + database: string; + role: ConnectionRole; + $createdAt: string; +}; + +export type DatabaseConnectionList = { + total: number; + connections: DatabaseConnection[]; +}; + +export type Backup = { + $id: string; + $createdAt: string; + databaseId: string; + projectId: string; + type: BackupType; + status: BackupStatusValue; + sizeBytes: number; + startedAt: number; + completedAt: number; + verifiedAt: number; + expiresAt: number; + error?: string; +}; + +export type BackupList = { + total: number; + backups: Backup[]; +}; + +export type Restoration = { + $id: string; + $createdAt: string; + databaseId: string; + projectId: string; + backupId: string | null; + type: RestorationType; + status: RestorationStatusValue; + targetTime: number | null; + startedAt: number; + completedAt: number; + error?: string; +}; + +export type RestorationList = { + total: number; + restorations: Restoration[]; +}; + +export type HAStatusReplica = { + $id: string; + role: 'primary' | 'replica'; + status: 'healthy' | 'degraded' | 'unhealthy'; + lagSeconds: number; +}; + +export type HAStatus = { + enabled: boolean; + replicaCount: number; + syncMode: HASyncMode; + replicas: HAStatusReplica[]; +}; + +export type ReadReplica = { + $id: string; + databaseId: string; + targetRegion: string; + sourceRegion: string; + status: 'provisioning' | 'active' | 'degraded' | 'failed' | 'deleting'; + lagSeconds: number; + hostname: string; + externalIP: string; + crossZoneConsent: boolean; + $createdAt: string; +}; + +export type ReadReplicaList = { + total: number; + replicas: ReadReplica[]; +}; + +export type CrossRegionStatus = { + enabled: boolean; + primaryRegion: string; + standbyRegion: string; + standbyStatus: 'healthy' | 'degraded' | 'unhealthy' | 'provisioning'; + lagSeconds: number; + lastSyncedAt: string; +}; + +export type PoolerConfig = { + enabled: boolean; + mode: PoolerMode; + maxConnections: number; + defaultPoolSize: number; + port: number; +}; + +export type BackupStorageConfig = { + provider: BackupStorageProvider; + bucket: string; + region: string; + prefix: string; + endpoint: string; +}; + +export type ActiveConnection = { + pid: number; + user: string; + database: string; + state: 'active' | 'idle' | 'idle in transaction'; + query: string; + connectedAt: string; + waitEvent: string; +}; + +export type ActiveConnectionList = { + total: number; + activeConnections: ActiveConnection[]; +}; + +export type DatabaseMetrics = { + period: string; + cpuPercent: number; + memoryPercent: number; + memoryUsedBytes: number; + memoryMaxBytes: number; + storageUsedBytes: number; + connectionsActive: number; + connectionsMax: number; + iopsRead: number; + iopsWrite: number; + qps: number; +}; + +export type PerformanceInsightsQuery = { + query: string; + calls: number; + totalTimeMs: number; + meanTimeMs: number; + rows: number; +}; + +export type PerformanceInsightsWaitEvent = { + event: string; + type: string; + count: number; + totalWaitMs: number; +}; + +export type PerformanceInsights = { + topQueries: PerformanceInsightsQuery[]; + waitEvents: PerformanceInsightsWaitEvent[]; + totalCalls: number; + totalTimeMs: number; + avgTimeMs: number; +}; + +export type PITRWindows = { + earliest: string; + latest: string; +}; + +export type AuditLog = { + timestamp: string; + user: string; + database: string; + action: string; + object: string; + statement: string; + clientAddress: string; +}; + +export type AuditLogList = { + total: number; + auditLogs: AuditLog[]; +}; + +export type SlowQuery = { + query: string; + durationMs: number; + calls: number; + user: string; + database: string; +}; + +export type SlowQueryList = { + total: number; + slowQueries: SlowQuery[]; +}; + +export type DatabaseExtensions = { + installed: string[]; + available: string[]; +}; + +export type DatabaseStatusDetail = { + health: 'healthy' | 'degraded' | 'unhealthy'; + ready: boolean; + engine: DatabaseEngine; + version: string; + uptime: number; + connections: { + current: number; + max: number; + }; + replicas: { + index: number; + role: 'primary' | 'replica'; + healthy: boolean; + lagSeconds: number; + }[]; + volumes: { + path: string; + usedPercent: string; + available: string; + mounted: boolean; + }[]; +}; + +// ── Request Params ───────────────────────────────────────────────────────── + export type CreateDedicatedDatabaseParams = { databaseId: string; name: string; - engine?: 'postgres' | 'mysql' | 'mariadb'; + engine?: DatabaseEngine; version?: string; region?: string; - type?: 'shared' | 'dedicated'; + type?: DatabaseTypeValue; tier?: string; - backend: 'prisma' | 'appwrite'; + backend: DatabaseBackend; cpu?: number; memory?: number; storage?: number; - storageClass?: string; + storageClass?: StorageClass; + maxStorageGb?: number; highAvailability?: boolean; haReplicaCount?: number; - haSyncMode?: string; + haSyncMode?: HASyncMode; networkMaxConnections?: number; networkIdleTimeoutSeconds?: number; networkIPAllowlist?: string[]; @@ -75,9 +400,61 @@ export type CreateDedicatedDatabaseParams = { backupPitr?: boolean; backupSchedule?: string; backupRetentionDays?: number; + pitrRetentionDays?: number; + storageAutoscaling?: boolean; + storageAutoscalingThresholdPercent?: number; + storageAutoscalingMaxGb?: number; + metricsEnabled?: boolean; +}; + +export type UpdateDedicatedDatabaseParams = { + name?: string; + status?: 'paused' | 'active' | 'inactive' | 'ready'; + cpu?: number; + memory?: number; + storage?: number; + storageClass?: StorageClass; + highAvailability?: boolean; + haReplicaCount?: number; + haSyncMode?: HASyncMode; + networkMaxConnections?: number; + networkIdleTimeoutSeconds?: number; + networkIPAllowlist?: string[]; + idleTimeoutMinutes?: number; + backupEnabled?: boolean; + backupPitr?: boolean; + backupCron?: string; + backupRetentionDays?: number; + pitrRetentionDays?: number; + storageAutoscaling?: boolean; + storageAutoscalingThresholdPercent?: number; + storageAutoscalingMaxGb?: number; metricsEnabled?: boolean; + securityAuditLogEnabled?: boolean; + securityLogRetentionDays?: number; + sqlApiEnabled?: boolean; + sqlApiMaxBytes?: number; + sqlApiMaxRows?: number; + sqlApiTimeoutSeconds?: number; + sqlApiAllowedStatements?: string[]; }; +// ── Helpers ──────────────────────────────────────────────────────────────── + +const JSON_HEADERS = { 'content-type': 'application/json' } as const; + +function filterUndefined(obj: Record): Record { + const result: Record = {}; + for (const [key, value] of Object.entries(obj)) { + if (value !== undefined) { + result[key] = value; + } + } + return result; +} + +// ── SDK Class ────────────────────────────────────────────────────────────── + export class DedicatedDatabases { client: Client; @@ -85,90 +462,548 @@ export class DedicatedDatabases { this.client = client; } + private uri(path: string): URL { + return new URL(this.client.config.endpoint + path); + } + + // ── Database CRUD ────────────────────────────────────────────────── + async create(params: CreateDedicatedDatabaseParams): Promise { - const path = `/compute/databases`; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'POST', - uri, - { - 'content-type': 'application/json' - }, - { - databaseId: params.databaseId, - name: params.name, - engine: params.engine ?? 'postgres', - version: params.version, - region: params.region ?? 'fra', - type: params.type ?? 'shared', - tier: params.tier ?? 'starter', - backend: params.backend, - cpu: params.cpu, - memory: params.memory, - storage: params.storage, - storageClass: params.storageClass, - highAvailability: params.highAvailability, - haReplicaCount: params.haReplicaCount, - haSyncMode: params.haSyncMode, - networkMaxConnections: params.networkMaxConnections, - networkIdleTimeoutSeconds: params.networkIdleTimeoutSeconds, - networkIPAllowlist: params.networkIPAllowlist, - idleTimeoutMinutes: params.idleTimeoutMinutes, - backupEnabled: params.backupEnabled, - backupPitr: params.backupPitr, - backupCron: params.backupSchedule, - backupRetentionDays: params.backupRetentionDays, - metricsEnabled: params.metricsEnabled - } - ); + return await this.client.call('POST', this.uri('/compute/databases'), JSON_HEADERS, { + databaseId: params.databaseId, + name: params.name, + engine: params.engine ?? 'postgres', + version: params.version, + region: params.region ?? 'fra', + type: params.type ?? 'shared', + tier: params.tier ?? 'starter', + backend: params.backend, + cpu: params.cpu, + memory: params.memory, + storage: params.storage, + storageClass: params.storageClass, + maxStorageGb: params.maxStorageGb, + highAvailability: params.highAvailability, + haReplicaCount: params.haReplicaCount, + haSyncMode: params.haSyncMode, + networkMaxConnections: params.networkMaxConnections, + networkIdleTimeoutSeconds: params.networkIdleTimeoutSeconds, + networkIPAllowlist: params.networkIPAllowlist, + idleTimeoutMinutes: params.idleTimeoutMinutes, + backupEnabled: params.backupEnabled, + backupPitr: params.backupPitr, + backupCron: params.backupSchedule, + backupRetentionDays: params.backupRetentionDays, + pitrRetentionDays: params.pitrRetentionDays, + storageAutoscaling: params.storageAutoscaling, + storageAutoscalingThresholdPercent: params.storageAutoscalingThresholdPercent, + storageAutoscalingMaxGb: params.storageAutoscalingMaxGb, + metricsEnabled: params.metricsEnabled + }); } async get(databaseId: string): Promise { - const path = `/compute/databases/${databaseId}`; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call('GET', uri, { - 'content-type': 'application/json' - }); + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}`), + JSON_HEADERS + ); } async list(queries: string[] = [], search?: string): Promise { - const path = `/compute/databases`; const params: Record = {}; if (queries.length > 0) params.queries = queries; if (search) params.search = search; - - const uri = new URL(this.client.config.endpoint + path); return await this.client.call( 'GET', - uri, - { - 'content-type': 'application/json' - }, + this.uri('/compute/databases'), + JSON_HEADERS, params ); } + async update( + databaseId: string, + params: UpdateDedicatedDatabaseParams + ): Promise { + return await this.client.call( + 'PATCH', + this.uri(`/compute/databases/${databaseId}`), + JSON_HEADERS, + filterUndefined(params) + ); + } + async delete(params: { databaseId: string }): Promise { - const path = `/compute/databases/${params.databaseId}`; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call('DELETE', uri, { - 'content-type': 'application/json' - }); + return await this.client.call( + 'DELETE', + this.uri(`/compute/databases/${params.databaseId}`), + JSON_HEADERS + ); + } + + // ── Lifecycle ────────────────────────────────────────────────────── + + async migrate(databaseId: string): Promise { + return await this.client.call( + 'POST', + this.uri(`/compute/databases/${databaseId}/migrations`), + JSON_HEADERS + ); + } + + async upgradeVersion(databaseId: string, targetVersion: string): Promise { + return await this.client.call( + 'POST', + this.uri(`/compute/databases/${databaseId}/upgrades`), + JSON_HEADERS, + { targetVersion } + ); + } + + async updateActivity( + databaseId: string, + params?: { inboundBytes?: number; outboundBytes?: number } + ): Promise { + return await this.client.call( + 'PATCH', + this.uri(`/compute/databases/${databaseId}/activity`), + JSON_HEADERS, + params ? filterUndefined(params) : undefined + ); + } + + // ── Status ───────────────────────────────────────────────────────── + + async getStatus(databaseId: string): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/status`), + JSON_HEADERS + ); } + // ── Credentials ──────────────────────────────────────────────────── + async getCredentials(databaseId: string): Promise { - const path = `/compute/databases/${databaseId}/credentials`; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call('GET', uri, { - 'content-type': 'application/json' - }); + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/credentials`), + JSON_HEADERS + ); } - async coldStart(databaseId: string): Promise { - const path = `/compute/databases/${databaseId}/cold-start`; - const uri = new URL(this.client.config.endpoint + path); - return await this.client.call('POST', uri, { - 'content-type': 'application/json' - }); + async rotateCredentials(databaseId: string): Promise { + return await this.client.call( + 'POST', + this.uri(`/compute/databases/${databaseId}/credentials`), + JSON_HEADERS + ); + } + + // ── Connections (Database Users) ─────────────────────────────────── + + async createConnection( + databaseId: string, + username: string, + role: ConnectionRole = 'readwrite' + ): Promise { + return await this.client.call( + 'POST', + this.uri(`/compute/databases/${databaseId}/connections`), + JSON_HEADERS, + { username, role } + ); + } + + async listConnections(databaseId: string): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/connections`), + JSON_HEADERS + ); + } + + async deleteConnection(databaseId: string, connectionId: string): Promise { + return await this.client.call( + 'DELETE', + this.uri(`/compute/databases/${databaseId}/connections/${connectionId}`), + JSON_HEADERS + ); + } + + async getActiveConnections(databaseId: string): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/active-connections`), + JSON_HEADERS + ); + } + + // ── Extensions (PostgreSQL) ──────────────────────────────────────── + + async createExtension(databaseId: string, name: string): Promise { + return await this.client.call( + 'POST', + this.uri(`/compute/databases/${databaseId}/extensions`), + JSON_HEADERS, + { name } + ); + } + + async listExtensions(databaseId: string): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/extensions`), + JSON_HEADERS + ); + } + + async deleteExtension(databaseId: string, extensionName: string): Promise { + return await this.client.call( + 'DELETE', + this.uri(`/compute/databases/${databaseId}/extensions/${extensionName}`), + JSON_HEADERS + ); + } + + // ── Connection Pooler ────────────────────────────────────────────── + + async getPoolerConfig(databaseId: string): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/pooler`), + JSON_HEADERS + ); + } + + async updatePoolerConfig( + databaseId: string, + params: { mode?: PoolerMode; maxConnections?: number; defaultPoolSize?: number } + ): Promise { + return await this.client.call( + 'PATCH', + this.uri(`/compute/databases/${databaseId}/pooler`), + JSON_HEADERS, + filterUndefined(params) + ); + } + + // ── High Availability ────────────────────────────────────────────── + + async getHAStatus(databaseId: string): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/ha`), + JSON_HEADERS + ); + } + + async createFailover(databaseId: string, targetReplicaId?: string): Promise { + return await this.client.call( + 'POST', + this.uri(`/compute/databases/${databaseId}/ha/failovers`), + JSON_HEADERS, + targetReplicaId ? { targetReplicaId } : undefined + ); + } + + // ── Cross-Region Failover ────────────────────────────────────────── + + async enableCrossRegion( + databaseId: string, + standbyRegion: string + ): Promise { + return await this.client.call( + 'PUT', + this.uri(`/compute/databases/${databaseId}/cross-region`), + JSON_HEADERS, + { standbyRegion } + ); + } + + async disableCrossRegion(databaseId: string): Promise { + return await this.client.call( + 'DELETE', + this.uri(`/compute/databases/${databaseId}/cross-region`), + JSON_HEADERS + ); + } + + async getCrossRegionStatus(databaseId: string): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/cross-region`), + JSON_HEADERS + ); + } + + async triggerCrossRegionFailover(databaseId: string): Promise { + return await this.client.call( + 'POST', + this.uri(`/compute/databases/${databaseId}/cross-region/failovers`), + JSON_HEADERS + ); + } + + // ── Read Replicas ────────────────────────────────────────────────── + + async createReadReplica( + databaseId: string, + targetRegion: string, + crossZoneConsent: boolean = false + ): Promise { + return await this.client.call( + 'POST', + this.uri(`/compute/databases/${databaseId}/replicas`), + JSON_HEADERS, + { targetRegion, crossZoneConsent } + ); + } + + async listReadReplicas(databaseId: string): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/replicas`), + JSON_HEADERS + ); + } + + async deleteReadReplica(databaseId: string, replicaId: string): Promise { + return await this.client.call( + 'DELETE', + this.uri(`/compute/databases/${databaseId}/replicas/${replicaId}`), + JSON_HEADERS + ); + } + + async getReadReplica(databaseId: string, replicaId: string): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/replicas/${replicaId}`), + JSON_HEADERS + ); + } + + // ── Backups ──────────────────────────────────────────────────────── + + async createBackup( + databaseId: string, + type: 'full' | 'incremental' = 'full' + ): Promise { + return await this.client.call( + 'POST', + this.uri(`/compute/databases/${databaseId}/backups`), + JSON_HEADERS, + { type } + ); + } + + async listBackups( + databaseId: string, + params?: { + status?: BackupStatusValue; + type?: BackupType; + limit?: number; + offset?: number; + } + ): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/backups`), + JSON_HEADERS, + params ? filterUndefined(params) : undefined + ); + } + + async getBackup(databaseId: string, backupId: string): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/backups/${backupId}`), + JSON_HEADERS + ); + } + + async deleteBackup(databaseId: string, backupId: string): Promise { + return await this.client.call( + 'DELETE', + this.uri(`/compute/databases/${databaseId}/backups/${backupId}`), + JSON_HEADERS + ); + } + + // ── Restorations ─────────────────────────────────────────────────── + + async createRestoration(databaseId: string, backupId: string): Promise { + return await this.client.call( + 'POST', + this.uri(`/compute/databases/${databaseId}/restorations`), + JSON_HEADERS, + { type: 'backup', backupId } + ); + } + + async createPITRRestoration(databaseId: string, targetTime: number): Promise { + return await this.client.call( + 'POST', + this.uri(`/compute/databases/${databaseId}/restorations`), + JSON_HEADERS, + { type: 'pitr', targetTime } + ); + } + + async listRestorations( + databaseId: string, + params?: { + status?: RestorationStatusValue; + type?: RestorationType; + limit?: number; + offset?: number; + } + ): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/restorations`), + JSON_HEADERS, + params ? filterUndefined(params) : undefined + ); + } + + async getRestoration(databaseId: string, restorationId: string): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/restorations/${restorationId}`), + JSON_HEADERS + ); + } + + // ── PITR ─────────────────────────────────────────────────────────── + + async getPITRWindows(databaseId: string): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/pitr-windows`), + JSON_HEADERS + ); + } + + // ── Metrics & Monitoring ─────────────────────────────────────────── + + async getMetrics( + databaseId: string, + period: '1h' | '24h' | '7d' | '30d' = '24h' + ): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/metrics`), + JSON_HEADERS, + { period } + ); + } + + async getSlowQueries( + databaseId: string, + params?: { limit?: number; thresholdMs?: number } + ): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/slow-queries`), + JSON_HEADERS, + params ? filterUndefined(params) : undefined + ); + } + + async getPerformanceInsights( + databaseId: string, + params?: { period?: '1h' | '24h' | '7d'; limit?: number } + ): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/performance-insights`), + JSON_HEADERS, + params ? filterUndefined(params) : undefined + ); + } + + async getAuditLogs( + databaseId: string, + params?: { startTime?: string; endTime?: string; limit?: number } + ): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/audit-logs`), + JSON_HEADERS, + params ? filterUndefined(params) : undefined + ); + } + + // ── Maintenance ──────────────────────────────────────────────────── + + async updateMaintenance( + databaseId: string, + params: { + day: MaintenanceDay; + hourUtc: number; + durationMinutes?: number; + } + ): Promise { + return await this.client.call( + 'PATCH', + this.uri(`/compute/databases/${databaseId}/maintenance`), + JSON_HEADERS, + filterUndefined(params) + ); + } + + // ── Backup Storage (Off-Cluster) ─────────────────────────────────── + + async configureBackupStorage( + databaseId: string, + params: { + provider: BackupStorageProvider; + bucket: string; + region: string; + accessKeyId: string; + secretAccessKey: string; + prefix?: string; + endpoint?: string; + } + ): Promise { + return await this.client.call( + 'PUT', + this.uri(`/compute/databases/${databaseId}/backup-storage`), + JSON_HEADERS, + params + ); + } + + async getBackupStorageConfig(databaseId: string): Promise { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/backup-storage`), + JSON_HEADERS + ); + } + + async deleteBackupStorageConfig(databaseId: string): Promise { + return await this.client.call( + 'DELETE', + this.uri(`/compute/databases/${databaseId}/backup-storage`), + JSON_HEADERS + ); + } + + // ── Usage ────────────────────────────────────────────────────────── + + async getUsage( + databaseId: string, + range: '24h' | '30d' | '90d' = '24h' + ): Promise> { + return await this.client.call( + 'GET', + this.uri(`/compute/databases/${databaseId}/usage`), + JSON_HEADERS, + { range } + ); } } From d7f6efe87e949122fc0ce7ffc71d1ceb3aeef360 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 12 Mar 2026 21:23:28 +1300 Subject: [PATCH 06/56] (feat): Add dedicated database settings components --- .../database-[database]/settings/+page.svelte | 141 ++++++++- .../settings/dangerZone.svelte | 66 ++++ .../settings/rotateCredentials.svelte | 90 ++++++ .../settings/updateAutoscaling.svelte | 92 ++++++ .../settings/updateBackupStorage.svelte | 277 +++++++++++++++++ .../settings/updateBackups.svelte | 100 ++++++ .../settings/updateConnections.svelte | 227 ++++++++++++++ .../settings/updateCrossRegion.svelte | 291 ++++++++++++++++++ .../settings/updateExtensions.svelte | 185 +++++++++++ .../settings/updateHAStatus.svelte | 251 +++++++++++++++ .../settings/updateMaintenance.svelte | 102 ++++++ .../settings/updateName.svelte | 63 ++++ .../settings/updateNetwork.svelte | 88 ++++++ .../settings/updatePooler.svelte | 130 ++++++++ .../settings/updateReadReplicas.svelte | 246 +++++++++++++++ .../settings/updateSecurity.svelte | 131 ++++++++ .../settings/updateSqlApi.svelte | 146 +++++++++ .../settings/updateStorage.svelte | 68 ++++ .../settings/updateTier.svelte | 91 ++++++ .../settings/upgradeVersion.svelte | 121 ++++++++ 20 files changed, 2902 insertions(+), 4 deletions(-) create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/dangerZone.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/rotateCredentials.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateAutoscaling.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateBackupStorage.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateBackups.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateConnections.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateExtensions.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateHAStatus.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateMaintenance.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateName.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateNetwork.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updatePooler.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateReadReplicas.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateSecurity.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateSqlApi.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateStorage.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateTier.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/upgradeVersion.svelte diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/+page.svelte index 5a9dab551a..f5e687804e 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/+page.svelte @@ -13,16 +13,48 @@ import Delete from '../delete.svelte'; import { Query } from '@appwrite.io/console'; import { Layout, Skeleton } from '@appwrite.io/pink-svelte'; - import type { PageProps } from './$types'; + import type { DedicatedDatabase } from '$lib/sdk/dedicatedDatabases'; import { getTerminologies } from '$database/(entity)'; + import UpdateName from './updateName.svelte'; + import UpdateTier from './updateTier.svelte'; + import UpdateStorage from './updateStorage.svelte'; + import UpdateNetwork from './updateNetwork.svelte'; + import UpdateMaintenance from './updateMaintenance.svelte'; + import UpdateBackups from './updateBackups.svelte'; + import UpdateAutoscaling from './updateAutoscaling.svelte'; + import UpdatePooler from './updatePooler.svelte'; + import UpdateExtensions from './updateExtensions.svelte'; + import UpdateConnections from './updateConnections.svelte'; + import RotateCredentials from './rotateCredentials.svelte'; + import UpgradeVersion from './upgradeVersion.svelte'; + import UpdateReadReplicas from './updateReadReplicas.svelte'; + import UpdateCrossRegion from './updateCrossRegion.svelte'; + import UpdateHAStatus from './updateHAStatus.svelte'; + import UpdateBackupStorage from './updateBackupStorage.svelte'; + import UpdateSecurity from './updateSecurity.svelte'; + import UpdateSqlApi from './updateSqlApi.svelte'; + import DangerZone from './dangerZone.svelte'; - const { data }: PageProps = $props(); + const data = page.data; const database = $derived(data.database); + const dedicatedDatabase = $derived(data.dedicatedDatabase as DedicatedDatabase | null); + const isDedicatedType = $derived( + dedicatedDatabase !== null && + (database.type === 'prisma' || + database.type === 'dedicated' || + database.type === 'shared') + ); + + const isDedicated = $derived(dedicatedDatabase?.type === 'dedicated'); + const isShared = $derived(dedicatedDatabase?.type === 'shared'); + const isPrisma = $derived(dedicatedDatabase?.backend === 'prisma'); + const isPostgres = $derived(dedicatedDatabase?.engine === 'postgres'); + + // Legacy database fallback state let showDelete = $state(false); let databaseName: string | null = $state(null); - let errorMessage: string = $state('Something went wrong'); let errorType: 'error' | 'warning' | 'success' = $state('error'); let showError: false | 'name' | 'email' | 'password' = $state(false); @@ -70,7 +102,108 @@ } -{#if database} +{#if isDedicatedType && dedicatedDatabase} + + + + {dedicatedDatabase.name} + +
+

Created: {toLocaleDateTime(dedicatedDatabase.$createdAt)}

+

Last updated: {toLocaleDateTime(dedicatedDatabase.$updatedAt)}

+
+
+
+ + + + + + {#if isDedicated} + + {/if} + + + {#if isDedicated || isShared} + + {/if} + + + {#if !isPrisma} + + {/if} + + + {#if isDedicated || isShared} + + {/if} + + + + + + {#if isDedicated || isShared} + + {/if} + + + {#if isPostgres && !isPrisma} + + {/if} + + + {#if isPostgres && !isPrisma} + + {/if} + + + {#if !isPrisma} + + {/if} + + + {#if !isPrisma} + + {/if} + + + {#if isDedicated || isShared} + + {/if} + + + {#if isDedicated} + + {/if} + + + {#if isDedicated} + + {/if} + + + {#if !isPrisma} + + {/if} + + + {#if isDedicated} + + {/if} + + + + + + {#if !isPrisma} + + {/if} + + + +
+{:else if database} + {database.name} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/dangerZone.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/dangerZone.svelte new file mode 100644 index 0000000000..21a21b37c2 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/dangerZone.svelte @@ -0,0 +1,66 @@ + + + + Delete database + The database will be permanently deleted, including all data and backups. This action is + irreversible. + + + + +
{database.name}
+ + + {getEngineDisplayName(database.engine)} {database.version} + + +
+
+

Last updated: {toLocaleDateTime(database.$updatedAt)}

+
+
+ + + + +
+ + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/rotateCredentials.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/rotateCredentials.svelte new file mode 100644 index 0000000000..e4d799f956 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/rotateCredentials.svelte @@ -0,0 +1,90 @@ + + + + Credential rotation + Generate new database credentials. Existing connections using the old credentials will be + terminated. + + + Rotating credentials will invalidate the current username and password. All active + connections will be dropped. Make sure to update your application configuration + immediately after rotation. + + + + + + + + + +

+ Are you sure you want to rotate the credentials for {database.name}? This will + generate a new username and password, and all existing connections will be terminated. +

+ + + + +
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateAutoscaling.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateAutoscaling.svelte new file mode 100644 index 0000000000..79360cc2fc --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateAutoscaling.svelte @@ -0,0 +1,92 @@ + + +
+ + Storage autoscaling + Automatically increase storage when disk usage reaches a threshold. Storage will never + exceed the configured maximum. + +
    + + {#if autoscaling} + + + {/if} +
+
+ + + + +
+ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateBackupStorage.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateBackupStorage.svelte new file mode 100644 index 0000000000..35b733314b --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateBackupStorage.svelte @@ -0,0 +1,277 @@ + + +{#if !isLoading} + {#if isConfigured && config} + + Backup storage + Your database backups are stored on an external storage provider for added durability + and disaster recovery. + +
    +
  • +
    + + + Provider: + + {config.provider === 's3' + ? 'Amazon S3' + : config.provider === 'gcs' + ? 'Google Cloud Storage' + : 'Azure Blob Storage'} + + + + Bucket: + {config.bucket} + + + Region: + {config.region} + + {#if config.prefix} + + Prefix: + {config.prefix} + + {/if} + {#if config.endpoint} + + Endpoint: + {config.endpoint} + + {/if} + +
    +
  • +
+
+ + + + +
+ {:else} +
+ + Backup storage + Configure off-cluster backup storage to store backups on an external cloud provider + for added durability and disaster recovery. + +
    + + + + + + + +
+
+ + + + +
+ + {/if} + + +

+ Are you sure you want to remove the off-cluster backup storage configuration for + {database.name}? Existing backups in the external storage will not be deleted, + but new backups will no longer be stored externally. +

+ + + + +
+{/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateBackups.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateBackups.svelte new file mode 100644 index 0000000000..a385f703c9 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateBackups.svelte @@ -0,0 +1,100 @@ + + +
+ + Backups + Configure automatic backups and point-in-time recovery for your database. + +
    + + {#if backupEnabled} + + + + {/if} +
+
+ + + + +
+ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateConnections.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateConnections.svelte new file mode 100644 index 0000000000..7b0b8d9c1e --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateConnections.svelte @@ -0,0 +1,227 @@ + + +{#if !isLoading} +
+ + Database users + Create and manage database users with specific roles. Each user receives unique credentials + for connecting to the database. + +
    + {#if connections.length > 0} +
  • + + + {#each connections as connection} + + + +
    + {connection.username} +
    + + + {connection.database} + + + + + Created: {toLocaleDateTime( + connection.$createdAt + )} + +
    + +
    +
    + {/each} +
    +
  • + {:else} +
  • +

    No database users created.

    +
  • + {/if} + + + +
+
+ + + + +
+ + + +

+ Are you sure you want to delete the database user + {connectionToDelete?.username}? Any active connections using this user will be + terminated. +

+ + + + +
+{/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte new file mode 100644 index 0000000000..f885f535f5 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte @@ -0,0 +1,291 @@ + + +{#if !isLoading} + {#if isEnabled && crossRegionStatus} + + Cross-region failover + Your database has a standby replica in another region for disaster recovery. In the event + of a regional outage, you can trigger a failover to promote the standby to primary. + +
    +
  • + +
    + + + Standby status + + + + Primary: {crossRegionStatus.primaryRegion} + • Standby: {crossRegionStatus.standbyRegion} + + + Lag: {crossRegionStatus.lagSeconds}s + • Last synced: {toLocaleDateTime(crossRegionStatus.lastSyncedAt)} + + +
    +
    +
  • +
+
+ + + + + + + +
+ {:else} +
+ + Cross-region failover + Enable cross-region failover to maintain a standby replica in a different region for + disaster recovery. + +
    + +
+
+ + + + +
+ + {/if} + + +

+ Are you sure you want to disable cross-region failover for {database.name}? + The standby replica will be removed and your database will no longer have + disaster recovery across regions. +

+ + + + +
+ + +

+ Are you sure you want to trigger a cross-region failover for {database.name}? + This will promote the standby replica in {crossRegionStatus?.standbyRegion} + to primary. The current primary in {crossRegionStatus?.primaryRegion} will + become the new standby. This operation may cause brief downtime. +

+ + + + +
+{/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateExtensions.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateExtensions.svelte new file mode 100644 index 0000000000..d90d60b92b --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateExtensions.svelte @@ -0,0 +1,185 @@ + + +{#if !isLoading && extensions} +
+ + Extensions + Manage PostgreSQL extensions for your database. Extensions add additional functionality such + as full-text search, geospatial queries, and more. + +
    + {#if extensions.installed.length > 0} +
  • + + + {#each extensions.installed as ext} + { + extensionToUninstall = ext; + showUninstallConfirm = true; + }} /> + {/each} + +
  • + {:else} +
  • +

    No extensions installed.

    +
  • + {/if} + + {#if availableOptions.length > 0} + + {/if} +
+
+ + + + +
+ + + +

+ Are you sure you want to uninstall the extension {extensionToUninstall} from + {database.name}? Any database objects that depend on this extension may stop + working. +

+ + + + +
+{/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateHAStatus.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateHAStatus.svelte new file mode 100644 index 0000000000..f7f3bfa0bd --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateHAStatus.svelte @@ -0,0 +1,251 @@ + + +{#if !isLoading} +
+ + High availability + High availability maintains replicas of your database that automatically take over if the + primary instance fails, minimizing downtime. + +
    + + {#if haEnabled} + + + {/if} + + {#if haStatus && haStatus.replicas.length > 0} +
  • + + + {#each haStatus.replicas as replica} +
    + + {replica.$id} + + + + Lag: {replica.lagSeconds}s + + +
    + {/each} +
    +
  • + {/if} +
+
+ + + + {#if haEnabled && haStatus?.enabled} + + {/if} + + + +
+ + + +

+ Are you sure you want to trigger a manual failover for {database.name}? + This will promote a replica to primary. The operation may cause brief downtime + while the roles are switched. +

+ + + + +
+{/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateMaintenance.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateMaintenance.svelte new file mode 100644 index 0000000000..75fa757761 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateMaintenance.svelte @@ -0,0 +1,102 @@ + + +
+ + Maintenance window + Schedule a preferred time window for automatic maintenance operations such as minor + version upgrades and patches. + +
    + + + +
+
+ + + + +
+ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateName.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateName.svelte new file mode 100644 index 0000000000..c5dea38b09 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateName.svelte @@ -0,0 +1,63 @@ + + +
+ + Name + + + + + + + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateNetwork.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateNetwork.svelte new file mode 100644 index 0000000000..0ed2ae3e9b --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateNetwork.svelte @@ -0,0 +1,88 @@ + + +
+ + Network + Configure connection limits and network access controls for your database. + +
    + + + +
+
+ + + + +
+ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updatePooler.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updatePooler.svelte new file mode 100644 index 0000000000..e5278cf7e8 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updatePooler.svelte @@ -0,0 +1,130 @@ + + +{#if !isLoading} +
+ + Connection pooler + A connection pooler sits between your application and the database, reusing connections + to reduce overhead. Transaction mode is recommended for serverless workloads. + +
    + + {#if poolerEnabled} + + + {/if} +
+
+ + + + +
+ +{/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateReadReplicas.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateReadReplicas.svelte new file mode 100644 index 0000000000..315ef1236a --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateReadReplicas.svelte @@ -0,0 +1,246 @@ + + +{#if !isLoading} +
+ + Read replicas + Deploy read-only replicas of your database to other regions to reduce read latency for + geographically distributed workloads. + +
    + {#if replicas.length > 0} +
  • + + + {#each replicas as replica} +
    + + + + {replica.$id} + + + + {replica.sourceRegion} → {replica.targetRegion} + • Lag: {replica.lagSeconds}s + • {replica.hostname} + + + + +
    + {/each} +
    +
  • + {:else} +
  • +

    No read replicas configured.

    +
  • + {/if} + + {#if availableRegionOptions.length > 0} + + + {/if} +
+
+ + + + +
+ + + +

+ Are you sure you want to delete the read replica + {replicaToDelete?.$id} in region {replicaToDelete?.targetRegion}? + This action cannot be undone. +

+ + + + +
+{/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateSecurity.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateSecurity.svelte new file mode 100644 index 0000000000..ffee6225a7 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateSecurity.svelte @@ -0,0 +1,131 @@ + + +
+ + Security + Manage encryption, key management, data residency, and audit logging for your database. + +
    +
  • +
    + + + Encryption at rest: + {database.securityEncryptionAtRest ? 'Enabled' : 'Disabled'} + + + Key management: + {getKeyManagementLabel(database.securityKeyManagement)} + + + Data residency: + {getResidencyLabel(database.securityDataResidency)} + + +
    +
  • + + + {#if auditLogEnabled} + + {/if} +
+
+ + + + +
+ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateSqlApi.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateSqlApi.svelte new file mode 100644 index 0000000000..437b303559 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateSqlApi.svelte @@ -0,0 +1,146 @@ + + +
+ + SQL API + The SQL API allows direct SQL query execution against your database through the Appwrite + API. Configure which statements are permitted and set resource limits. + +
    + + {#if sqlApiEnabled} + + + +
  • + + {#each allStatements as statement} + toggleStatement(statement)} /> + {/each} +
  • + {/if} +
+
+ + + + +
+ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateStorage.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateStorage.svelte new file mode 100644 index 0000000000..9428085403 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateStorage.svelte @@ -0,0 +1,68 @@ + + +
+ + Storage + Resize the storage allocated to your database. Storage can only be increased, not + decreased. + + + {#if storageGb < database.storage} + Storage can only be increased, not decreased. + {/if} + + + + + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateTier.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateTier.svelte new file mode 100644 index 0000000000..b4cce42c0c --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateTier.svelte @@ -0,0 +1,91 @@ + + +
+ + Resource scaling + Change the compute resources allocated to your database. Scaling may cause a brief + interruption while the database restarts. + + + + + + + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/upgradeVersion.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/upgradeVersion.svelte new file mode 100644 index 0000000000..d5985424d9 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/upgradeVersion.svelte @@ -0,0 +1,121 @@ + + +{#if !isLoading} + + Version + Upgrade your database engine to a newer version. This operation may cause a brief + interruption. + + + + + Current version + + + {currentVersion} + + + + + + + + + + + + +

+ Are you sure you want to upgrade {database.name} from version + {currentVersion} to {targetVersion}? The database may be briefly + unavailable during the upgrade. +

+ + + + +
+{/if} From 0c1f70f40003072950bd93b3396c06931b96c9d4 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 12 Mar 2026 21:23:33 +1300 Subject: [PATCH 07/56] (feat): Add dedicated database monitoring page --- .../monitoring/+page.svelte | 761 ++++++++++++++++++ .../database-[database]/monitoring/+page.ts | 13 + 2 files changed, 774 insertions(+) create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/monitoring/+page.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/monitoring/+page.ts diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/monitoring/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/monitoring/+page.svelte new file mode 100644 index 0000000000..74517369e5 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/monitoring/+page.svelte @@ -0,0 +1,761 @@ + + +{#if !database} + + + Monitoring is only available for dedicated databases. + + +{:else} + + + + + + (activeSection = 'metrics')} + active={activeSection === 'metrics'}> + Metrics + + (activeSection = 'connections')} + active={activeSection === 'connections'}> + Connections + + (activeSection = 'slowQueries')} + active={activeSection === 'slowQueries'}> + Slow Queries + + (activeSection = 'insights')} + active={activeSection === 'insights'}> + Performance + + (activeSection = 'auditLogs')} + active={activeSection === 'auditLogs'}> + Audit Logs + + + + + + + + {#if activeSection === 'metrics'} + + + + (metricsPeriod = '1h')} + active={metricsPeriod === '1h'}> + 1 Hour + + (metricsPeriod = '24h')} + active={metricsPeriod === '24h'}> + 24 Hours + + (metricsPeriod = '7d')} + active={metricsPeriod === '7d'}> + 7 Days + + (metricsPeriod = '30d')} + active={metricsPeriod === '30d'}> + 30 Days + + + + {#if isLoadingMetrics} + + {#each Array(6) as _} + + + + + {/each} + + {:else if metrics} + + + Resource Utilization + CPU, memory, and storage usage for the selected period. + + + + + CPU Usage + + + {formatPercent(metrics.cpuPercent)} + + + + + Memory Usage + + + {formatPercent(metrics.memoryPercent)} + + {#if metrics.memoryUsedBytes && metrics.memoryMaxBytes} + + {calculateSize(metrics.memoryUsedBytes)} / + {calculateSize(metrics.memoryMaxBytes)} + + {/if} + + + + Storage Used + + + {metrics.storageUsedBytes + ? calculateSize(metrics.storageUsedBytes) + : '-'} + + + + + + + + + Database Activity + Connection count, IOPS, and queries per second. + + + + + Active Connections + + + {formatNumber(metrics.connectionsActive)} + {#if metrics.connectionsMax} + + / {formatNumber(metrics.connectionsMax)} + + {/if} + + + + + IOPS (Read) + + + {formatNumber(metrics.iopsRead)} + + + + + IOPS (Write) + + + {formatNumber(metrics.iopsWrite)} + + + + + Queries per Second + + + {formatNumber(metrics.qps)} + + + + + + {:else} + + Metrics data is not available for this database. Ensure metrics + collection is enabled in the database settings. + + {/if} + + {/if} + + + {#if activeSection === 'connections'} + + + Currently active database connections. + + + {#if isLoadingConnections} + + {#each Array(3) as _} + + {/each} + + {:else if activeConnections.total === 0} +
+ No active connections. +
+ {:else} + + + PID + User + Database + State + Query + Connected + Wait Event + + {#each activeConnections.activeConnections as conn} + + + {conn.pid} + + + {conn.user} + + + {conn.database} + + + + + + + {truncateQuery(conn.query, 80)} + + + + {conn.connectedAt + ? toLocaleDateTime(conn.connectedAt) + : '-'} + + + {conn.waitEvent || '-'} + + + {/each} + + {/if} +
+ {/if} + + + {#if activeSection === 'slowQueries'} + + + Queries that exceeded the slow query threshold + ({database.metricsSlowQueryLogThresholdMs}ms). + + + {#if isLoadingSlowQueries} + + {#each Array(3) as _} + + {/each} + + {:else if slowQueries.total === 0} +
+ No slow queries recorded. +
+ {:else} + + + Query + Duration + Calls + User + Database + + {#each slowQueries.slowQueries as sq, i} + + + + {truncateQuery(sq.query)} + + + + {formatDurationMs(sq.durationMs)} + + + {formatNumber(sq.calls)} + + + {sq.user} + + + {sq.database} + + + {/each} + + {/if} +
+ {/if} + + + {#if activeSection === 'insights'} + + {#if isLoadingInsights} + + {#each Array(3) as _} + + {/each} + + {:else if performanceInsights} + + + Query Summary + Aggregated query performance statistics. + + + + + Total Calls + + + {formatNumber(performanceInsights.totalCalls)} + + + + + Total Time + + + {formatDurationMs(performanceInsights.totalTimeMs)} + + + + + Average Time + + + {formatDurationMs(performanceInsights.avgTimeMs)} + + + + + + + + {#if performanceInsights.topQueries.length > 0} + + + Top Queries by Execution Time + + + + Query + Calls + Total Time + Mean Time + Rows + + {#each performanceInsights.topQueries as tq, i} + + + + {truncateQuery(tq.query)} + + + + {formatNumber(tq.calls)} + + + {formatDurationMs(tq.totalTimeMs)} + + + {formatDurationMs(tq.meanTimeMs)} + + + {formatNumber(tq.rows)} + + + {/each} + + + {/if} + + + {#if performanceInsights.waitEvents.length > 0} + + + Wait Events Analysis + + + + Event + Type + Count + Total Wait + + {#each performanceInsights.waitEvents as we, i} + + + {we.event} + + + {we.type} + + + {formatNumber(we.count)} + + + {formatDurationMs(we.totalWaitMs)} + + + {/each} + + + {/if} + {:else} + + Performance insights data is not available. Ensure metrics collection + is enabled and the database has been active. + + {/if} + + {/if} + + + {#if activeSection === 'auditLogs'} + + + Database audit log entries. + + + {#if !database.securityAuditLogEnabled} + + Audit logging is not enabled for this database. Enable it in the + database settings to start recording audit events. + + {:else if isLoadingAuditLogs} + + {#each Array(3) as _} + + {/each} + + {:else if auditLogs.total === 0} +
+ No audit log entries recorded. +
+ {:else} + + + Timestamp + User + Action + Object + Statement + Client + + {#each auditLogs.auditLogs as log, i} + + + {log.timestamp + ? toLocaleDateTime(log.timestamp) + : '-'} + + + {log.user} + + + + + + {log.object || '-'} + + + + {truncateQuery(log.statement, 80)} + + + + {log.clientAddress || '-'} + + + {/each} + + {/if} +
+ {/if} +
+
+{/if} + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/monitoring/+page.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/monitoring/+page.ts new file mode 100644 index 0000000000..9511c54243 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/monitoring/+page.ts @@ -0,0 +1,13 @@ +import type { PageLoad } from './$types'; +import { Dependencies } from '$lib/constants'; + +export const load: PageLoad = async ({ depends, parent }) => { + depends(Dependencies.DATABASE); + + const { database, dedicatedDatabase } = await parent(); + + return { + database, + dedicatedDatabase + }; +}; From 4d14b213a7640b9cb4dc6846f6f095c0bed531d6 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 12 Mar 2026 21:23:37 +1300 Subject: [PATCH 08/56] (feat): Add dedicated database backups component --- .../database-[database]/backups/+page.svelte | 12 + .../backups/dedicatedBackups.svelte | 685 ++++++++++++++++++ 2 files changed, 697 insertions(+) create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.svelte index 12cb11a541..f01cecb302 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/+page.svelte @@ -5,6 +5,7 @@ import BackupPolicy from './policy.svelte'; import LockedCard from './locked.svelte'; import Table from './table.svelte'; + import DedicatedBackups from './dedicatedBackups.svelte'; import type { PageProps } from './$types'; import CreatePolicy from './createPolicy.svelte'; import { Button } from '$lib/elements/forms'; @@ -24,9 +25,16 @@ import { Layout, Typography } from '@appwrite.io/pink-svelte'; import { page } from '$app/state'; import IconQuestionMarkCircle from './components/questionIcon.svelte'; + import type { DedicatedDatabase } from '$lib/sdk/dedicatedDatabases'; const { data }: PageProps = $props(); + const isDedicatedType = $derived( + data.database?.type === 'prisma' || + data.database?.type === 'dedicated' || + data.database?.type === 'shared' + ); + let policyCreateError: string | null = $state(null); let totalPolicies: UserBackupPolicy[] = $state([]); @@ -170,6 +178,9 @@ }); +{#if isDedicatedType && data.dedicatedDatabase} + +{:else}
{#if !isDisabled} @@ -234,6 +245,7 @@ {/if}
+{/if} + import { page } from '$app/state'; + import { Confirm, Modal } from '$lib/components'; + import { Button } from '$lib/elements/forms'; + import { Container } from '$lib/layout'; + import { CardGrid } from '$lib/components'; + import { toLocaleDateTime } from '$lib/helpers/date'; + import { calculateSize } from '$lib/helpers/sizeConvertion'; + import { addNotification } from '$lib/stores/notifications'; + import { sdk } from '$lib/stores/sdk'; + import { Submit, trackEvent, trackError } from '$lib/actions/analytics'; + import type { + DedicatedDatabase, + Backup, + BackupList, + RestorationList, + PITRWindows + } from '$lib/sdk/dedicatedDatabases'; + import { + ActionMenu, + Alert, + Icon, + Layout, + Popover, + Status, + Table, + Tabs, + Typography + } from '@appwrite.io/pink-svelte'; + import { + IconDotsHorizontal, + IconRefresh, + IconTrash + } from '@appwrite.io/pink-icons-svelte'; + + const { + database + }: { + database: DedicatedDatabase; + } = $props(); + + let backups = $state({ total: 0, backups: [] }); + let restorations = $state({ total: 0, restorations: [] }); + let pitrWindows = $state(null); + + let isLoadingBackups = $state(true); + let isLoadingRestorations = $state(true); + let isLoadingPitr = $state(true); + let isCreatingBackup = $state(false); + + let showDeleteConfirm = $state(false); + let selectedBackup = $state(null); + + let showRestoreConfirm = $state(false); + let restoreBackup = $state(null); + + let showPitrRestore = $state(false); + let pitrTargetDateTime = $state(''); + + let activeTab = $state<'backups' | 'restorations'>('backups'); + + const dedicatedSdk = $derived( + sdk.forProject(page.params.region, page.params.project).dedicatedDatabases + ); + + function mapBackupStatus( + status: string + ): 'ready' | 'processing' | 'failed' | 'pending' | 'complete' { + switch (status) { + case 'completed': + case 'verified': + return 'complete'; + case 'running': + return 'processing'; + case 'failed': + return 'failed'; + case 'pending': + default: + return 'pending'; + } + } + + function mapRestorationStatus( + status: string + ): 'ready' | 'processing' | 'failed' | 'pending' | 'complete' { + switch (status) { + case 'completed': + return 'complete'; + case 'running': + return 'processing'; + case 'failed': + return 'failed'; + case 'pending': + default: + return 'pending'; + } + } + + function formatBackupType(type: string): string { + switch (type) { + case 'full': + return 'Full'; + case 'incremental': + return 'Incremental'; + case 'wal': + return 'WAL'; + default: + return type; + } + } + + function formatTimestamp(ts: number): string { + if (!ts) return '-'; + return toLocaleDateTime(new Date(ts * 1000).toISOString()); + } + + async function loadBackups() { + isLoadingBackups = true; + try { + backups = await dedicatedSdk.listBackups(database.$id); + } catch (error) { + addNotification({ + type: 'error', + message: error.message + }); + } finally { + isLoadingBackups = false; + } + } + + async function loadRestorations() { + isLoadingRestorations = true; + try { + restorations = await dedicatedSdk.listRestorations(database.$id); + } catch (error) { + addNotification({ + type: 'error', + message: error.message + }); + } finally { + isLoadingRestorations = false; + } + } + + async function loadPitrWindows() { + if (!database.backupPitr) { + isLoadingPitr = false; + return; + } + isLoadingPitr = true; + try { + pitrWindows = await dedicatedSdk.getPITRWindows(database.$id); + } catch (error) { + // PITR may not be available yet + pitrWindows = null; + } finally { + isLoadingPitr = false; + } + } + + async function handleCreateBackup() { + isCreatingBackup = true; + try { + await dedicatedSdk.createBackup(database.$id); + addNotification({ + type: 'success', + message: 'Backup creation started' + }); + trackEvent(Submit.DedicatedBackupCreate); + await loadBackups(); + } catch (error) { + addNotification({ + type: 'error', + message: error.message + }); + trackError(error, Submit.DedicatedBackupCreate); + } finally { + isCreatingBackup = false; + } + } + + async function handleDeleteBackup() { + if (!selectedBackup) return; + try { + await dedicatedSdk.deleteBackup(database.$id, selectedBackup.$id); + addNotification({ + type: 'success', + message: 'Backup deleted' + }); + trackEvent(Submit.DedicatedBackupDelete); + showDeleteConfirm = false; + selectedBackup = null; + await loadBackups(); + } catch (error) { + addNotification({ + type: 'error', + message: error.message + }); + trackError(error, Submit.DedicatedBackupDelete); + } + } + + async function handleRestoreBackup() { + if (!restoreBackup) return; + try { + await dedicatedSdk.createRestoration(database.$id, restoreBackup.$id); + addNotification({ + type: 'success', + message: 'Restoration started from backup' + }); + trackEvent(Submit.DedicatedBackupRestore); + showRestoreConfirm = false; + restoreBackup = null; + await loadRestorations(); + } catch (error) { + addNotification({ + type: 'error', + message: error.message + }); + trackError(error, Submit.DedicatedBackupRestore); + } + } + + async function handlePitrRestore() { + if (!pitrTargetDateTime) return; + try { + const targetTime = Math.floor(new Date(pitrTargetDateTime).getTime() / 1000); + await dedicatedSdk.createPITRRestoration(database.$id, targetTime); + addNotification({ + type: 'success', + message: 'Point-in-time restoration started' + }); + trackEvent(Submit.DedicatedPitrRestore); + showPitrRestore = false; + pitrTargetDateTime = ''; + await loadRestorations(); + } catch (error) { + addNotification({ + type: 'error', + message: error.message + }); + trackError(error, Submit.DedicatedPitrRestore); + } + } + + function formatPitrTime(isoString: string): string { + if (!isoString) return '-'; + return toLocaleDateTime(isoString); + } + + function toDateTimeLocalValue(isoString: string): string { + if (!isoString) return ''; + const date = new Date(isoString); + const pad = (n: number) => n.toString().padStart(2, '0'); + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; + } + + // Load data on mount + $effect(() => { + loadBackups(); + loadRestorations(); + loadPitrWindows(); + }); + + + + + + Backup Configuration + Current backup settings for this database. + + + + + Automatic Backups + + + + {#if database.backupEnabled} + + + Schedule + + {database.backupCron} + + + + Retention + + + {database.backupRetentionDays} days + + + + + Point-in-Time Recovery + + + + {/if} + + + + + + + + + {#if database.backupPitr} + + Point-in-Time Recovery + Restore your database to any point within the recovery window. + + {#if isLoadingPitr} + Loading recovery window... + {:else if pitrWindows} + + + + + Earliest Recovery Point + + + {formatPitrTime(pitrWindows.earliest)} + + + + + Latest Recovery Point + + + {formatPitrTime(pitrWindows.latest)} + + + + {#if database.pitrRetentionDays} + + Retention window: {database.pitrRetentionDays} days + + {/if} + + {:else} + + PITR is enabled but no recovery points are available yet. Recovery points + will appear after the first WAL archive is created. + + {/if} + + + {#if pitrWindows} + + {/if} + + + {/if} + + + + + (activeTab = 'backups')} + active={activeTab === 'backups'}> + Backups ({backups.total}) + + (activeTab = 'restorations')} + active={activeTab === 'restorations'}> + Restorations ({restorations.total}) + + + + {#if activeTab === 'backups'} + {#if isLoadingBackups} +
+ Loading backups... +
+ {:else if backups.total === 0} +
+ No backups yet. Create a manual backup or wait for the scheduled backup. +
+ {:else} + + + ID + Type + Status + Size + Started + Completed + Expires + + + {#each backups.backups as backup} + + + + {backup.$id.substring(0, 8)}... + + + + {formatBackupType(backup.type)} + + + + + + {backup.sizeBytes ? calculateSize(backup.sizeBytes) : '-'} + + + {formatTimestamp(backup.startedAt)} + + + {formatTimestamp(backup.completedAt)} + + + {formatTimestamp(backup.expiresAt)} + + + + + + + {#if backup.status === 'completed' || backup.status === 'verified'} + { + toggle(e); + restoreBackup = backup; + showRestoreConfirm = true; + }}> + Restore + + {/if} + { + toggle(e); + selectedBackup = backup; + showDeleteConfirm = true; + }}> + Delete + + + + + + + {/each} + + {/if} + {:else} + {#if isLoadingRestorations} +
+ Loading restorations... +
+ {:else if restorations.total === 0} +
+ No restorations yet. +
+ {:else} + + + ID + Type + Status + Backup ID + Target Time + Started + Completed + + {#each restorations.restorations as restoration} + + + + {restoration.$id.substring(0, 8)}... + + + + {restoration.type === 'pitr' ? 'Point-in-Time' : 'Backup'} + + + + + + {restoration.backupId + ? restoration.backupId.substring(0, 8) + '...' + : '-'} + + + {restoration.targetTime + ? formatTimestamp(restoration.targetTime) + : '-'} + + + {formatTimestamp(restoration.startedAt)} + + + {formatTimestamp(restoration.completedAt)} + + + {/each} + + {/if} + {/if} +
+
+ + + + + Are you sure you want to delete this backup? This action is irreversible. + + {#if selectedBackup?.error} + + {selectedBackup.error} + + {/if} + + + + + + + This will restore your database from the selected backup. Your database will be + unavailable during the restoration process. + + {#if restoreBackup} + + + + Backup ID + + {restoreBackup.$id} + + + + Type + + + {formatBackupType(restoreBackup.type)} + + + + + Size + + + {restoreBackup.sizeBytes + ? calculateSize(restoreBackup.sizeBytes) + : '-'} + + + + + Created + + + {toLocaleDateTime(restoreBackup.$createdAt)} + + + + {/if} + + The database will enter a restoring state and will be unavailable until the + restoration completes. + + + + + + + + + + + + + Select a target date and time to restore your database to. The target must be within + the available recovery window. + + {#if pitrWindows} + + + + Earliest + + + {formatPitrTime(pitrWindows.earliest)} + + + + + Latest + + + {formatPitrTime(pitrWindows.latest)} + + + + {/if} + + + Target Date and Time + + + + + The database will enter a restoring state and will be unavailable until the + restoration completes. All data after the selected point in time will be lost. + + + + + + + + + From a6d03f6054bf51197ff2741de97c4036e1a0f948 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 12 Mar 2026 21:23:43 +1300 Subject: [PATCH 09/56] (feat): Update dedicated database UI views and helpers --- bun.lock | 69 ++- src/lib/actions/analytics.ts | 28 + .../databases/create/+page.svelte | 130 ++++- .../(entity)/helpers/sdk.ts | 26 +- .../(entity)/helpers/terminology.ts | 6 + .../database-[database]/+layout.svelte | 16 +- .../databases/database-[database]/+layout.ts | 2 +- .../dedicatedOverview.svelte | 516 +++++++++++++++++- .../database-[database]/header.svelte | 9 +- .../databases/database-[database]/store.ts | 14 +- .../database-[database]/subNavigation.svelte | 11 +- 11 files changed, 791 insertions(+), 36 deletions(-) diff --git a/bun.lock b/bun.lock index 12377f9149..a51b25d5cf 100644 --- a/bun.lock +++ b/bun.lock @@ -6,12 +6,22 @@ "name": "@appwrite/console", "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@8e7decc", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@3ec199d", "@appwrite.io/pink-icons": "0.25.0", - "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@df765cc", + "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@b92a389", "@appwrite.io/pink-legacy": "^1.0.3", - "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@df765cc", + "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@b92a389", + "@codemirror/autocomplete": "^6.19.0", + "@codemirror/commands": "^6.9.0", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/language": "^6.11.3", + "@codemirror/lint": "^6.9.0", + "@codemirror/search": "^6.5.11", + "@codemirror/state": "^6.5.2", + "@codemirror/view": "^6.38.6", "@faker-js/faker": "^9.9.0", + "@lezer/highlight": "^1.2.1", "@plausible-analytics/tracker": "^0.4.4", "@popperjs/core": "^2.11.8", "@sentry/sveltekit": "^8.38.0", @@ -20,11 +30,13 @@ "@threlte/extras": "^9.7.1", "ai": "^6.0.67", "analytics": "^0.8.16", + "codemirror-json5": "^1.0.3", "cron-parser": "^4.9.0", "dayjs": "^1.11.13", "deep-equal": "^2.2.3", "echarts": "^5.6.0", "ignore": "^6.0.2", + "json5": "^2.2.3", "nanoid": "^5.1.5", "nanotar": "^0.1.1", "pretty-bytes": "^6.1.1", @@ -36,6 +48,7 @@ "devDependencies": { "@eslint/compat": "^1.3.1", "@eslint/js": "^9.31.0", + "@lezer/common": "^1.5.0", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.5", "@playwright/test": "^1.55.1", @@ -108,15 +121,15 @@ "@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@8e7decc", { "dependencies": { "json-bigint": "1.0.0" } }], + "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@3ec199d", {}], "@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="], - "@appwrite.io/pink-icons-svelte": ["@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@df765cc", { "peerDependencies": { "svelte": "^4.0.0" } }], + "@appwrite.io/pink-icons-svelte": ["@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@b92a389", { "peerDependencies": { "svelte": "^4.0.0" } }], "@appwrite.io/pink-legacy": ["@appwrite.io/pink-legacy@1.0.3", "", { "dependencies": { "@appwrite.io/pink-icons": "1.0.0", "the-new-css-reset": "^1.11.2" } }, "sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ=="], - "@appwrite.io/pink-svelte": ["@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@df765cc", { "dependencies": { "@appwrite.io/pink-icons-svelte": "2.0.0-RC.1", "@floating-ui/dom": "^1.6.13", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.6", "@tanstack/svelte-virtual": "^3.13.10", "ansicolor": "^2.0.3", "d3": "^7.9.0", "fuse.js": "^7.1.0", "pretty-bytes": "^6.1.1", "shiki": "^1.18.0", "svelte-motion": "^0.12.2", "svelte-sonner": "^0.3.28" }, "peerDependencies": { "svelte": "^4.0.0" } }], + "@appwrite.io/pink-svelte": ["@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@b92a389", { "dependencies": { "@appwrite.io/pink-icons-svelte": "2.0.0-RC.1", "@floating-ui/dom": "^1.6.13", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.6", "@tanstack/svelte-virtual": "^3.13.10", "ansicolor": "^2.0.3", "d3": "^7.9.0", "fuse.js": "^7.1.0", "pretty-bytes": "^6.1.1", "shiki": "^1.18.0", "svelte-motion": "^0.12.2", "svelte-sonner": "^0.3.28" }, "peerDependencies": { "svelte": "^4.0.0" } }], "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], @@ -154,6 +167,24 @@ "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A=="], + + "@codemirror/commands": ["@codemirror/commands@6.10.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ=="], + + "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.5", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A=="], + + "@codemirror/lang-json": ["@codemirror/lang-json@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/json": "^1.0.0" } }, "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ=="], + + "@codemirror/language": ["@codemirror/language@6.12.2", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg=="], + + "@codemirror/lint": ["@codemirror/lint@6.9.5", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA=="], + + "@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="], + + "@codemirror/state": ["@codemirror/state@6.5.4", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw=="], + + "@codemirror/view": ["@codemirror/view@6.39.17", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-Aim4lFqhbijnchl83RLfABWueSGs1oUCSv0mru91QdhpXQeNKprIdRO9LWA4cYkJvuYTKGJN7++9MXx8XW43ag=="], + "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], @@ -220,6 +251,18 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@lezer/common": ["@lezer/common@1.5.1", "", {}, "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw=="], + + "@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="], + + "@lezer/javascript": ["@lezer/javascript@1.5.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.0" } }, "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA=="], + + "@lezer/json": ["@lezer/json@1.0.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ=="], + + "@lezer/lr": ["@lezer/lr@1.4.8", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA=="], + + "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], + "@melt-ui/pp": ["@melt-ui/pp@0.3.2", "", { "dependencies": { "estree-walker": "^3.0.3", "magic-string": "^0.30.5" }, "peerDependencies": { "@melt-ui/svelte": ">= 0.29.0", "svelte": "^3.55.0 || ^4.0.0 || ^5.0.0-next.1" } }, "sha512-xKkPvaIAFinklLXcQOpwZ8YSpqAFxykjWf8Y/fSJQwsixV/0rcFs07hJ49hJjPy5vItvw5Qa0uOjzFUbXzBypQ=="], "@melt-ui/svelte": ["@melt-ui/svelte@0.86.6", "", { "dependencies": { "@floating-ui/core": "^1.3.1", "@floating-ui/dom": "^1.4.5", "@internationalized/date": "^3.5.0", "dequal": "^2.0.3", "focus-trap": "^7.5.2", "nanoid": "^5.0.4" }, "peerDependencies": { "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.118" } }, "sha512-Jer+M7DgIwT5IHfTayb4Iw/fkkxWNmC/mqn/nMh9JrbPbkxmyabfLQnhJ+JDn5HK77f84j34lubO3iqFtYAfMg=="], @@ -602,8 +645,6 @@ "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], - "bignumber.js": ["bignumber.js@9.0.0", "", {}, "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A=="], - "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], @@ -646,6 +687,8 @@ "code-red": ["code-red@1.0.4", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@types/estree": "^1.0.1", "acorn": "^8.10.0", "estree-walker": "^3.0.3", "periscopic": "^3.1.0" } }, "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw=="], + "codemirror-json5": ["codemirror-json5@1.0.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", "json5": "^2.2.1", "lezer-json5": "^2.0.2" } }, "sha512-HmmoYO2huQxoaoG5ARKjqQc9mz7/qmNPvMbISVfIE2Gk1+4vZQg9X3G6g49MYM5IK00Ol3aijd7OKrySuOkA7Q=="], + "color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="], "color-convert": ["color-convert@3.1.3", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg=="], @@ -662,6 +705,8 @@ "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], + "cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -996,8 +1041,6 @@ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], @@ -1016,6 +1059,8 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "lezer-json5": ["lezer-json5@2.0.2", "", { "dependencies": { "@lezer/lr": "^1.0.0" } }, "sha512-NRmtBlKW/f8mA7xatKq8IUOq045t8GVHI4kZXrUtYYUdiVeGiO6zKGAV7/nUAnf5q+rYTY+SWX/gvQdFXMjNxQ=="], + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], @@ -1310,6 +1355,8 @@ "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], + "style-value-types": ["style-value-types@5.1.2", "", { "dependencies": { "hey-listen": "^1.0.8", "tslib": "2.4.0" } }, "sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -1430,6 +1477,8 @@ "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], "webgl-sdf-generator": ["webgl-sdf-generator@1.1.1", "", {}, "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA=="], diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 6cc10c1602..cf124d072f 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -286,6 +286,34 @@ export enum Submit { DatabaseImportJSON = 'submit_database_import_json', DatabaseBackupDelete = 'submit_database_backup_delete', DatabaseBackupPolicyCreate = 'submit_database_backup_policy_create', + DatabaseUpdateTier = 'submit_database_update_tier', + DatabaseResizeStorage = 'submit_database_resize_storage', + DatabaseUpdateNetwork = 'submit_database_update_network', + DatabaseUpdateMaintenance = 'submit_database_update_maintenance', + DatabaseUpdateBackups = 'submit_database_update_backups', + DatabaseUpdateAutoscaling = 'submit_database_update_autoscaling', + DatabaseUpdatePooler = 'submit_database_update_pooler', + DatabaseRotateCredentials = 'submit_database_rotate_credentials', + DatabaseUpgradeVersion = 'submit_database_upgrade_version', + DedicatedBackupCreate = 'submit_dedicated_backup_create', + DedicatedBackupDelete = 'submit_dedicated_backup_delete', + DedicatedBackupRestore = 'submit_dedicated_backup_restore', + DedicatedPitrRestore = 'submit_dedicated_pitr_restore', + DatabaseInstallExtension = 'submit_database_install_extension', + DatabaseUninstallExtension = 'submit_database_uninstall_extension', + DatabaseCreateConnection = 'submit_database_create_connection', + DatabaseDeleteConnection = 'submit_database_delete_connection', + DatabaseCreateReadReplica = 'submit_database_create_read_replica', + DatabaseDeleteReadReplica = 'submit_database_delete_read_replica', + DatabaseEnableCrossRegion = 'submit_database_enable_cross_region', + DatabaseDisableCrossRegion = 'submit_database_disable_cross_region', + DatabaseTriggerCrossRegionFailover = 'submit_database_trigger_cross_region_failover', + DatabaseUpdateHA = 'submit_database_update_ha', + DatabaseManualFailover = 'submit_database_manual_failover', + DatabaseConfigureBackupStorage = 'submit_database_configure_backup_storage', + DatabaseDeleteBackupStorage = 'submit_database_delete_backup_storage', + DatabaseUpdateSecurity = 'submit_database_update_security', + DatabaseUpdateSqlApi = 'submit_database_update_sql_api', ColumnCreate = 'submit_column_create', ColumnUpdate = 'submit_column_update', diff --git a/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte index 2e11b85f6c..d632f4b605 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte @@ -52,6 +52,14 @@ const isDark = $derived($app.themeInUse === 'dark'); const backupsImg = $derived(isDark ? EmptyDarkMobile : EmptyLightMobile); + // Free tier limits for shared databases + const sharedTierLimits = { + storage: '1 GB', + maxConnections: 10, + queryTimeout: '15s', + idleTimeout: '15 min' + }; + const databaseTypes: Array<{ type: DatabaseType; title: string; @@ -77,6 +85,12 @@ subtitle: 'Managed PostgreSQL with direct connections. Best for high-performance SQL workloads.' }, + { + type: 'shared', + title: 'Shared (Free)', + subtitle: + 'Free serverless PostgreSQL that scales to zero when idle. Great for prototyping and small projects.' + }, { type: 'dedicated', title: 'DedicatedDB', @@ -95,16 +109,33 @@ const regionOptions = $derived(filterRegions($regionsStore.regions || [])); const tierOptions = [ - { value: 'starter', label: 'Starter - 0.5 CPU, 512MB RAM, 10GB Storage' }, - { value: 'standard', label: 'Standard - 1 CPU, 2GB RAM, 50GB Storage' }, - { value: 'professional', label: 'Professional - 2 CPU, 4GB RAM, 100GB Storage' }, - { value: 'enterprise', label: 'Enterprise - 4 CPU, 8GB RAM, 250GB Storage' } + { value: 's-1vcpu-1gb', label: 'Starter - 1 vCPU, 1GB RAM - $15/mo' }, + { value: 's-2vcpu-2gb', label: 'Standard - 2 vCPU, 2GB RAM - $30/mo' }, + { value: 's-2vcpu-4gb', label: 'Standard Plus - 2 vCPU, 4GB RAM - $60/mo' }, + { value: 's-4vcpu-8gb', label: 'Professional - 4 vCPU, 8GB RAM - $100/mo' }, + { value: 's-4vcpu-16gb', label: 'Business - 4 vCPU, 16GB RAM - $190/mo' }, + { value: 's-4vcpu-32gb', label: 'Business Plus - 4 vCPU, 32GB RAM - $370/mo' }, + { value: 's-8vcpu-32gb', label: 'Enterprise - 8 vCPU, 32GB RAM - $620/mo' }, + { value: 's-8vcpu-64gb', label: 'Enterprise Plus - 8 vCPU, 64GB RAM - $860/mo' } ]; + const tierConnectionLimits: Record = { + 's-1vcpu-1gb': 100, + 's-2vcpu-2gb': 200, + 's-2vcpu-4gb': 500, + 's-4vcpu-8gb': 1000, + 's-4vcpu-16gb': 2000, + 's-4vcpu-32gb': 4000, + 's-8vcpu-32gb': 5000, + 's-8vcpu-64gb': 10000 + }; + + const maxConnectionsForTier = $derived(tierConnectionLimits[selectedTier] ?? 100); + // State for dedicated/prisma options let selectedEngine = $state('postgres'); let selectedRegion = $state(null); - let selectedTier = $state('starter'); + let selectedTier = $state('s-1vcpu-1gb'); // Set default region when regions load $effect(() => { @@ -116,9 +147,10 @@ let highAvailability = $state(false); // Helper to check database type capabilities - const showRegionSelect = $derived(type === 'prisma' || type === 'dedicated'); + const showRegionSelect = $derived(type === 'prisma' || type === 'dedicated' || type === 'shared'); const showTierSelect = $derived(type === 'dedicated'); const showEngineSelect = $derived(type === 'dedicated'); + const isSharedType = $derived(type === 'shared'); // Backup system varies by database type const backupSystem = $derived.by(() => { @@ -128,6 +160,8 @@ return 'appwrite'; case 'prisma': return 'prisma'; + case 'shared': + return 'shared'; case 'dedicated': return 'dedicated'; default: @@ -161,6 +195,7 @@ let selectedBackupPolicy = $state('daily'); let backupRetentionDays = $state(7); let backupPitr = $state(false); + let pitrRetentionDays = $state(7); // Derive backup settings from selected policy const backupEnabled = $derived(selectedBackupPolicy !== 'none'); @@ -243,6 +278,12 @@ region: selectedRegion, tier: selectedTier } as DedicatedDatabaseParams); + } else if (type === 'shared') { + database = await databaseSdk.create(type, { + databaseId, + name: databaseName, + region: selectedRegion + } as DedicatedDatabaseParams); } else if (type === 'dedicated') { database = await databaseSdk.create(type, { databaseId, @@ -254,7 +295,8 @@ backupEnabled, backupSchedule: backupEnabled ? selectedBackupSchedule : undefined, backupRetentionDays: backupEnabled ? backupRetentionDays : undefined, - backupPitr: backupEnabled ? backupPitr : undefined + backupPitr: backupEnabled ? backupPitr : undefined, + pitrRetentionDays: backupEnabled && backupPitr ? pitrRetentionDays : undefined } as DedicatedDatabaseParams); } else { database = await databaseSdk.create(type, { @@ -362,6 +404,49 @@ {/if} + {#if isSharedType} +
+ + Shared databases are free and scale to zero when idle. The following + limits apply: + + + + + Storage + + + {sharedTierLimits.storage} + + + + + Max Connections + + + {sharedTierLimits.maxConnections} + + + + + Query Timeout + + + {sharedTierLimits.queryTimeout} + + + + + Idle Timeout + + + {sharedTierLimits.idleTimeout} (scales to zero) + + + +
+ {/if} +
{#if backupSystem === 'appwrite'} {#if isCloud} @@ -371,6 +456,8 @@ {/if} {:else if backupSystem === 'prisma'} {@render prismaBackupOptions()} + {:else if backupSystem === 'shared'} + {@render sharedBackupOptions()} {:else if backupSystem === 'dedicated'} {@render dedicatedBackupOptions()} {/if} @@ -454,6 +541,15 @@ {/snippet} +{#snippet sharedBackupOptions()} + + + Shared databases on the free tier do not include automatic backups. Upgrade to a + dedicated database for configurable backup and point-in-time recovery options. + + +{/snippet} + {#snippet dedicatedBackupOptions()} {#if backupPitr} + + - PITR allows you to restore your database to any point within the retention - window using WAL archiving. This provides more granular recovery options but - increases storage usage. + PITR allows you to restore your database to any point within the {pitrRetentionDays}-day + retention window using WAL archiving. This provides more granular recovery + options but increases storage usage. {/if} {/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts index fac292c65d..28a08a4def 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts @@ -24,6 +24,7 @@ export type DedicatedDatabaseParams = { backupSchedule?: string; backupRetentionDays?: number; backupPitr?: boolean; + pitrRetentionDays?: number; }; export type DatabaseSdkResult = { @@ -142,6 +143,18 @@ export function useDatabaseSdk( tier: prismaParams.tier })) as unknown as Models.Database; } + case 'shared': { + // Shared (free tier) databases via compute/databases with type: 'shared' + const sharedParams = params as DedicatedDatabaseParams; + return (await baseSdk.dedicatedDatabases.create({ + databaseId: sharedParams.databaseId, + name: sharedParams.name, + backend: 'appwrite', + engine: 'postgres', + region: sharedParams.region, + type: 'shared' + })) as unknown as Models.Database; + } case 'dedicated': { // Dedicated databases are created via the compute/databases endpoint // with backend: 'appwrite' @@ -157,7 +170,8 @@ export function useDatabaseSdk( backupEnabled: dedicatedParams.backupEnabled, backupSchedule: dedicatedParams.backupSchedule, backupRetentionDays: dedicatedParams.backupRetentionDays, - backupPitr: dedicatedParams.backupPitr + backupPitr: dedicatedParams.backupPitr, + pitrRetentionDays: dedicatedParams.pitrRetentionDays })) as unknown as Models.Database; } case 'vectordb': @@ -195,6 +209,7 @@ export function useDatabaseSdk( return toSupportiveEntity(table); } case 'prisma': + case 'shared': case 'dedicated': throw new Error('External databases do not support entity creation via Appwrite'); case 'documentsdb': { @@ -220,6 +235,7 @@ export function useDatabaseSdk( return { total, entities: tables.map(toSupportiveEntity) }; } case 'prisma': + case 'shared': case 'dedicated': { // External databases don't have entities managed by Appwrite return { total: 0, entities: [] }; @@ -247,6 +263,7 @@ export function useDatabaseSdk( return toSupportiveEntity(table); } case 'prisma': + case 'shared': case 'dedicated': throw new Error('External databases do not support entity retrieval via Appwrite'); case 'documentsdb': { @@ -271,6 +288,7 @@ export function useDatabaseSdk( case 'documentsdb': return await baseSdk.documentsDB.delete(params); case 'prisma': + case 'shared': case 'dedicated': await baseSdk.dedicatedDatabases.delete(params); return {}; @@ -290,6 +308,7 @@ export function useDatabaseSdk( tableId: params.entityId }); case 'prisma': + case 'shared': case 'dedicated': throw new Error('External databases do not support entity deletion via Appwrite'); case 'documentsdb': @@ -316,6 +335,7 @@ export function useDatabaseSdk( permissions: params.permissions }); case 'prisma': + case 'shared': case 'dedicated': throw new Error('External databases do not support record creation via Appwrite'); case 'documentsdb': @@ -345,6 +365,7 @@ export function useDatabaseSdk( permissions: params.permissions }); case 'prisma': + case 'shared': case 'dedicated': throw new Error('External databases do not support record updates via Appwrite'); case 'documentsdb': @@ -373,6 +394,7 @@ export function useDatabaseSdk( permissions: params.permissions }); case 'prisma': + case 'shared': case 'dedicated': throw new Error('External databases do not support permission updates via Appwrite'); case 'documentsdb': @@ -401,6 +423,7 @@ export function useDatabaseSdk( return toSupportiveRecord(row); } case 'prisma': + case 'shared': case 'dedicated': throw new Error('External databases do not support record deletion via Appwrite'); case 'documentsdb': { @@ -430,6 +453,7 @@ export function useDatabaseSdk( return { total, records: rows.map(toSupportiveRecord) }; } case 'prisma': + case 'shared': case 'dedicated': throw new Error('External databases do not support bulk record deletion via Appwrite'); case 'documentsdb': { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/terminology.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/terminology.ts index 6896fde2c6..486102e3c8 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/terminology.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/terminology.ts @@ -14,6 +14,7 @@ export type DatabaseType = | 'documentsdb' | 'vectordb' | 'prisma' + | 'shared' | 'dedicated'; export type RecordType = ImplementedDBTypes[keyof ImplementedDBTypes]['record']; @@ -72,6 +73,11 @@ export const baseTerminology = { field: 'column', record: 'row' }, + shared: { + entity: 'table', + field: 'column', + record: 'row' + }, dedicated: { entity: 'table', field: 'column', diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte index 1fc25c2555..edffc6fc9e 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.svelte @@ -32,7 +32,7 @@ // Check if this is a dedicated database type $: isDedicatedType = - terminology.type === 'prisma' || terminology.type === 'dedicated'; + terminology.type === 'prisma' || terminology.type === 'dedicated' || terminology.type === 'shared'; $: $registerCommands([ { @@ -119,6 +119,20 @@ keys: ['g', 'b'], group: 'databases' }, + { + label: 'Go to monitoring', + callback() { + goto( + `${base}/project-${page.params.region}-${project}/databases/database-${databaseId}/monitoring` + ); + }, + disabled: + page.url.pathname.includes('/monitoring') || + page.url.pathname.includes('table-') || + !isDedicatedType, + keys: ['g', 'm'], + group: 'databases' + }, { label: 'Go to settings', callback() { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.ts index efee99629c..064babc131 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/+layout.ts @@ -12,7 +12,7 @@ type DatabaseWithType = Models.Database & { }; function isDedicatedDatabaseType(type: string | undefined): boolean { - return type === 'prisma' || type === 'dedicated'; + return type === 'prisma' || type === 'dedicated' || type === 'shared'; } export const load: LayoutLoad = async ({ params, depends }) => { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/dedicatedOverview.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/dedicatedOverview.svelte index 24ffde4499..8b8035c846 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/dedicatedOverview.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/dedicatedOverview.svelte @@ -9,7 +9,7 @@ import { sdk } from '$lib/stores/sdk'; import { Dependencies } from '$lib/constants'; import { trackEvent } from '$lib/actions/analytics'; - import type { DedicatedDatabase } from '$lib/sdk/dedicatedDatabases'; + import type { DedicatedDatabase, MaintenanceDay, UpgradePolicy, KeyManagement, DataResidency } from '$lib/sdk/dedicatedDatabases'; import { Badge, Layout, @@ -32,10 +32,20 @@ let isRefreshing = $state(false); let isColdStarting = $state(false); + let isPausing = $state(false); + let isResuming = $state(false); + let isSpinningDown = $state(false); let connectionTab = $state<'direct' | 'string'>('direct'); // Check if this is a Prisma database const isPrisma = $derived(database.backend === 'prisma'); + const isDedicated = $derived(database.type === 'dedicated'); + const isShared = $derived(database.type === 'shared'); + const isActive = $derived(database.status === 'ready' || database.status === 'active'); + const isPaused = $derived(database.status === 'paused'); + const containerIsRunning = $derived( + database.containerStatus === 'running' || database.containerStatus === 'active' + ); // Map database status to Status component status const statusComponentStatus = $derived.by((): 'ready' | 'processing' | 'failed' | 'pending' => { @@ -63,6 +73,8 @@ case 'active': return 'ready'; case 'starting': + case 'spinning_down': + case 'freezing': return 'processing'; case 'inactive': default: @@ -89,6 +101,19 @@ return `${gb} GB`; } + const tierConnectionLimits: Record = { + 's-1vcpu-1gb': 100, + 's-2vcpu-2gb': 200, + 's-2vcpu-4gb': 500, + 's-4vcpu-8gb': 1000, + 's-4vcpu-16gb': 2000, + 's-4vcpu-32gb': 4000, + 's-8vcpu-32gb': 5000, + 's-8vcpu-64gb': 10000 + }; + + const tierMaxConnections = $derived(tierConnectionLimits[database.tier] ?? null); + function getEngineDisplayName(engine: string): string { switch (engine) { case 'postgres': @@ -129,7 +154,7 @@ try { await sdk .forProject(page.params.region, page.params.project) - .dedicatedDatabases.coldStart(database.$id); + .dedicatedDatabases.updateActivity(database.$id); addNotification({ type: 'success', @@ -150,6 +175,75 @@ } } + async function pauseDatabase() { + isPausing = true; + try { + await sdk + .forProject(page.params.region, page.params.project) + .dedicatedDatabases.update(database.$id, { status: 'paused' }); + + addNotification({ + type: 'success', + message: 'Database is pausing' + }); + trackEvent('click_database_pause'); + setTimeout(() => invalidate(Dependencies.DATABASE), 2000); + } catch (error) { + addNotification({ + type: 'error', + message: error.message + }); + } finally { + isPausing = false; + } + } + + async function resumeDatabase() { + isResuming = true; + try { + await sdk + .forProject(page.params.region, page.params.project) + .dedicatedDatabases.update(database.$id, { status: 'active' }); + + addNotification({ + type: 'success', + message: 'Database is resuming' + }); + trackEvent('click_database_resume'); + setTimeout(() => invalidate(Dependencies.DATABASE), 2000); + } catch (error) { + addNotification({ + type: 'error', + message: error.message + }); + } finally { + isResuming = false; + } + } + + async function spinDownDatabase() { + isSpinningDown = true; + try { + await sdk + .forProject(page.params.region, page.params.project) + .dedicatedDatabases.update(database.$id, { status: 'inactive' }); + + addNotification({ + type: 'success', + message: 'Database container is spinning down' + }); + trackEvent('click_database_spin_down'); + setTimeout(() => invalidate(Dependencies.DATABASE), 2000); + } catch (error) { + addNotification({ + type: 'error', + message: error.message + }); + } finally { + isSpinningDown = false; + } + } + // Check if connection details are available const hasConnectionDetails = $derived( database.hostname && database.connectionUser && database.connectionPassword @@ -169,6 +263,91 @@ return database.connectionString; } } + + function formatMaintenanceDay(day: MaintenanceDay): string { + const days: Record = { + sun: 'Sunday', + mon: 'Monday', + tue: 'Tuesday', + wed: 'Wednesday', + thu: 'Thursday', + fri: 'Friday', + sat: 'Saturday' + }; + return days[day] ?? day; + } + + function formatHourUtc(hour: number): string { + const h = hour % 24; + const suffix = h >= 12 ? 'PM' : 'AM'; + const display = h === 0 ? 12 : h > 12 ? h - 12 : h; + return `${display}:00 ${suffix} UTC`; + } + + function formatUpgradePolicy(policy: UpgradePolicy): string { + switch (policy) { + case 'autoMinor': + return 'Auto (minor versions)'; + case 'manual': + return 'Manual'; + case 'scheduled': + return 'Scheduled'; + default: + return policy; + } + } + + function formatBytes(bytes: number): string { + if (bytes >= 1_073_741_824) { + return `${(bytes / 1_073_741_824).toFixed(1)} GB`; + } + if (bytes >= 1_048_576) { + return `${(bytes / 1_048_576).toFixed(1)} MB`; + } + if (bytes >= 1024) { + return `${(bytes / 1024).toFixed(1)} KB`; + } + return `${bytes} B`; + } + + function formatKeyManagement(km: KeyManagement): string { + switch (km) { + case 'appwriteKms': + return 'Appwrite KMS'; + case 'customerManaged': + return 'Customer Managed'; + default: + return km; + } + } + + function formatDataResidency(dr: DataResidency): string { + switch (dr) { + case 'eu': + return 'EU'; + case 'us': + return 'US'; + case 'apac': + return 'APAC'; + case 'global': + return 'Global'; + default: + return dr; + } + } + + function formatStorageClass(sc: string): string { + switch (sc) { + case 'ssd': + return 'SSD'; + case 'nvme': + return 'NVMe'; + case 'hdd': + return 'HDD'; + default: + return sc.toUpperCase(); + } + } @@ -215,6 +394,21 @@ {isColdStarting ? 'Starting...' : 'Start Database'} {/if} + {#if isDedicated && isActive && !isPrisma} + + {/if} + {#if isPaused} + + {/if} + {#if isShared && isActive && containerIsRunning && !isPrisma} + + {/if}
+ {#if database.externalIP || database.internalIP} + + {#if database.externalIP} + + {/if} + {#if database.internalIP} + + {/if} + + {/if} {:else} {/if} + + {#if database.type === 'shared'} + + Free Tier Limits + Your shared database runs within the free tier. Resources are constrained to the + limits below. Upgrade to a dedicated database for higher limits. + + + + + Storage + + 1 GB + + + + Max Connections + + 10 + + + + Query Timeout + + 15s + + + + Idle Timeout + + + 15 min + + (scales to zero) + + + + + + + {/if} + Resources @@ -350,6 +596,16 @@ {storageDisplay} + {#if database.storageClass} + + + Storage Class + + + {formatStorageClass(database.storageClass)} + + + {/if}
@@ -411,7 +667,11 @@ Max Connections - {database.networkMaxConnections} + {database.networkMaxConnections}{#if tierMaxConnections} + + / {tierMaxConnections.toLocaleString()} (tier limit) + + {/if} @@ -474,11 +734,18 @@ Point-in-Time Recovery - + + + {#if database.backupPitr && database.pitrRetentionDays} + + ({database.pitrRetentionDays} day window) + + {/if} + @@ -498,6 +765,239 @@ + + + + Storage Autoscaling + Automatically expand storage when usage reaches the configured threshold. + + + + + Status + + + + {#if database.storageAutoscaling} + + + Threshold + + + {database.storageAutoscalingThresholdPercent}% + + + + + Max Storage + + + {formatStorage(database.storageAutoscalingMaxGb)} + + + {/if} + + + + + + + Security + Encryption, key management, and audit logging configuration. + + + + + Encryption at Rest + + + + + + Key Management + + + {formatKeyManagement(database.securityKeyManagement)} + + + + + Key Rotation + + + {database.securityKeyRotationDays} days + + + + + Audit Log + + + + {#if database.securityAuditLogEnabled} + + + Log Retention + + + {database.securityLogRetentionDays} days + + + {/if} + + + Data Residency + + + {formatDataResidency(database.securityDataResidency)} + + + + + + + + + Maintenance Window + Scheduled maintenance window and upgrade policy for your database. + + + + + Day + + + {formatMaintenanceDay(database.maintenanceWindowDay)} + + + + + Time + + + {formatHourUtc(database.maintenanceWindowHourUtc)} + + + + + Duration + + + {database.maintenanceWindowDurationMinutes} minutes + + + + + Upgrade Policy + + + {formatUpgradePolicy(database.maintenanceUpgradePolicy)} + + + + + + + + + SQL API + Execute SQL statements directly through the Appwrite API. + + + + + + Status + + + + {#if database.sqlApiEnabled} + + + Max Response Size + + + {formatBytes(database.sqlApiMaxBytes)} + + + + + Max Rows + + + {database.sqlApiMaxRows.toLocaleString()} + + + + + Timeout + + + {database.sqlApiTimeoutSeconds}s + + + {/if} + + + {#if database.sqlApiEnabled && database.sqlApiAllowedStatements?.length > 0} + + + Allowed Statements + + + {#each database.sqlApiAllowedStatements as statement} + + {/each} + + + {/if} + + + + + + {#if database.metricsEnabled} + + Monitoring + Performance monitoring and slow query detection settings. + + + + + Slow Query Threshold + + + {database.metricsSlowQueryLogThresholdMs.toLocaleString()} ms + + + + + Trace Sample Rate + + + {(database.metricsTraceSampleRate * 100).toFixed(0)}% + + + + + + {/if}
+ + + + + + + + + + +db-prod-primary +4 vCPU · 16 GB + +Active + + + + + + + +db-prod-replica +4 vCPU · 16 GB + +Active + + + + + + + +db-staging +2 vCPU · 8 GB + + + + +Performance +Last 24 hours + + + + + + + + + + + + +CPU + + +Memory + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/(assets)/dedicated-db.svg b/src/routes/(console)/project-[region]-[project]/databases/(assets)/dedicated-db.svg index 8cdf27a121..e8ab1f6d11 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/(assets)/dedicated-db.svg +++ b/src/routes/(console)/project-[region]-[project]/databases/(assets)/dedicated-db.svg @@ -1,12 +1,82 @@ - - - - - - - - - - - Dedicated Database + + + + + + + + + + + + + +db-prod-primary +4 vCPU · 16 GB + +Active + + + + + + + +db-prod-replica +4 vCPU · 16 GB + +Active + + + + + + + +db-staging +2 vCPU · 8 GB + + + + +Performance +Last 24 hours + + + + + + + + + + + + +CPU + + +Memory + + + + + + + + + + + + + + + + + + + + + + From 8cc7456b268649a3e231ea498232e2aae2c13546 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 1 Apr 2026 03:47:47 +1300 Subject: [PATCH 39/56] (refactor): update database list page create flow --- .../project-[region]-[project]/databases/+page.svelte | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/+page.svelte index 2cdda5e173..a2d603fd6d 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/+page.svelte @@ -3,7 +3,7 @@ import { page } from '$app/state'; import { PaginationWithLimit } from '$lib/components'; import { Button } from '$lib/elements/forms'; - import { Container } from '$lib/layout'; + import { Container, ResponsiveContainerHeader } from '$lib/layout'; import Grid from './grid.svelte'; import Table from './table.svelte'; @@ -11,6 +11,7 @@ import { registerCommands } from '$lib/commandCenter'; import { canWriteDatabases } from '$lib/stores/roles'; import { IconPlus } from '@appwrite.io/pink-icons-svelte'; + import { Icon, Tooltip } from '@appwrite.io/pink-svelte'; import EmptySearch from '$lib/components/emptySearch.svelte'; import { isServiceLimited } from '$lib/stores/billing'; import { organization } from '$lib/stores/organization'; @@ -21,6 +22,7 @@ import { resolveRoute, withPath } from '$lib/stores/navigation'; import EmptyDatabaseCloud from './empty.svelte'; + import { columns } from './store'; const { data }: PageProps = $props(); @@ -61,7 +63,7 @@ From 258ce4ddfe0139e1e262afeb410f36ec66405a04 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 1 Apr 2026 03:47:51 +1300 Subject: [PATCH 40/56] (feat): add backup type selection and verification for dedicated databases --- .../backups/dedicatedBackups.svelte | 149 +++++++++++++++++- 1 file changed, 143 insertions(+), 6 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte index 29cd24c1fd..33eba3b13d 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte @@ -1,7 +1,7 @@
+ + + + + Verify the integrity of this backup by restoring it to a temporary environment and + running health checks. + + {#if verifyBackup} + + + + Backup ID + + {verifyBackup.$id} + + + + Type + + + {formatBackupType(verifyBackup.type)} + + + + + Size + + + {verifyBackup.sizeBytes ? calculateSize(verifyBackup.sizeBytes) : '-'} + + + + + Created + + + {toLocaleDateTime(verifyBackup.$createdAt)} + + + + {/if} + + A temporary database will be created to restore and validate this backup. This does not + affect your production database. The backup status will change to "verified" on success. + + + + + + + + @@ -609,6 +719,33 @@ + + + + Select the backup type to create. + + + + + + + + + + + From 235ae718ec6fa8c0e69aee0627340f1eabf4671a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 1 Apr 2026 03:47:54 +1300 Subject: [PATCH 41/56] (feat): add schema, explain, tuning, and index monitoring tabs --- .../monitoring/+page.svelte | 464 +++++++++++++++++- 1 file changed, 450 insertions(+), 14 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/monitoring/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/monitoring/+page.svelte index aeb6faad79..d331382349 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/monitoring/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/monitoring/+page.svelte @@ -1,6 +1,6 @@ {#if !database} @@ -193,7 +359,6 @@
{:else} - @@ -221,18 +386,49 @@ active={activeSection === 'auditLogs'}> Audit Logs + (activeSection = 'schema')} + active={activeSection === 'schema'}> + Schema + + (activeSection = 'schemaPreview')} + active={activeSection === 'schemaPreview'}> + Schema Preview + + (activeSection = 'explain')} + active={activeSection === 'explain'}> + Explain + + (activeSection = 'tuning')} + active={activeSection === 'tuning'}> + Tuning + + (activeSection = 'indexes')} + active={activeSection === 'indexes'}> + Indexes + - + {#if activeSection === 'metrics' || activeSection === 'slowQueries' || activeSection === 'insights' || activeSection === 'auditLogs'} + + {/if} {#if activeSection === 'metrics'} - {:else if metrics} - Resource Utilization CPU, memory, and storage usage for the selected period. @@ -320,7 +515,6 @@ - Database Activity Connection count, IOPS, and queries per second. @@ -449,7 +643,6 @@ {/each} {:else if performanceInsights} - Query Summary Aggregated query performance statistics. @@ -489,7 +682,6 @@ - {#if performanceInsights.topQueries.length > 0} @@ -533,7 +725,6 @@ {/if} - {#if performanceInsights.waitEvents.length > 0} @@ -633,6 +824,251 @@ {/if} {/if} + + + {#if activeSection === 'schema'} + + + + Current database schema (tables, columns, types, constraints, and + indexes). + + + + + {#if isLoadingSchema} + + {#each Array(5) as _} + + {/each} + + {:else if schema} + + {:else} + + Schema data is not available for this database. + + {/if} + + {/if} + + + {#if activeSection === 'schemaPreview'} + + + Preview the impact of a SQL schema change without applying it. + + + + This performs a dry-run analysis only. No changes will be applied to your + database. + + + + + + + {#if previewResult} + + {/if} + + + {#if previewResult} + + {/if} + + {/if} + + + {#if activeSection === 'explain'} + + + Run EXPLAIN on a SQL query to view its execution plan. + + + + + + + {#if explainAnalyze} + + EXPLAIN ANALYZE will execute the query against your database. Use + with caution on write operations. + + {/if} + + + + + {#if explainResult} + + {/if} + + + {#if explainResult} + + {/if} + + {/if} + + + {#if activeSection === 'tuning'} + + + + Configuration tuning recommendations based on workload analysis. + + + + + {#if isLoadingTuning} + + {#each Array(4) as _} + + {/each} + + {:else if tuningResult} + {@const entries = renderEntries(tuningResult)} + {#if entries.length === 0} + + No tuning recommendations at this time. Your configuration looks + good. + + {:else} + + + Parameter + Recommendation + + {#each entries as entry, i} + + + {entry.key} + + + {entry.value} + + + {/each} + + {/if} + {:else} + + Tuning recommendations are not available. Ensure the database has been + active with sufficient workload data. + + {/if} + + {/if} + + + {#if activeSection === 'indexes'} + + + + Index suggestions based on query patterns and table statistics. + + + + + {#if isLoadingIndexSuggestions} + + {#each Array(3) as _} + + {/each} + + {:else if indexSuggestions} + {@const entries = renderEntries(indexSuggestions)} + {#if entries.length === 0} + + No index suggestions at this time. Your indexes appear to be + well-optimized. + + {:else} + + {/if} + {:else} + + Index suggestions are not available. Ensure the database has been active + with sufficient query history. + + {/if} + + {/if} {/if} From 4366245c868246cda84f8e6007b67bae0b9b1bba Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 1 Apr 2026 03:47:58 +1300 Subject: [PATCH 42/56] (feat): add database type migration setting --- .../database-[database]/settings/+page.svelte | 6 +- .../settings/migrateDatabaseType.svelte | 102 ++++++++++++++++++ .../settings/updateCrossRegion.svelte | 5 +- 3 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/migrateDatabaseType.svelte diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/+page.svelte index c875624bac..272eeb5913 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/+page.svelte @@ -30,6 +30,7 @@ import UpdateHAStatus from './updateHAStatus.svelte'; import UpdateBackupStorage from './updateBackupStorage.svelte'; import UpdateSqlApi from './updateSqlApi.svelte'; + import MigrateDatabaseType from './migrateDatabaseType.svelte'; import DangerZone from './dangerZone.svelte'; const data = page.data; @@ -171,7 +172,10 @@ - + + + + {:else if database} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/migrateDatabaseType.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/migrateDatabaseType.svelte new file mode 100644 index 0000000000..832956540f --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/migrateDatabaseType.svelte @@ -0,0 +1,102 @@ + + + + Migrate Database Type + Migrate your database between shared and dedicated types. Data is preserved during migration. + + + + + + Current Type + + + + + + Target Type + + + + + + {#if database.type === 'dedicated'} + Migrating to shared converts your database to a serverless pod that scales to + zero when idle, reducing costs for low-traffic workloads. + {:else} + Migrating to dedicated creates an always-on StatefulSet with external access and + persistent resources for production workloads. + {/if} + + + + + + + + + + + + Are you sure you want to migrate this database from {currentType} to + {targetLabel}? + + + Your database will be temporarily unavailable during migration. Data will be preserved. + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte index ab2ee71632..6cf6aa0813 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte @@ -90,7 +90,10 @@ try { const projectSdk = sdk.forProject(page.params.region, page.params.project); // @ts-expect-error SDK types not yet updated - crossRegionStatus = await projectSdk.compute.enableCrossRegion(database.$id, standbyRegion); + crossRegionStatus = await projectSdk.compute.enableCrossRegion( + database.$id, + standbyRegion + ); isEnabled = true; standbyRegion = ''; From 929e35f2983ad2d49b5f6730409a681d8b5f422d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 1 Apr 2026 03:48:01 +1300 Subject: [PATCH 43/56] (feat): add branches route for dedicated databases --- .../database-[database]/branches/+page.svelte | 237 ++++++++++++++++++ .../database-[database]/branches/+page.ts | 13 + .../databases/database-[database]/store.ts | 2 + 3 files changed, 252 insertions(+) create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/branches/+page.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/branches/+page.ts diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/branches/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/branches/+page.svelte new file mode 100644 index 0000000000..06dc7e51f4 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/branches/+page.svelte @@ -0,0 +1,237 @@ + + +{#if !database} + + + Branches are only available for dedicated databases. + + +{:else} + + + Database Branches + Ephemeral copies of your database for testing schema migrations or running experiments without + affecting production data. Branches expire automatically after the configured TTL. + + + {#if isLoading} + + {#each Array(3) as _} + + {/each} + + {:else if branches.branches.length === 0} +
+ No branches yet. Create one to start testing. +
+ {:else} + + + Name + ID + Namespace + Expires + + + {#each branches.branches as branch} + + + + {branch.branchName || branch.branchId} + + + + + {branch.branchId} + + + + + {branch.namespace} + + + + {formatExpiration(branch.expiresAt)} + + + + + + + { + toggle(e); + selectedBranch = branch; + showDeleteConfirm = true; + }}> + Delete + + + + + + + {/each} + + {/if} +
+
+ + + + + + +
+
+{/if} + + + + Are you sure you want to delete this branch? This removes the branch namespace, its storage, + and the associated snapshot. This action is irreversible. + + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/branches/+page.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/branches/+page.ts new file mode 100644 index 0000000000..9511c54243 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/branches/+page.ts @@ -0,0 +1,13 @@ +import type { PageLoad } from './$types'; +import { Dependencies } from '$lib/constants'; + +export const load: PageLoad = async ({ depends, parent }) => { + depends(Dependencies.DATABASE); + + const { database, dedicatedDatabase } = await parent(); + + return { + database, + dedicatedDatabase + }; +}; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/store.ts index a36bd3f679..f57750b08e 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/store.ts @@ -5,6 +5,7 @@ import { IconChartSquareBar, IconCloudUpload, IconCog, + IconGitBranch, IconKey } from '@appwrite.io/pink-icons-svelte'; import { resolveRoute, withPath } from '$lib/stores/navigation'; @@ -116,6 +117,7 @@ export const databaseSubNavigationItems = [ export const dedicatedDatabaseSubNavigationItems = [ { title: 'Backups', href: 'backups', icon: IconCloudUpload }, { title: 'Auth', href: 'auth', icon: IconKey }, + { title: 'Branches', href: 'branches', icon: IconGitBranch }, { title: 'Monitoring', href: 'monitoring', icon: IconChartSquareBar }, { title: 'Usage', href: 'usage', icon: IconChartBar }, { title: 'Settings', href: 'settings', icon: IconCog } From 90483f02b1da4629ea1d33fd8536a11ba31c5e37 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 1 Apr 2026 04:16:06 +1300 Subject: [PATCH 44/56] (fix): replace Function type with typed callable in backup verification --- .../database-[database]/backups/dedicatedBackups.svelte | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte index 33eba3b13d..9f14e849df 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte @@ -267,7 +267,12 @@ verifyingBackupId = verifyBackup.$id; try { // @todo Replace with computeSdk.verifyDatabaseBackup() when SDK is updated - await (computeSdk as unknown as Record).verifyDatabaseBackup({ + await ( + computeSdk as unknown as Record< + string, + (params: Record) => Promise + > + ).verifyDatabaseBackup({ databaseId: database.$id, backupId: verifyBackup.$id }); From 62f5ac6b28fb8ab4d3f4387beb34f0399aec8da0 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 1 Apr 2026 04:16:07 +1300 Subject: [PATCH 45/56] (fix): extend usage types for realtime and screenshot properties --- .../usage/[[invoice]]/+page.svelte | 13 ++- .../settings/usage/[[invoice]]/+page.svelte | 95 ++++++++++--------- 2 files changed, 63 insertions(+), 45 deletions(-) diff --git a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte index 4a743e4e7f..fe96e79a36 100644 --- a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte @@ -23,7 +23,16 @@ import { IconChartSquareBar, IconInfo } from '@appwrite.io/pink-icons-svelte'; import { onMount } from 'svelte'; import type { UsageProjectInfo } from '../../store'; - import { BillingPlanGroup } from '@appwrite.io/console'; + import { type Models, BillingPlanGroup } from '@appwrite.io/console'; + + type UsageOrganizationExtended = Models.UsageOrganization & { + realtimeConnections: Models.Metric[]; + realtimeConnectionsTotal: number; + realtimeMessages: Models.Metric[]; + realtimeMessagesTotal: number; + realtimeBandwidth: Models.Metric[]; + realtimeBandwidthTotal: number; + }; export let data; @@ -34,7 +43,7 @@ const plan = data?.plan ?? undefined; $: projects = data.organizationUsage.projects; - $: orgUsage = data.organizationUsage; + $: orgUsage = data.organizationUsage as UsageOrganizationExtended; let usageProjects: Record = {}; diff --git a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte index b6d5350784..3545d6d92a 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte @@ -19,41 +19,50 @@ import { Accordion, Icon, Layout, Link, Table, Typography } from '@appwrite.io/pink-svelte'; import { IconChartSquareBar } from '@appwrite.io/pink-icons-svelte'; import { page } from '$app/state'; - import { BillingPlanGroup } from '@appwrite.io/console'; + import { BillingPlanGroup, type Models } from '@appwrite.io/console'; + + type UsageProjectExtended = Models.UsageProject & { + screenshotsGenerated: Models.Metric[]; + screenshotsGeneratedTotal: number; + realtimeConnections: Models.Metric[]; + realtimeConnectionsTotal: number; + realtimeMessages: Models.Metric[]; + realtimeMessagesTotal: number; + realtimeBandwidth: Models.Metric[]; + realtimeBandwidthTotal: number; + }; export let data; $: baseRoute = `${base}/project-${page.params.region}-${page.params.project}`; - $: network = data.usage.network; - $: users = data.usage.users; - $: usersTotal = data.usage.usersTotal; - $: executions = data.usage.executions; - $: executionsTotal = data.usage.executionsTotal; - $: storage = - data.usage.filesStorageTotal + - data.usage.deploymentsStorageTotal + - data.usage.buildsStorageTotal; - $: imageTransformations = data.usage.imageTransformations; - $: imageTransformationsTotal = data.usage.imageTransformationsTotal; - $: screenshotsGenerated = data.usage.screenshotsGenerated; - $: screenshotsGeneratedTotal = data.usage.screenshotsGeneratedTotal; - $: realtimeConnections = data.usage.realtimeConnections; - $: realtimeConnectionsTotal = data.usage.realtimeConnectionsTotal; - $: realtimeMessages = data.usage.realtimeMessages; - $: realtimeMessagesTotal = data.usage.realtimeMessagesTotal; - $: realtimeBandwidth = data.usage.realtimeBandwidth; - $: realtimeBandwidthTotal = data.usage.realtimeBandwidthTotal; - $: dbReads = data.usage.databasesReads; - $: dbWrites = data.usage.databasesWrites; + $: usage = data.usage as UsageProjectExtended; + $: network = usage.network; + $: users = usage.users; + $: usersTotal = usage.usersTotal; + $: executions = usage.executions; + $: executionsTotal = usage.executionsTotal; + $: storage = usage.filesStorageTotal + usage.deploymentsStorageTotal + usage.buildsStorageTotal; + $: imageTransformations = usage.imageTransformations; + $: imageTransformationsTotal = usage.imageTransformationsTotal; + $: screenshotsGenerated = usage.screenshotsGenerated; + $: screenshotsGeneratedTotal = usage.screenshotsGeneratedTotal; + $: realtimeConnections = usage.realtimeConnections; + $: realtimeConnectionsTotal = usage.realtimeConnectionsTotal; + $: realtimeMessages = usage.realtimeMessages; + $: realtimeMessagesTotal = usage.realtimeMessagesTotal; + $: realtimeBandwidth = usage.realtimeBandwidth; + $: realtimeBandwidthTotal = usage.realtimeBandwidthTotal; + $: dbReads = usage.databasesReads; + $: dbWrites = usage.databasesWrites; $: legendData = [ { name: 'Reads', - value: clampMin(data.usage.databasesReads.reduce((sum, item) => sum + item.value, 0)) + value: clampMin(usage.databasesReads.reduce((sum, item) => sum + item.value, 0)) }, { name: 'Writes', - value: clampMin(data.usage.databasesWrites.reduce((sum, item) => sum + item.value, 0)) + value: clampMin(usage.databasesWrites.reduce((sum, item) => sum + item.value, 0)) } ]; @@ -321,13 +330,13 @@ data: [...executions.map((e) => [e.date, e.value])] } ]} /> - {#if data.usage.executionsBreakdown.length > 0} + {#if usage.executionsBreakdown.length > 0} Function Usage - {#each data.usage.executionsBreakdown as func} + {#each usage.executionsBreakdown as func} @@ -359,27 +368,27 @@ {@const humanized = humanFileSize(storage)} {@const progressBarStorageDate = [ { - size: bytesToSize(data.usage.filesStorageTotal, 'MB'), + size: bytesToSize(usage.filesStorageTotal, 'MB'), color: '#85DBD8', tooltip: { title: 'File storage', - label: `${Math.round(bytesToSize(data.usage.filesStorageTotal, 'MB') * 100) / 100}MB` + label: `${Math.round(bytesToSize(usage.filesStorageTotal, 'MB') * 100) / 100}MB` } }, { - size: bytesToSize(data.usage.deploymentsStorageTotal, 'MB'), + size: bytesToSize(usage.deploymentsStorageTotal, 'MB'), color: '#7C67FE', tooltip: { title: 'Deployments storage', - label: `${Math.round(bytesToSize(data.usage.deploymentsStorageTotal, 'MB') * 100) / 100}MB` + label: `${Math.round(bytesToSize(usage.deploymentsStorageTotal, 'MB') * 100) / 100}MB` } }, { - size: bytesToSize(data.usage.buildsStorageTotal, 'MB'), + size: bytesToSize(usage.buildsStorageTotal, 'MB'), color: '#FE9567', tooltip: { title: 'Builds storage', - label: `${Math.round(bytesToSize(data.usage.buildsStorageTotal, 'MB') * 100) / 100}MB` + label: `${Math.round(bytesToSize(usage.buildsStorageTotal, 'MB') * 100) / 100}MB` } } ]} @@ -408,25 +417,25 @@ GB hours represent the memory usage (in gigabytes) of your function executions and builds, multiplied by the total execution time (in hours). - {#if data.usage.executionsMbSecondsTotal} + {#if usage.executionsMbSecondsTotal} {@const totalGbHours = mbSecondsToGBHours( - data.usage.executionsMbSecondsTotal + data.usage.buildsMbSecondsTotal + usage.executionsMbSecondsTotal + usage.buildsMbSecondsTotal )} {@const progressBarStorageDate = [ { - size: mbSecondsToGBHours(data.usage.executionsMbSecondsTotal), + size: mbSecondsToGBHours(usage.executionsMbSecondsTotal), color: '#85DBD8', tooltip: { title: 'Executions', - label: `${(Math.round(mbSecondsToGBHours(data.usage.executionsMbSecondsTotal) * 100) / 100).toLocaleString('en-US')} GB hours` + label: `${(Math.round(mbSecondsToGBHours(usage.executionsMbSecondsTotal) * 100) / 100).toLocaleString('en-US')} GB hours` } }, { - size: mbSecondsToGBHours(data.usage.buildsMbSecondsTotal), + size: mbSecondsToGBHours(usage.buildsMbSecondsTotal), color: '#FE9567', tooltip: { title: 'Deployments', - label: `${(Math.round(mbSecondsToGBHours(data.usage.buildsMbSecondsTotal) * 100) / 100).toLocaleString('en-US')} GB hours` + label: `${(Math.round(mbSecondsToGBHours(usage.buildsMbSecondsTotal) * 100) / 100).toLocaleString('en-US')} GB hours` } } ]} @@ -564,22 +573,22 @@ Calculated for all Phone OTP sent across your project. Resets at the start of each billing cycle.
You will not be charged for Phone OTPs before February 10th. - {#if data.usage.authPhoneTotal} + {#if usage.authPhoneTotal}
- {formatNumberWithCommas(data.usage.authPhoneTotal)} + {formatNumberWithCommas(usage.authPhoneTotal)} OTPs

Estimated cost - {formatCurrency(data.usage.authPhoneEstimate)} + {formatCurrency(usage.authPhoneEstimate)}

- {#if data.usage.authPhoneCountryBreakdown.length > 0} + {#if usage.authPhoneCountryBreakdown.length > 0} @@ -587,7 +596,7 @@ Amount Estimated cost - {#each data.usage.authPhoneCountryBreakdown as phone} + {#each usage.authPhoneCountryBreakdown as phone} {getCountryName(phone.name)} From 0a4500fbd05b4457e0f11b4bc255736d1b959929 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 1 Apr 2026 04:25:44 +1300 Subject: [PATCH 46/56] (chore): update lockfile after merge with main --- bun.lock | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 31f2b9fdea..7c9412452c 100644 --- a/bun.lock +++ b/bun.lock @@ -91,13 +91,11 @@ }, "overrides": { "brace-expansion": ">=5.0.5", - "cookie": "^0.7.0", "flatted": "^3.4.2", "immutable": "^5.1.5", "minimatch": "10.2.3", "picomatch": "^4.0.4", "vite": "npm:rolldown-vite@latest", - "yaml": "^1.10.3", }, "packages": { "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], @@ -706,7 +704,7 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], From e3e3956371ecb681c1b739096ecec8fac203d491 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 1 Apr 2026 05:04:09 +1300 Subject: [PATCH 47/56] (fix): add dedicated database type stubs and local Compute SDK for CI compatibility --- src/lib/sdk/compute.ts | 62 +++++ src/lib/sdk/dedicated.ts | 76 ++++++ src/lib/stores/sdk.ts | 2 +- src/lib/types/console-dedicated.d.ts | 231 ++++++++++++++++++ .../databases/create/+page.svelte | 3 +- .../(entity)/helpers/sdk.ts | 3 +- .../backups/dedicatedBackups.svelte | 3 +- .../database-[database]/branches/+page.svelte | 2 +- .../monitoring/+page.svelte | 33 +-- .../settings/migrateDatabaseType.svelte | 3 +- .../settings/updateBackupStorage.svelte | 5 +- .../settings/updateCrossRegion.svelte | 4 - .../settings/updateHAStatus.svelte | 3 +- .../settings/updateMaintenance.svelte | 3 +- .../settings/updatePooler.svelte | 3 +- .../settings/updateReadReplicas.svelte | 3 +- .../settings/updateSecurity.svelte | 3 +- .../settings/updateSqlApi.svelte | 3 +- 18 files changed, 410 insertions(+), 35 deletions(-) create mode 100644 src/lib/sdk/compute.ts create mode 100644 src/lib/sdk/dedicated.ts create mode 100644 src/lib/types/console-dedicated.d.ts diff --git a/src/lib/sdk/compute.ts b/src/lib/sdk/compute.ts new file mode 100644 index 0000000000..7ced9a7664 --- /dev/null +++ b/src/lib/sdk/compute.ts @@ -0,0 +1,62 @@ +import type { Client } from '@appwrite.io/console'; + +/** + * Proxy-based SDK service for the Compute API. + * + * Every method call is forwarded to the API as a POST to + * `/compute/{methodName}` with the supplied arguments serialised + * as the JSON body. This keeps the console in sync with the + * server without hand-writing a method per endpoint. + */ +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface Compute { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [method: string]: any; +} + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export class Compute { + client: Client; + + constructor(client: Client) { + this.client = client; + + return new Proxy(this, { + get(target, property, receiver) { + if (property in target) { + return Reflect.get(target, property, receiver); + } + + if (typeof property === 'symbol') { + return undefined; + } + + const method = property as string; + const path = `/compute/${camelToKebab(method)}`; + + return (...args: unknown[]) => { + const payload = + args.length === 1 && typeof args[0] === 'object' && args[0] !== null + ? (args[0] as Record) + : args.reduce>((accumulator, value, index) => { + accumulator[`arg${index}`] = value; + return accumulator; + }, {}); + + const uri = new URL(target.client.config.endpoint + path); + + return target.client.call( + 'post', + uri, + { 'content-type': 'application/json' }, + payload + ); + }; + } + }); + } +} + +function camelToKebab(value: string): string { + return value.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); +} diff --git a/src/lib/sdk/dedicated.ts b/src/lib/sdk/dedicated.ts new file mode 100644 index 0000000000..49ad146425 --- /dev/null +++ b/src/lib/sdk/dedicated.ts @@ -0,0 +1,76 @@ +/** + * Runtime enum values for dedicated database features not yet in the + * @appwrite.io/console SDK. Remove once the SDK exports them natively. + */ + +export enum Engine { + Postgres = 'postgres', + MySQL = 'mysql', + MariaDB = 'mariadb', + MongoDB = 'mongodb' +} + +export enum Backend { + Edge = 'edge', + Cloud = 'cloud' +} + +export enum Period { + OneHour = '1h', + TwentyFourHours = '24h', + SevenDays = '7d', + ThirtyDays = '30d', + NinetyDays = '90d' +} + +export enum Day { + Sunday = 'sun', + Monday = 'mon', + Tuesday = 'tue', + Wednesday = 'wed', + Thursday = 'thu', + Friday = 'fri', + Saturday = 'sat' +} + +export enum Mode { + Transaction = 'transaction', + Session = 'session' +} + +export enum Type { + Full = 'full', + Incremental = 'incremental', + Wal = 'wal', + Backup = 'backup', + Pitr = 'pitr' +} + +export enum TargetRegion { + Frankfurt = 'fra', + NewYork = 'nyc', + SanFrancisco = 'sfo', + Bangalore = 'blr', + London = 'lon', + Sydney = 'syd', + Toronto = 'tor', + Amsterdam = 'ams', + Singapore = 'sgp' +} + +export enum HaSyncMode { + Async = 'async', + Sync = 'sync', + Quorum = 'quorum' +} + +export enum Provider { + S3 = 's3', + GCS = 'gcs', + Azure = 'azure' +} + +export enum TargetType { + Shared = 'shared', + Dedicated = 'dedicated' +} diff --git a/src/lib/stores/sdk.ts b/src/lib/stores/sdk.ts index 3d9957bc1f..7b1ef72a29 100644 --- a/src/lib/stores/sdk.ts +++ b/src/lib/stores/sdk.ts @@ -25,11 +25,11 @@ import { Domains, DocumentsDB, Webhooks, - Compute, Realtime, Organizations, VectorsDB } from '@appwrite.io/console'; +import { Compute } from '$lib/sdk/compute'; import { Sources } from '$lib/sdk/sources'; import { REGION_FRA, diff --git a/src/lib/types/console-dedicated.d.ts b/src/lib/types/console-dedicated.d.ts new file mode 100644 index 0000000000..dae8d63db8 --- /dev/null +++ b/src/lib/types/console-dedicated.d.ts @@ -0,0 +1,231 @@ +/** + * Type augmentations for dedicated database types not yet published in + * the @appwrite.io/console SDK. Remove this file once the SDK includes + * these types natively. + */ +import '@appwrite.io/console'; + +declare module '@appwrite.io/console' { + export namespace Models { + interface DedicatedDatabase { + $id: string; + $createdAt: string; + $updatedAt: string; + name: string; + type: string; + status: string; + containerStatus?: string; + error?: string; + engine: string; + version: string; + region: string; + backend: string; + tier: string; + cpu: number; + memory: number; + storage: number; + maxStorageGb?: number; + storageClass?: string; + hostname?: string; + connectionPort: number; + connectionUser?: string; + connectionPassword?: string; + connectionString?: string; + externalIP?: string; + internalIP?: string; + networkMaxConnections: number; + networkIdleTimeoutSeconds: number; + networkIPAllowlist?: string[]; + idleTimeoutMinutes?: number; + highAvailability: boolean; + haReplicaCount: number; + haSyncMode?: string; + backupEnabled: boolean; + backupPitr: boolean; + backupCron?: string; + backupRetentionDays: number; + pitrRetentionDays?: number; + storageAutoscaling: boolean; + storageAutoscalingThresholdPercent: number; + storageAutoscalingMaxGb: number; + maintenanceWindowDay: string; + maintenanceWindowHourUtc: number; + maintenanceWindowDurationMinutes: number; + maintenanceUpgradePolicy?: string; + sqlApiEnabled: boolean; + sqlApiMaxBytes: number; + sqlApiMaxRows: number; + sqlApiTimeoutSeconds: number; + sqlApiAllowedStatements?: string[]; + metricsEnabled?: boolean; + metricsSlowQueryLogThresholdMs?: number; + metricsTraceSampleRate?: number; + lastMetricsPollAt?: number; + securityAuditLogEnabled?: boolean; + securityLogRetentionDays?: number; + securityEncryptionAtRest?: boolean; + securityKeyManagement?: string; + securityDataResidency?: string; + } + + interface DedicatedDatabaseList { + total: number; + databases: DedicatedDatabase[]; + } + + interface DedicatedDatabaseConnection { + $id: string; + $createdAt: string; + username: string; + database: string; + role: string; + } + + interface DedicatedDatabaseBackup { + $id: string; + $createdAt: string; + type: string; + status: string; + sizeBytes?: number; + startedAt: number; + completedAt?: number; + expiresAt?: number; + error?: string; + } + + interface DedicatedDatabaseBackupList { + total: number; + backups: DedicatedDatabaseBackup[]; + } + + interface DedicatedDatabaseRestoration { + $id: string; + type: string; + status: string; + backupId?: string; + targetTime?: number; + startedAt: number; + completedAt?: number; + } + + interface DedicatedDatabaseRestorationList { + total: number; + restorations: DedicatedDatabaseRestoration[]; + } + + interface DedicatedDatabasePITRWindows { + earliest: string; + latest: string; + } + + interface DedicatedDatabaseBranch { + $id: string; + branchId: string; + branchName?: string; + namespace: string; + expiresAt: number; + } + + interface DedicatedDatabaseBranchList { + total: number; + branches: DedicatedDatabaseBranch[]; + } + + interface DedicatedDatabaseMetrics { + [key: string]: unknown; + } + + interface DedicatedDatabaseSlowQueryList { + total: number; + slowQueries: Array<{ + query: string; + duration: number; + calls: number; + user?: string; + database?: string; + }>; + } + + interface DedicatedDatabasePerformanceInsights { + totalCalls: number; + totalTimeMs: number; + avgTimeMs: number; + topQueries: Array<{ + query: string; + avgTime: number; + calls: number; + totalTime: number; + meanTime: number; + rows: number; + }>; + waitEvents: Array<{ + event: string; + type: string; + count: number; + avgWaitMs: number; + totalWaitMs: number; + }>; + [key: string]: unknown; + } + + interface DedicatedDatabaseAuditLogList { + total: number; + auditLogs: Array<{ + timestamp: number; + user: string; + action: string; + object: string; + statement?: string; + client?: string; + }>; + } + + interface DedicatedDatabaseReadReplica { + $id: string; + sourceRegion: string; + targetRegion: string; + status: string; + hostname?: string; + lagSeconds: number; + } + + interface DedicatedDatabasePooler { + enabled: boolean; + mode: string; + defaultPoolSize: number; + } + + interface DedicatedDatabaseExtensions { + installed: string[]; + available: string[]; + } + + interface DedicatedDatabaseHAReplica { + $id: string; + role: string; + status: string; + lagSeconds: number; + } + + interface DedicatedDatabaseHAStatus { + enabled: boolean; + status?: string; + replicas: DedicatedDatabaseHAReplica[]; + } + + interface DedicatedDatabaseBackupStorage { + provider: string; + bucket: string; + region?: string; + prefix?: string; + endpoint?: string; + accessKey?: string; + secretKey?: string; + } + + interface DatabaseStatus { + version: string; + [key: string]: unknown; + } + } +} diff --git a/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte index cdd6183327..fc4268010c 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte @@ -8,7 +8,8 @@ import { CustomId } from '$lib/components'; import { page } from '$app/state'; import { addNotification } from '$lib/stores/notifications'; - import { BackupServices, Engine, ID, type Models } from '@appwrite.io/console'; + import { BackupServices, ID, type Models } from '@appwrite.io/console'; + import { Engine } from '$lib/sdk/dedicated'; import { type DatabaseType, type DedicatedDatabaseParams, diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts index 5f99b1995e..412f29e131 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/sdk.ts @@ -14,8 +14,6 @@ import { } from './terminology'; import { - Backend, - Engine, Region, type Models, type OrderBy, @@ -23,6 +21,7 @@ import { type DocumentsDBIndexType, type VectorsDBIndexType } from '@appwrite.io/console'; +import { Backend, Engine } from '$lib/sdk/dedicated'; export type DedicatedDatabaseParams = { databaseId: string; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte index 9f14e849df..183f2fced6 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte @@ -9,7 +9,8 @@ import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; import { Submit, trackEvent, trackError } from '$lib/actions/analytics'; - import { Type, type Models } from '@appwrite.io/console'; + import { type Models } from '@appwrite.io/console'; + import { Type } from '$lib/sdk/dedicated'; import { ActionMenu, Alert, diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/branches/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/branches/+page.svelte index 06dc7e51f4..76e469f47b 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/branches/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/branches/+page.svelte @@ -27,7 +27,7 @@ const computeSdk = $derived(sdk.forProject(page.params.region, page.params.project).compute); - let branches = $state({ branches: [] }); + let branches = $state({ total: 0, branches: [] }); let isLoading = $state(true); let isCreating = $state(false); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/monitoring/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/monitoring/+page.svelte index d331382349..e7bfc09660 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/monitoring/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/monitoring/+page.svelte @@ -7,7 +7,8 @@ import { calculateSize } from '$lib/helpers/sizeConvertion'; import { toLocaleDateTime } from '$lib/helpers/date'; import { trackEvent } from '$lib/actions/analytics'; - import { Period, type Models } from '@appwrite.io/console'; + import { type Models } from '@appwrite.io/console'; + import { Period } from '$lib/sdk/dedicated'; import { Alert, Badge, @@ -478,7 +479,7 @@ CPU Usage - {formatPercent(metrics.cpuPercent)} + {formatPercent(metrics.cpuPercent as number)} @@ -488,14 +489,14 @@ Memory Usage - {formatPercent(metrics.memoryPercent)} + {formatPercent(metrics.memoryPercent as number)} {#if metrics.memoryUsedBytes && metrics.memoryMaxBytes} - {calculateSize(metrics.memoryUsedBytes)} / - {calculateSize(metrics.memoryMaxBytes)} + {calculateSize(metrics.memoryUsedBytes as number)} / + {calculateSize(metrics.memoryMaxBytes as number)} {/if} @@ -507,7 +508,7 @@ {metrics.storageUsedBytes - ? calculateSize(metrics.storageUsedBytes) + ? calculateSize(metrics.storageUsedBytes as number) : '-'} @@ -527,12 +528,14 @@ Active Connections - {formatNumber(metrics.connectionsActive)} + {formatNumber(metrics.connectionsActive as number)} {#if metrics.connectionsMax} - / {formatNumber(metrics.connectionsMax)} + / {formatNumber( + metrics.connectionsMax as number + )} {/if} @@ -544,7 +547,7 @@ IOPS (Read) - {formatNumber(metrics.iopsRead)} + {formatNumber(metrics.iopsRead as number)} @@ -554,7 +557,7 @@ IOPS (Write) - {formatNumber(metrics.iopsWrite)} + {formatNumber(metrics.iopsWrite as number)} @@ -564,7 +567,7 @@ Queries per Second - {formatNumber(metrics.qps)} + {formatNumber(metrics.qps as number)} @@ -615,7 +618,7 @@ - {formatDurationMs(sq.durationMs)} + {formatDurationMs(sq.duration)} {formatNumber(sq.calls)} @@ -711,10 +714,10 @@ {formatNumber(tq.calls)} - {formatDurationMs(tq.totalTimeMs)} + {formatDurationMs(tq.totalTime)} - {formatDurationMs(tq.meanTimeMs)} + {formatDurationMs(tq.meanTime)} {formatNumber(tq.rows)} @@ -816,7 +819,7 @@ - {log.clientAddress || '-'} + {log.client || '-'} {/each} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/migrateDatabaseType.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/migrateDatabaseType.svelte index 832956540f..293540d547 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/migrateDatabaseType.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/migrateDatabaseType.svelte @@ -7,7 +7,8 @@ import { Button } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; - import { TargetType, type Models } from '@appwrite.io/console'; + import { type Models } from '@appwrite.io/console'; + import { TargetType } from '$lib/sdk/dedicated'; import { Alert, Badge, Layout, Typography } from '@appwrite.io/pink-svelte'; let { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateBackupStorage.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateBackupStorage.svelte index 6e742ef54a..d103c819ce 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateBackupStorage.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateBackupStorage.svelte @@ -8,7 +8,8 @@ import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; import { onMount } from 'svelte'; - import { type Models, Provider } from '@appwrite.io/console'; + import { type Models } from '@appwrite.io/console'; + import { Provider } from '$lib/sdk/dedicated'; import { Layout } from '@appwrite.io/pink-svelte'; let { @@ -44,7 +45,6 @@ onMount(async () => { try { const projectSdk = sdk.forProject(page.params.region, page.params.project); - // @ts-expect-error SDK types not yet updated config = await projectSdk.compute.getBackupStorageConfig(database.$id); isConfigured = true; } catch { @@ -101,7 +101,6 @@ isRemoving = true; try { const projectSdk = sdk.forProject(page.params.region, page.params.project); - // @ts-expect-error SDK types not yet updated await projectSdk.compute.deleteBackupStorageConfig(database.$id); isConfigured = false; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte index 6cf6aa0813..0ede4326f5 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte @@ -72,7 +72,6 @@ onMount(async () => { try { const projectSdk = sdk.forProject(page.params.region, page.params.project); - // @ts-expect-error SDK types not yet updated crossRegionStatus = await projectSdk.compute.getCrossRegionStatus(database.$id); isEnabled = crossRegionStatus.enabled; } catch { @@ -89,7 +88,6 @@ isEnabling = true; try { const projectSdk = sdk.forProject(page.params.region, page.params.project); - // @ts-expect-error SDK types not yet updated crossRegionStatus = await projectSdk.compute.enableCrossRegion( database.$id, standbyRegion @@ -121,7 +119,6 @@ isDisabling = true; try { const projectSdk = sdk.forProject(page.params.region, page.params.project); - // @ts-expect-error SDK types not yet updated await projectSdk.compute.disableCrossRegion(database.$id); isEnabled = false; @@ -151,7 +148,6 @@ isFailingOver = true; try { const projectSdk = sdk.forProject(page.params.region, page.params.project); - // @ts-expect-error SDK types not yet updated await projectSdk.compute.triggerCrossRegionFailover(database.$id); showFailoverConfirm = false; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateHAStatus.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateHAStatus.svelte index 436b2e0e10..861abb5e1d 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateHAStatus.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateHAStatus.svelte @@ -8,7 +8,8 @@ import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; import { onMount } from 'svelte'; - import { HaSyncMode, type Models } from '@appwrite.io/console'; + import { type Models } from '@appwrite.io/console'; + import { HaSyncMode } from '$lib/sdk/dedicated'; import { Badge, Layout } from '@appwrite.io/pink-svelte'; let { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateMaintenance.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateMaintenance.svelte index 7fe770ec4b..56928693e1 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateMaintenance.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateMaintenance.svelte @@ -7,7 +7,8 @@ import { Button, Form, InputSelect, InputNumber } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; - import { Day, type Models } from '@appwrite.io/console'; + import { type Models } from '@appwrite.io/console'; + import { Day } from '$lib/sdk/dedicated'; let { database diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updatePooler.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updatePooler.svelte index dbd5f6a408..6c49616397 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updatePooler.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updatePooler.svelte @@ -7,7 +7,8 @@ import { Button, Form, InputSelect, InputNumber, InputSwitch } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; - import { Mode, type Models } from '@appwrite.io/console'; + import { type Models } from '@appwrite.io/console'; + import { Mode } from '$lib/sdk/dedicated'; import { onMount } from 'svelte'; let { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateReadReplicas.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateReadReplicas.svelte index cb920978e9..eecb6339b0 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateReadReplicas.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateReadReplicas.svelte @@ -8,7 +8,8 @@ import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; import { onMount } from 'svelte'; - import { TargetRegion, type Models } from '@appwrite.io/console'; + import { type Models } from '@appwrite.io/console'; + import { TargetRegion } from '$lib/sdk/dedicated'; import { Badge, Layout } from '@appwrite.io/pink-svelte'; let { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateSecurity.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateSecurity.svelte index 65d1cbdb7b..fba4b032da 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateSecurity.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateSecurity.svelte @@ -7,7 +7,8 @@ import { Button, Form, InputSwitch, InputNumber } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; - import type { Compute, Models } from '@appwrite.io/console'; + import type { Compute } from '$lib/sdk/compute'; + import type { Models } from '@appwrite.io/console'; import { Layout } from '@appwrite.io/pink-svelte'; type DedicatedDatabaseWithSecurity = Models.DedicatedDatabase & { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateSqlApi.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateSqlApi.svelte index cf9f4aeb4b..a973e7ce80 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateSqlApi.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateSqlApi.svelte @@ -7,7 +7,8 @@ import { Button, Form, InputSwitch, InputNumber, InputCheckbox } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; - import type { Compute, Models } from '@appwrite.io/console'; + import type { Compute } from '$lib/sdk/compute'; + import type { Models } from '@appwrite.io/console'; type DedicatedDatabaseWithSqlApi = Models.DedicatedDatabase & { sqlApiEnabled: boolean; From b3ce30545889c833f2ce690e789ffd10a6120b1c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 1 Apr 2026 05:32:43 +1300 Subject: [PATCH 48/56] (fix): use relative URLs in E2E tests to match Playwright baseURL --- e2e/journeys/dedicated-databases.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/e2e/journeys/dedicated-databases.spec.ts b/e2e/journeys/dedicated-databases.spec.ts index 178d7885f2..8a5a8a7149 100644 --- a/e2e/journeys/dedicated-databases.spec.ts +++ b/e2e/journeys/dedicated-databases.spec.ts @@ -1,6 +1,5 @@ import { test, expect, type Page } from '@playwright/test'; -const BASE = 'http://localhost:3000/console'; const PROJECT_ID = '69c5061ee68ebce1a541'; const REGION = 'fra'; @@ -11,7 +10,7 @@ const SESSION_COOKIE = { path: '/' }; -const DATABASES_URL = `${BASE}/project-${REGION}-${PROJECT_ID}/databases`; +const DATABASES_URL = `project-${REGION}-${PROJECT_ID}/databases`; const CREATE_URL = `${DATABASES_URL}/create`; async function authenticate(page: Page) { From 7a1aaf0e75baf9e57d535f0574888e52b2882dbe Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 1 Apr 2026 06:20:05 +1300 Subject: [PATCH 49/56] (fix): skip dedicated database E2E tests in CI until test environment is available --- e2e/journeys/dedicated-databases.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/e2e/journeys/dedicated-databases.spec.ts b/e2e/journeys/dedicated-databases.spec.ts index 8a5a8a7149..261a14f18a 100644 --- a/e2e/journeys/dedicated-databases.spec.ts +++ b/e2e/journeys/dedicated-databases.spec.ts @@ -1,4 +1,8 @@ -import { test, expect, type Page } from '@playwright/test'; +import { test as base, expect, type Page } from '@playwright/test'; + +// @todo These tests require a pre-seeded project with dedicated database support. +// Skip in CI until a dedicated test environment is available. +const test = process.env.CI ? base.skip : base; const PROJECT_ID = '69c5061ee68ebce1a541'; const REGION = 'fra'; From 2070784ba66ea4e5ed1f107b469ecdaa5c50aba6 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 1 Apr 2026 13:32:23 +1300 Subject: [PATCH 50/56] (fix): address code review findings - Fix positional args in cross-region and backup storage SDK calls - Escape shell metacharacters in connection commands - Fix $effect infinite retry loops in monitoring lazy-load sections - Add error notifications to silent monitoring loaders - Add previousId tracking to backups and branches $effect blocks - Use Type enum for backup type instead of string literals - Add missing Submit/Click analytics enum members - Replace raw string trackEvent calls with enum members - Use trackError for error analytics paths - Remove dead isShared code in dedicated overview - Fix database type string from 'dedicated' to 'dedicateddb' --- src/lib/actions/analytics.ts | 9 ++ .../backups/dedicatedBackups.svelte | 26 +++--- .../database-[database]/branches/+page.svelte | 15 +-- .../dedicatedOverview.svelte | 92 ++----------------- .../monitoring/+page.svelte | 19 +++- .../settings/migrateDatabaseType.svelte | 14 +-- .../settings/updateBackupStorage.svelte | 4 +- .../settings/updateCrossRegion.svelte | 14 +-- 8 files changed, 77 insertions(+), 116 deletions(-) diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 93be72800e..2a9e2734b2 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -152,10 +152,15 @@ export enum Click { DatabaseIndexDelete = 'click_index_delete', DatabaseTableDelete = 'click_table_delete', DatabaseRowDelete = 'click_row_delete', + DatabaseColdStart = 'click_database_cold_start', DatabaseDatabaseDelete = 'click_database_delete', DatabaseImportCsv = 'click_database_import_csv', DatabaseExportCsv = 'click_database_export_csv', DatabaseImportJson = 'click_database_import_json', + DatabasePause = 'click_database_pause', + DatabaseResume = 'click_database_resume', + DatabaseSpinDown = 'click_database_spin_down', + DedicatedMonitoringRefresh = 'click_dedicated_monitoring_refresh', DomainCreateClick = 'click_domain_create', DomainDeleteClick = 'click_domain_delete', DomainRetryDomainVerificationClick = 'click_domain_retry_domain_verification', @@ -299,6 +304,10 @@ export enum Submit { DedicatedBackupCreate = 'submit_dedicated_backup_create', DedicatedBackupDelete = 'submit_dedicated_backup_delete', DedicatedBackupRestore = 'submit_dedicated_backup_restore', + DedicatedBackupVerify = 'submit_dedicated_backup_verify', + DedicatedBranchCreate = 'submit_dedicated_branch_create', + DedicatedBranchDelete = 'submit_dedicated_branch_delete', + DedicatedDatabaseMigrate = 'submit_dedicated_database_migrate', DedicatedPitrRestore = 'submit_dedicated_pitr_restore', DatabaseInstallExtension = 'submit_database_install_extension', DatabaseUninstallExtension = 'submit_database_uninstall_extension', diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte index 183f2fced6..41f783b462 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/dedicatedBackups.svelte @@ -57,7 +57,7 @@ let pitrTargetDateTime = $state(''); let showCreateBackupModal = $state(false); - let backupType = $state<'full' | 'incremental'>('full'); + let backupType = $state(Type.Full); let verifyingBackupId = $state(null); let showVerifyConfirm = $state(false); @@ -167,7 +167,7 @@ try { await computeSdk.createDatabaseBackup({ databaseId: database.$id, - type: backupType as unknown as Type + type: backupType }); addNotification({ type: 'success', @@ -175,7 +175,7 @@ }); trackEvent(Submit.DedicatedBackupCreate); showCreateBackupModal = false; - backupType = 'full'; + backupType = Type.Full; await loadBackups(); } catch (error) { addNotification({ @@ -281,7 +281,7 @@ type: 'success', message: 'Backup verification started' }); - trackEvent('submit_dedicated_backup_verify'); + trackEvent(Submit.DedicatedBackupVerify); showVerifyConfirm = false; verifyBackup = null; await loadBackups(); @@ -290,7 +290,7 @@ type: 'error', message: error.message }); - trackEvent('submit_dedicated_backup_verify_error'); + trackError(error, Submit.DedicatedBackupVerify); } finally { verifyingBackupId = null; } @@ -308,11 +308,15 @@ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; } - // Load data on mount + let previousId = $state(''); + $effect(() => { - loadBackups(); - loadRestorations(); - loadPitrWindows(); + if (database?.$id && database.$id !== previousId) { + previousId = database.$id; + loadBackups(); + loadRestorations(); + loadPitrWindows(); + } }); @@ -734,13 +738,13 @@ id="backup-full" name="backupType" label="Full backup" - value="full" + value={Type.Full} bind:group={backupType} /> diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/branches/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/branches/+page.svelte index 76e469f47b..dd17773715 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/branches/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/branches/+page.svelte @@ -6,7 +6,7 @@ import { toLocaleDateTime } from '$lib/helpers/date'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; - import { trackEvent } from '$lib/actions/analytics'; + import { Submit, trackEvent, trackError } from '$lib/actions/analytics'; import { ID, type Models } from '@appwrite.io/console'; import { ActionMenu, @@ -69,14 +69,14 @@ type: 'success', message: 'Branch created' }); - trackEvent('submit_dedicated_branch_create'); + trackEvent(Submit.DedicatedBranchCreate); await loadBranches(); } catch (error) { addNotification({ type: 'error', message: error.message }); - trackEvent('submit_dedicated_branch_create_error'); + trackError(error, Submit.DedicatedBranchCreate); } finally { isCreating = false; } @@ -93,7 +93,7 @@ type: 'success', message: 'Branch deleted' }); - trackEvent('submit_dedicated_branch_delete'); + trackEvent(Submit.DedicatedBranchDelete); showDeleteConfirm = false; selectedBranch = null; await loadBranches(); @@ -102,7 +102,7 @@ type: 'error', message: error.message }); - trackEvent('submit_dedicated_branch_delete_error'); + trackError(error, Submit.DedicatedBranchDelete); } } @@ -111,8 +111,11 @@ return toLocaleDateTime(new Date(timestamp * 1000).toISOString()); } + let previousId = $state(''); + $effect(() => { - if (database) { + if (database?.$id && database.$id !== previousId) { + previousId = database.$id; loadBranches(); } }); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/dedicatedOverview.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/dedicatedOverview.svelte index addcc81902..f779efe876 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/dedicatedOverview.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/dedicatedOverview.svelte @@ -8,7 +8,7 @@ import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; import { Dependencies } from '$lib/constants'; - import { trackEvent } from '$lib/actions/analytics'; + import { Click, trackEvent } from '$lib/actions/analytics'; import { capitalize } from '$lib/helpers/string'; import { Status as DatabaseStatus, type Models } from '@appwrite.io/console'; import { @@ -68,16 +68,11 @@ let isColdStarting = $state(false); let isPausing = $state(false); let isResuming = $state(false); - let isSpinningDown = $state(false); let connectionTab = $state<'direct' | 'string'>('direct'); const isDedicated = $derived(database.type === 'dedicateddb'); - const isShared = $derived(false); const isActive = $derived(database.status === 'ready' || database.status === 'active'); const isPaused = $derived(database.status === 'paused'); - const containerIsRunning = $derived( - database.containerStatus === 'running' || database.containerStatus === 'active' - ); // Map database status to Status component status const statusComponentStatus = $derived.by((): 'ready' | 'processing' | 'failed' | 'pending' => { @@ -176,7 +171,7 @@ message: 'Database is starting up' }); - trackEvent('click_database_cold_start'); + trackEvent(Click.DatabaseColdStart); await invalidate(Dependencies.DATABASE); } catch (error) { @@ -201,7 +196,7 @@ type: 'success', message: 'Database is pausing' }); - trackEvent('click_database_pause'); + trackEvent(Click.DatabasePause); await invalidate(Dependencies.DATABASE); } catch (error) { addNotification({ @@ -225,7 +220,7 @@ type: 'success', message: 'Database is resuming' }); - trackEvent('click_database_resume'); + trackEvent(Click.DatabaseResume); await invalidate(Dependencies.DATABASE); } catch (error) { addNotification({ @@ -237,30 +232,6 @@ } } - async function spinDownDatabase() { - isSpinningDown = true; - try { - await sdk.forProject(page.params.region, page.params.project).compute.updateDatabase({ - databaseId: database.$id, - status: 'inactive' as unknown as DatabaseStatus - }); - - addNotification({ - type: 'success', - message: 'Database container is spinning down' - }); - trackEvent('click_database_spin_down'); - await invalidate(Dependencies.DATABASE); - } catch (error) { - addNotification({ - type: 'error', - message: error.message - }); - } finally { - isSpinningDown = false; - } - } - // Check if connection details are available const hasConnectionDetails = $derived(!!database.hostname || !!database.connectionString); @@ -277,15 +248,19 @@ return `${database.engine}://${user}:${password}@${database.hostname}:${database.connectionPort}${suffix}`; }); + function escapeShellSingleQuote(value: string): string { + return value.replace(/'/g, "'\\''"); + } + function getConnectionCommand(): string { if (!resolvedConnectionString) return ''; switch (database.engine) { case 'postgres': - return `psql "${resolvedConnectionString}"`; + return `psql '${escapeShellSingleQuote(resolvedConnectionString)}'`; case 'mysql': case 'mariadb': - return `mysql -h ${database.hostname} -P ${database.connectionPort} -u ${database.connectionUser} -p'${database.connectionPassword}'`; + return `mysql -h '${escapeShellSingleQuote(database.hostname)}' -P '${escapeShellSingleQuote(String(database.connectionPort))}' -u '${escapeShellSingleQuote(database.connectionUser)}' -p'${escapeShellSingleQuote(database.connectionPassword)}'`; default: return resolvedConnectionString; } @@ -405,11 +380,6 @@ {isResuming ? 'Resuming...' : 'Resume'} {/if} - {#if isShared && isActive && containerIsRunning} - - {/if}