From cc8fe2814b2a82c0e2a266afdbf6150db88c8bec Mon Sep 17 00:00:00 2001 From: Darshan Date: Mon, 13 Oct 2025 14:40:23 +0530 Subject: [PATCH 01/60] feat: documents db [wip]. --- .../databases/(assets)/dark/mongo-db.svg | 27 ++ .../databases/(assets)/documents-db.svg | 103 ++++++ .../databases/(assets)/mongo-db.svg | 27 ++ .../databases/(assets)/tables-db.svg | 185 +++++++++++ .../databases/+page.svelte | 69 ++-- .../databases/create/+page.svelte | 296 ++++++++++++++++++ .../(entity)/helpers/init.ts | 2 +- .../(entity)/helpers/sdk.ts | 26 +- .../(suggestions)/input.svelte | 32 +- .../databases/database-[database]/+layout.ts | 4 +- .../database-[database]/+page.svelte | 17 +- .../database-[database]/breadcrumbs.svelte | 56 ++-- .../database-[database]/delete.svelte | 6 +- .../database-[database]/settings/+page.svelte | 6 +- .../table-[table]/rows/editRelated.svelte | 4 +- .../databases/empty.svelte | 129 ++++++++ .../databases/grid.svelte | 55 +++- .../databases/store.ts | 33 +- .../databases/table.svelte | 4 +- 19 files changed, 978 insertions(+), 103 deletions(-) create mode 100644 src/routes/(console)/project-[region]-[project]/databases/(assets)/dark/mongo-db.svg create mode 100644 src/routes/(console)/project-[region]-[project]/databases/(assets)/documents-db.svg create mode 100644 src/routes/(console)/project-[region]-[project]/databases/(assets)/mongo-db.svg create mode 100644 src/routes/(console)/project-[region]-[project]/databases/(assets)/tables-db.svg create mode 100644 src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/empty.svelte diff --git a/src/routes/(console)/project-[region]-[project]/databases/(assets)/dark/mongo-db.svg b/src/routes/(console)/project-[region]-[project]/databases/(assets)/dark/mongo-db.svg new file mode 100644 index 0000000000..98239f7288 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/(assets)/dark/mongo-db.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/(assets)/documents-db.svg b/src/routes/(console)/project-[region]-[project]/databases/(assets)/documents-db.svg new file mode 100644 index 0000000000..0acf91caea --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/(assets)/documents-db.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/(assets)/mongo-db.svg b/src/routes/(console)/project-[region]-[project]/databases/(assets)/mongo-db.svg new file mode 100644 index 0000000000..5f4f09c805 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/(assets)/mongo-db.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/routes/(console)/project-[region]-[project]/databases/(assets)/tables-db.svg b/src/routes/(console)/project-[region]-[project]/databases/(assets)/tables-db.svg new file mode 100644 index 0000000000..59c64a58e6 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/(assets)/tables-db.svg @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/routes/(console)/project-[region]-[project]/databases/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/+page.svelte index 6044cf62f6..a81fd1876f 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/+page.svelte @@ -1,35 +1,29 @@ - - {#if $canWriteDatabases} - - {/if} - - {#if data.databases.total} + {@render containerHeader()} + {#if data.view === 'grid'} - + {:else} {/if} @@ -84,13 +67,33 @@ secondary>Clear Search {:else} - (showCreate = true)} /> + { + await goto( + withPath( + resolveRoute( + '/(console)/project-[region]-[project]/databases/create', + page.params + ), + `?type=${type}` + ) + ); + }} /> {/if} - +{#snippet containerHeader()} + + {#if $canWriteDatabases} + + {/if} + +{/snippet} diff --git a/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte new file mode 100644 index 0000000000..76b7fa1e04 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/create/+page.svelte @@ -0,0 +1,296 @@ + + + +
+ +
+ + + + {#if !showCustomId} +
+ (showCustomId = true)}> + + Database ID + +
+ {/if} + + +
+
+ + {#if typeFromParams === null} +
+ {@render selectDatabaseType()} +
+ {/if} + +
+ {#if isCloud} + {@render cloudBackupOptions()} + {:else} + {@render selfHostedBackupOptions()} + {/if} +
+
+ + + + + + +
+ +{#snippet cloudBackupOptions()} + {#if $organization?.billingPlan === BillingPlan.FREE} + + {#each Array.from({ length: 3 }) as _} + + One backup every 24 hours, retained for 30 days + + {/each} + + {:else} + + Upgrade your plan to ensure your data stays safe and backed up. + + + + + {/if} +{/snippet} + +{#snippet selfHostedBackupOptions()} + + {@const length = $isTabletViewport ? 2 : 3} + {@const gridsColumn = $isSmallViewport ? 2 : 3} + + {#if $isSmallViewport} +
+ +
+ Mock Numbers Example +
+
+ {:else if $isTabletViewport} + +
+
+ Backups Example +
+
+ {:else} + {#each Array.from({ length }) as _} + + One backup every 24 hours, retained for 30 days + + {/each} + {/if} +
+ + + + + Database Backups are available on Appwrite Cloud + + + + Sign up now to access Appwrite's backups. Schedule automatic or manual backups + to protect your data and ensure quick recovery. + + + + + +
+{/snippet} + +{#snippet selectDatabaseType()} + + {#each databaseTypes as databaseType} + + {databaseType.subtitle} + + {/each} + +{/snippet} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/init.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/init.ts index 739e074344..d152b8df65 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/init.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/init.ts @@ -17,7 +17,7 @@ export type Terminologies = { analytics: AnalyticsResult; terminology: TerminologyResult; dependencies: DependenciesResult; - databaseSdk: DatabaseSdkResult; + databasesSdk: DatabaseSdkResult; }; export function getTerminologies(): Terminologies { 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 54220872ac..5700e1938b 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 @@ -5,6 +5,14 @@ import { type DatabaseType, type Entity, type EntityList, toSupportiveEntity } f import type { Models } from '@appwrite.io/console'; export type DatabaseSdkResult = { + create: ( + type: DatabaseType, + params: { + databaseId: string; + name: string; + enabled?: boolean; + } + ) => Promise; list: (params: { queries?: string[]; search?: string }) => Promise; getEntity: (params: { databaseId: string; @@ -42,6 +50,22 @@ export function useDatabasesSdk( const baseSdk = sdk.forProject(region, project); return { + async create(type, params): Promise { + switch (type) { + case 'legacy': /* databases api */ + case 'tablesdb': { + return await baseSdk.tablesDB.create(params); + } + case 'documentsdb': { + return await baseSdk.documentsDB.create(params); + } + case 'vectordb': + throw new Error(`Database type not supported yet`); + default: + throw new Error('Unknown database type'); + } + }, + async list(params): Promise { const results = await Promise.all([ baseSdk.tablesDB.list(params) @@ -74,7 +98,7 @@ export function useDatabasesSdk( case 'vectordb': throw new Error(`Database type not supported yet`); default: - throw new Error(`Unknown database type`); + throw new Error('Unknown database type'); } }, diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/input.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/input.svelte index 16666a9042..be98a0ee25 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/input.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/input.svelte @@ -16,19 +16,39 @@ const featureActive = $derived(isCloud); const { terminology } = getTerminologies(); + + const type = terminology.type; const field = terminology.field.lower; const entity = terminology.entity.lower.singular; const title = $derived.by(() => { - return featureActive - ? `Smart ${field.singular} suggestions` - : `Smart ${field.singular} suggestions available on Cloud`; + switch (type) { + default: + case 'legacy': + case 'tablesdb': + return featureActive + ? `Smart ${field.singular} suggestions` + : `Smart ${field.singular} suggestions available on Cloud`; + + case 'documentsdb': + return featureActive ? `Sample Data` : `Sample Data available on Cloud`; + } }); const subtitle = $derived.by(() => { - return featureActive - ? `Enable AI to suggest useful ${field.plural} based on your ${entity} name` - : `Sign up for Cloud to generate ${field.plural} based on your ${entity} name`; + switch (type) { + default: + case 'legacy': + case 'tablesdb': + return featureActive + ? `Enable AI to suggest useful ${field.plural} based on your ${entity} name` + : `Sign up for Cloud to generate ${field.plural} based on your ${entity} name`; + + case 'documentsdb': + return featureActive + ? `Enable AI to generate sample documents based on your ${entity} name` + : `Sign up for Cloud to generate sample documents based on your ${entity} name`; + } }); 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 d469b86b39..2c097408dd 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 @@ -1,8 +1,8 @@ +import Header from './header.svelte'; import { sdk } from '$lib/stores/sdk'; -import { Dependencies } from '$lib/constants'; import type { LayoutLoad } from './$types'; +import { Dependencies } from '$lib/constants'; import Breadcrumbs from './breadcrumbs.svelte'; -import Header from './header.svelte'; import SubNavigation from './subNavigation.svelte'; export const load: LayoutLoad = async ({ params, depends }) => { 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 90066c9f96..e9bf52da23 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 @@ -34,6 +34,17 @@ function getImageRoute(type: 'light' | 'dark'): string { return withPath(resolveRoute('/'), `/images/empty-database-${type}.svg`); } + + const emptyPageText = $derived.by(() => { + switch (terminology.type) { + default: + case 'legacy': + case 'tablesdb': + return `Create, organize, and query structured data with ${entityTitle.plural}.`; + case 'documentsdb': + return `Create, organize, and query flexible data with ${entityTitle.plural}.`; + } + }); @@ -53,7 +64,7 @@ {#if $canWriteTables} {/if} @@ -86,9 +97,7 @@ - - Create, organize, and query structured data with {entityTitle.plural}. - + {emptyPageText} + + {/if} {/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/store.ts index 43cc2552d5..55a48f9263 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/store.ts @@ -5,8 +5,21 @@ import type { UserBackupPolicy } from '$lib/helpers/backups'; export const showCreatePolicy = writable(false); export const showCreateBackup = writable(false); +export const dailyPolicy: UserBackupPolicy = { + id: 'daily', + label: 'Daily', + retained: 7, + default: true, + checked: false, + schedule: '{time} * * *', + selectedTime: '00:00', + plainTextFrequency: 'daily', + description: 'Runs every day and is retained for 7 days' +}; + export const presetPolicies = writable([ { + id: 'hourly', label: 'Hourly', retained: 1, default: true, @@ -17,6 +30,7 @@ export const presetPolicies = writable([ description: 'Runs every hour and is retained for 24 hours' }, { + id: 'daily', label: 'Daily', retained: 7, default: true, @@ -25,6 +39,13 @@ export const presetPolicies = writable([ selectedTime: '00:00', plainTextFrequency: 'daily', description: 'Runs every day and is retained for 7 days' + }, + { + id: 'none', + label: 'No backup', + retained: null, + default: false, + description: 'Skip backups. You can change this later' } ]); diff --git a/src/routes/(console)/project-[region]-[project]/databases/grid.svelte b/src/routes/(console)/project-[region]-[project]/databases/grid.svelte index 99e3fc8462..ca8f745920 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/grid.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/grid.svelte @@ -37,11 +37,7 @@ {database.name} - + @@ -66,9 +62,3 @@

Create a database

- - From 8c71fe34227b84b56b26e1b8e4dc69ee96d30cc6 Mon Sep 17 00:00:00 2001 From: Darshan Date: Mon, 13 Oct 2025 20:12:15 +0530 Subject: [PATCH 03/60] update: setup collections page, route. update: move things around. --- src/lib/actions/analytics.ts | 13 ++ src/lib/commandCenter/commands.ts | 2 + src/lib/constants.ts | 3 + .../databases/create.svelte | 154 ---------------- .../(entity)/helpers/sdk.ts | 39 ++++- .../(entity)/helpers/terminology.ts | 2 +- .../(entity)/views/header.svelte | 4 +- .../(entity)/views/indexes/view.svelte | 4 +- .../(entity)/views/layouts/empty.svelte | 3 +- .../(suggestions)/empty.svelte | 3 +- .../database-[database]/+layout.svelte | 30 ++-- .../collection-[collection]/+layout.svelte | 164 ++++++++++++++++++ .../collection-[collection]/+layout.ts | 22 +++ .../collection-[collection]/+page.svelte | 136 +++++++++++++++ .../collection-[collection]/+page.ts | 62 +++++++ .../activity/+page.svelte | 14 ++ .../collection-[collection]/activity/+page.ts | 21 +++ .../collection-[collection]/header.svelte | 54 ++++++ .../indexes/+page.svelte | 68 ++++++++ .../settings/+page.svelte | 65 +++++++ .../collection-[collection]/store.ts | 5 + .../usage/[[period]]/+page.svelte | 11 ++ .../usage/[[period]]/+page.ts | 16 ++ .../databases/database-[database]/store.ts | 54 ++++++ .../table-[table]/+layout.svelte | 15 +- .../table-[table]/+page.svelte | 4 +- .../table-[table]/+page.ts | 5 +- .../table-[table]/rows/edit.svelte | 7 +- .../table-[table]/rows/editRelated.svelte | 11 +- .../table-[table]/rows/store.ts | 19 +- .../table-[table]/spreadsheet.svelte | 10 +- .../table-[table]/store.ts | 38 +--- 32 files changed, 803 insertions(+), 255 deletions(-) delete mode 100644 src/routes/(console)/project-[region]-[project]/databases/create.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.ts create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.ts create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/activity/+page.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/activity/+page.ts create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/header.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/indexes/+page.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/settings/+page.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/usage/[[period]]/+page.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/usage/[[period]]/+page.ts diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 5e28ad881f..28ce7f5b09 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -288,6 +288,11 @@ export enum Submit { RowUpdate = 'submit_row_update', RowUpdatePermissions = 'submit_row_update_permissions', + DocumentCreate = 'submit_document_create', + DocumentDelete = 'submit_document_delete', + DocumentUpdate = 'submit_document_update', + DocumentUpdatePermissions = 'submit_document_update_permissions', + IndexCreate = 'submit_index_create', IndexDelete = 'submit_index_delete', @@ -299,6 +304,14 @@ export enum Submit { TableUpdateEnabled = 'submit_table_update_enabled', TableUpdateDisplayNames = 'submit_table_update_display_names', + CollectionCreate = 'submit_collection_create', + CollectionDelete = 'submit_collection_delete', + CollectionUpdateName = 'submit_collection_update_name', + CollectionUpdatePermissions = 'submit_collection_update_permissions', + CollectionUpdateSecurity = 'submit_collection_update_security', + CollectionUpdateEnabled = 'submit_collection_update_enabled', + CollectionUpdateDisplayNames = 'submit_collection_update_display_names', + FunctionCreate = 'submit_function_create', FunctionDelete = 'submit_function_delete', FunctionUpdateName = 'submit_function_update_name', diff --git a/src/lib/commandCenter/commands.ts b/src/lib/commandCenter/commands.ts index fa8427d30b..a2edb2f962 100644 --- a/src/lib/commandCenter/commands.ts +++ b/src/lib/commandCenter/commands.ts @@ -29,9 +29,11 @@ const groups = [ 'migrations', 'users', 'tables', + 'collections', 'columns', 'indexes', 'rows', + 'documents', 'teams', 'security', 'buckets', diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 8fd03090e7..962b685ac7 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -46,6 +46,9 @@ export enum Dependencies { TABLE = 'dependency:table', ROW = 'dependency:row', ROWS = 'dependency:rows', + COLLECTION = 'dependency:collection', + DOCUMENT = 'dependency:document', + DOCUMENTS = 'dependency:documents', BUCKET = 'dependency:bucket', FILE = 'dependency:file', FILE_TOKENS = 'dependency:file_tokens', diff --git a/src/routes/(console)/project-[region]-[project]/databases/create.svelte b/src/routes/(console)/project-[region]-[project]/databases/create.svelte deleted file mode 100644 index 065daf6509..0000000000 --- a/src/routes/(console)/project-[region]-[project]/databases/create.svelte +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - {#if !showCustomId} -
- { - showCustomId = true; - }}> Database ID -
- {/if} - - - - {#if isCloud} - {#if $organization?.billingPlan === BillingPlan.FREE} - - Upgrade your plan to ensure your data stays safe and backed up. - - - - - {:else} - - {/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 5700e1938b..dc4bdf05b5 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,6 +14,12 @@ export type DatabaseSdkResult = { } ) => Promise; list: (params: { queries?: string[]; search?: string }) => Promise; + createEntity: (params: { + databaseId: string; + entityId: string; + name: string; + databaseType?: DatabaseType; + }) => Promise; getEntity: (params: { databaseId: string; entityId: string; @@ -60,7 +66,7 @@ export function useDatabasesSdk( return await baseSdk.documentsDB.create(params); } case 'vectordb': - throw new Error(`Database type not supported yet`); + throw new Error('Database type not supported yet'); default: throw new Error('Unknown database type'); } @@ -83,6 +89,31 @@ export function useDatabasesSdk( ); }, + async createEntity(params) { + switch (type ?? params.databaseType) { + case 'legacy': /* databases api */ + case 'tablesdb': { + const table = await baseSdk.tablesDB.createTable({ + ...params, + tableId: params.entityId + }); + return toSupportiveEntity(table); + } + case 'documentsdb': { + const table = await baseSdk.documentsDB.createCollection({ + ...params, + collectionId: params.entityId + }); + + return toSupportiveEntity(table); + } + case 'vectordb': + throw new Error('Database type not supported yet'); + default: + throw new Error('Unknown database type'); + } + }, + async listEntities(params) { switch (type ?? params.databaseType) { case 'legacy': /* databases api */ @@ -96,7 +127,7 @@ export function useDatabasesSdk( return { total, entities: collections.map(toSupportiveEntity) }; } case 'vectordb': - throw new Error(`Database type not supported yet`); + throw new Error('Database type not supported yet'); default: throw new Error('Unknown database type'); } @@ -120,7 +151,7 @@ export function useDatabasesSdk( return toSupportiveEntity(table); } case 'vectordb': - throw new Error(`Database type not supported yet`); + throw new Error('Database type not supported yet'); default: throw new Error(`Unknown database type`); } @@ -134,7 +165,7 @@ export function useDatabasesSdk( case 'documentsdb': return await baseSdk.documentsDB.delete(params); case 'vectordb': - throw new Error(`Database type not supported yet`); + throw new Error('Database type not supported yet'); default: throw new Error(`Unknown database type`); } 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 6fff779c4e..faac888aa8 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,8 +1,8 @@ import type { Page } from '@sveltejs/kit'; import { capitalize, plural } from '$lib/helpers/string'; +import type { Attributes, Columns, Table } from '$database/store'; import { AppwriteException, type Models } from '@appwrite.io/console'; -import type { Attributes, Columns, Table } from '$database/table-[table]/store'; import type { Term, TerminologyResult, TerminologyShape } from '$database/(entity)/helpers/types'; export type DatabaseType = 'legacy' | 'tablesdb' | 'documentsdb' | 'vectordb'; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/header.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/header.svelte index c30dfece89..4ea12bc79b 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/header.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/header.svelte @@ -8,8 +8,8 @@ import { type Entity, useTerminology } from '$database/(entity)'; import { resolveRoute, withPath } from '$lib/stores/navigation'; + import { expandTabs } from '$database/store'; import { preferences } from '$lib/stores/preferences'; - import { expandTabs } from '$database/table-[table]/store'; interface EntityTab { href: string; @@ -65,7 +65,7 @@ $effect(() => { if (nonSheetPages) expandTabs.set(true); else { - expandTabs.set(preferences.getKey('tableHeaderExpanded', true)); + expandTabs.set(preferences.getKey('entityHeaderExpanded', true)); } }); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/indexes/view.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/indexes/view.svelte index 8f68ba2c6e..9a7a3840e8 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/indexes/view.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/indexes/view.svelte @@ -50,7 +50,7 @@ onCreateIndex: (index: CreateIndexesCallbackType) => Promise; onDeleteIndexes: (indexKeys: string[]) => Promise; emptyIndexesSheetView: Snippet<[() => void]>; - emptyEntitiesSheetView?: Snippet; + emptyEntitiesSheetView?: Snippet<[() => void]>; } = $props(); let showCreateIndex = $state(false); @@ -294,7 +294,7 @@ {@render emptyIndexesSheetView(() => (showCreateIndex = true))} {/if} {:else} - {@render emptyEntitiesSheetView?.()} + {@render emptyEntitiesSheetView?.(() => (showCreateIndex = true))} {/if} {#if selectedIndexes.length > 0} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte index f4bedb2e70..66e6d3a2a7 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte @@ -14,7 +14,8 @@ import { SpreadsheetContainer } from '$database/(entity)'; import { onDestroy, onMount } from 'svelte'; import { debounce } from '$lib/helpers/debounce'; - import { expandTabs, spreadsheetLoading } from '$database/table-[table]/store'; + import { expandTabs } from '$database/store'; + import { spreadsheetLoading } from '$database/table-[table]/store'; type Mode = 'rows' | 'rows-filtered' | 'indexes'; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte index a6260fe608..2be6de4ce3 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte @@ -13,7 +13,7 @@ import { IconFingerPrint, IconPlus } from '@appwrite.io/pink-icons-svelte'; import { isSmallViewport, isTabletViewport } from '$lib/stores/viewport'; import type { Column } from '$lib/helpers/types'; - import { expandTabs } from '../table-[table]/store'; + import { expandTabs, type Columns } from '../store'; import { SpreadsheetContainer } from '$database/(entity)'; import { onDestroy, onMount, tick } from 'svelte'; import { sdk } from '$lib/stores/sdk'; @@ -33,7 +33,6 @@ import { invalidate } from '$app/navigation'; import { Dependencies } from '$lib/constants'; import { isWithinSafeRange } from '$lib/helpers/numbers'; - import type { Columns } from '../table-[table]/store'; import { columnOptions } from '../table-[table]/columns/store'; import Options from './options.svelte'; import { InputSelect, InputText } from '$lib/elements/forms'; 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 b11e83f6ef..c84cc44613 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 @@ -18,8 +18,10 @@ import { currentPlan } from '$lib/stores/organization'; import { isCloud } from '$lib/system'; import { noWidthTransition } from '$lib/stores/sidebar'; - import { CreateEntity, setTerminologies } from '$database/(entity)'; - import { sdk } from '$lib/stores/sdk'; + import { CreateEntity, getTerminologies, setTerminologies } from '$database/(entity)'; + import { resolveRoute, withPath } from '$lib/stores/navigation'; + + setTerminologies(page); const project = page.params.project; const databaseId = page.params.database; @@ -137,22 +139,26 @@ $noWidthTransition = true; - async function createEntity(tableId: string, name: string) { - const table = await sdk - .forProject(page.params.region, page.params.project) - .tablesDB.createTable({ - databaseId, - tableId, - name - }); + async function createEntity(entityId: string, name: string) { + const entity = await databasesSdk.createEntity({ + databaseId, + entityId, + name + }); await invalidate(Dependencies.DATABASE); await goto( - `${base}/project-${page.params.region}-${project}/databases/database-${databaseId}/table-${table.$id}` + withPath( + resolveRoute( + '/(console)/project-[region]-[project]/databases/database-[database]', + page.params + ), + `/${terminology.entity.lower.singular}-${entity.$id}` + ) ); } - $: setTerminologies(page); + const { databasesSdk, terminology } = getTerminologies(); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte new file mode 100644 index 0000000000..f49d3685fd --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte @@ -0,0 +1,164 @@ + + + + + + {collection?.name ?? 'Collection'} - Appwrite + + + 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 new file mode 100644 index 0000000000..4ea0e31eac --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.ts @@ -0,0 +1,22 @@ +import Header from './header.svelte'; +import type { LayoutLoad } from './$types'; +import { Dependencies } from '$lib/constants'; +import { Breadcrumbs, useDatabasesSdk } from '$database/(entity)'; + +export const load: LayoutLoad = async ({ params, depends, parent }) => { + const { database } = await parent(); + depends(Dependencies.COLLECTION); + + const databasesSdk = useDatabasesSdk(params.region, params.project, database.type); + + const collection = await databasesSdk.getEntity({ + databaseId: params.database, + entityId: params.collection + }); + + return { + collection, + header: Header, + breadcrumbs: Breadcrumbs + }; +}; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte new file mode 100644 index 0000000000..675ff6671b --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte @@ -0,0 +1,136 @@ + + +{#key page.params.collection} + + + + + + {#if !$isSmallViewport} + + + + {/if} + + + {#if $isSmallViewport} + + {/if} + + + +
+ {#if data.documents.total} + + + {JSON.stringify( + { + documents: data.documents + }, + 2, + null + )} + {:else if $hasPageQueries} + Nothing here, please go + {:else} + Nothing here, please go + {/if} +
+{/key} + +{#if showImportCSV} + + +{/if} + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.ts new file mode 100644 index 0000000000..494e8c3ade --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.ts @@ -0,0 +1,62 @@ +import { Dependencies, SPREADSHEET_PAGE_LIMIT } from '$lib/constants'; +import { getLimit, getPage, getQuery, getView, pageToOffset, View } from '$lib/helpers/load'; +import { sdk } from '$lib/stores/sdk'; +import { Query } from '@appwrite.io/console'; +import type { PageLoad } from './$types'; +import { queries, queryParamToMap } from '$lib/components/filters'; +import type { TagValue } from '$lib/components/filters/store'; +import type { Entity } from '$database/(entity)'; +import { buildWildcardEntitiesQuery } from '$database/store'; + +export const load: PageLoad = async ({ params, depends, url, route, parent }) => { + const { collection } = await parent(); + depends(Dependencies.DOCUMENTS); + + const page = getPage(url); + const limit = getLimit(url, route, SPREADSHEET_PAGE_LIMIT); + const view = getView(url, route, View.Grid); + const offset = pageToOffset(page, limit); + const query = getQuery(url); + + const paramQueries = url.searchParams.get('query'); + const parsedQueries = queryParamToMap(paramQueries || '[]'); + queries.set(parsedQueries); + + // const currentSort = extractSortFromQueries(parsedQueries); + + return { + offset, + limit, + view, + query, + // currentSort, + parsedQueries, + documents: await sdk.forProject(params.region, params.project).documentsDB.listDocuments({ + databaseId: params.database, + collectionId: params.collection, + queries: buildGridQueries(limit, offset, parsedQueries, collection) + }) + }; +}; + +function buildGridQueries( + limit: number, + offset: number, + parsedQueries: Map, + entity: Entity +) { + const hasOrderQuery = Array.from(parsedQueries.values()).some( + (q) => q.includes('orderAsc') || q.includes('orderDesc') + ); + + const queryArray = [Query.limit(limit), Query.offset(offset)]; + + // don't override if there's a user created sort! + if (!hasOrderQuery) { + queryArray.push(Query.orderDesc('')); + } + + queryArray.push(...parsedQueries.values(), ...buildWildcardEntitiesQuery(entity)); + + return queryArray; +} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/activity/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/activity/+page.svelte new file mode 100644 index 0000000000..849d38953b --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/activity/+page.svelte @@ -0,0 +1,14 @@ + + +
+ +
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/activity/+page.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/activity/+page.ts new file mode 100644 index 0000000000..c7187a963f --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/activity/+page.ts @@ -0,0 +1,21 @@ +import { sdk } from '$lib/stores/sdk'; +import type { PageLoad } from './$types'; +import { PAGE_LIMIT } from '$lib/constants'; +import { Query } from '@appwrite.io/console'; +import { getLimit, getPage, pageToOffset } from '$lib/helpers/load'; + +export const load: PageLoad = async ({ params, url, route }) => { + const page = getPage(url); + const limit = getLimit(url, route, PAGE_LIMIT); + const offset = pageToOffset(page, limit); + + return { + offset, + limit, + logs: await sdk.forProject(params.region, params.project).documentsDB.listCollectionLogs({ + databaseId: params.database, + collectionId: params.collection, + queries: [Query.limit(limit), Query.offset(offset)] + }) + }; +}; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/header.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/header.svelte new file mode 100644 index 0000000000..bfeef04331 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/header.svelte @@ -0,0 +1,54 @@ + + +{#if collection} +
+{/if} 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 new file mode 100644 index 0000000000..5864535151 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/indexes/+page.svelte @@ -0,0 +1,68 @@ + + + + {#snippet emptyIndexesSheetView(toggle)} + + {/snippet} + + {#snippet emptyEntitiesSheetView(toggle)} + + {/snippet} + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/settings/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/settings/+page.svelte new file mode 100644 index 0000000000..71b22832cd --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/settings/+page.svelte @@ -0,0 +1,65 @@ + + +
+ + updateCollection({ enabled })} /> + + updateCollection({ name })} /> + + updateCollection({ permissions })} /> + + updateCollection({ documentSecurity })} /> + + + +
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts new file mode 100644 index 0000000000..9d3d04459d --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts @@ -0,0 +1,5 @@ +import { page } from '$app/stores'; +import { derived } from 'svelte/store'; +import type { Models } from '@appwrite.io/console'; + +export const indexes = derived(page, ($page) => $page.data.collection.indexes as Models.Index[]); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/usage/[[period]]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/usage/[[period]]/+page.svelte new file mode 100644 index 0000000000..472f1e4f77 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/usage/[[period]]/+page.svelte @@ -0,0 +1,11 @@ + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/usage/[[period]]/+page.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/usage/[[period]]/+page.ts new file mode 100644 index 0000000000..6b9caf5ba9 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/usage/[[period]]/+page.ts @@ -0,0 +1,16 @@ +import { isValueOfStringEnum } from '$lib/helpers/types'; +import { sdk } from '$lib/stores/sdk'; +import { UsageRange } from '@appwrite.io/console'; +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ params }) => { + const period = isValueOfStringEnum(UsageRange, params.period) + ? params.period + : UsageRange.ThirtyDays; + + return sdk.forProject(params.region, params.project).documentsDB.getCollectionUsage({ + databaseId: params.database, + collectionId: params.collection, + range: period + }); +}; 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 d5e75fb54e..52705eedce 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 @@ -3,6 +3,47 @@ import type { Column } from '$lib/helpers/types'; import { IconChartBar, IconCloudUpload, IconCog } from '@appwrite.io/pink-icons-svelte'; import { resolveRoute, withPath } from '$lib/stores/navigation'; import type { Page } from '@sveltejs/kit'; +import { type Models, Query } from '@appwrite.io/console'; +import type { Entity, Field } from '$database/(entity)'; +import { isRelationship } from '$database/table-[table]/rows/store'; + +export type Columns = + | Models.ColumnBoolean + | Models.ColumnEmail + | Models.ColumnEnum + | Models.ColumnFloat + | Models.ColumnInteger + | Models.ColumnIp + | Models.ColumnString + | Models.ColumnUrl + | Models.ColumnPoint + | Models.ColumnLine + | Models.ColumnPolygon + | (Models.ColumnRelationship & { default?: never }); + +export type Attributes = + | Models.AttributeBoolean + | Models.AttributeEmail + | Models.AttributeEnum + | Models.AttributeFloat + | Models.AttributeInteger + | Models.AttributeIp + | Models.AttributeString + | Models.AttributeUrl + | Models.AttributePoint + | Models.AttributeLine + | Models.AttributePolygon + | (Models.AttributeRelationship & { default?: never }); + +export type Collection = Omit & { + attributes: Array; +}; + +export type Table = Omit & { + columns: Array; +}; + +export const expandTabs = writable(null); export const showCreateEntity = writable(false); @@ -45,3 +86,16 @@ export function buildEntityRoute(page: Page, entityType: string, entityId: strin `/${entityType}-${entityId}` ); } + +/** + * Returns select queries for all main and related fields in an `Entity`. + */ +export function buildWildcardEntitiesQuery(entity: Entity | null = null): string[] { + return [ + ...(entity?.fields + ?.filter((field: Field) => field.status === 'available' && isRelationship(field)) + ?.map((field: Field) => Query.select([`${field.key}.*`])) ?? []), + + Query.select(['*']) + ]; +} 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 a734417489..a901afaf4e 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 @@ -32,10 +32,9 @@ spreadsheetLoading, rowActivitySheet, spreadsheetRenderKey, - expandTabs, databaseRelatedRowSheetOptions, rowPermissionSheet, - type Columns + showRowCreateSheet } from './store'; import { addSubPanel, registerCommands, updateCommandGroupRanks } from '$lib/commandCenter'; import CreateColumn from './createColumn.svelte'; @@ -62,6 +61,8 @@ import { chunks } from '$lib/helpers/array'; import { Submit, trackEvent } from '$lib/actions/analytics'; + import { expandTabs, type Columns } from '../store'; + import type { LayoutData } from './$types'; import { CreateIndex } from '$database/(entity)'; @@ -93,7 +94,7 @@ ); onMount(() => { - expandTabs.set(preferences.getKey('tableHeaderExpanded', true)); + expandTabs.set(preferences.getKey('entityHeaderExpanded', true)); return realtime .forProject(page.params.region, page.params.project) @@ -121,8 +122,12 @@ $: $registerCommands([ { label: 'Create row', - keys: page.url.pathname.endsWith(table?.$id) ? ['t'] : ['t', 'd'], - callback: () => ($showCreateEntity = true), + keys: page.url.pathname.endsWith(table?.$id) ? ['r'] : ['r', 'd'], + callback: () => { + if (table.fields) { + $showRowCreateSheet.show = true; + } + }, icon: IconPlus, group: 'rows' }, diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte index bec674be31..3f2db4fc1d 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte @@ -14,7 +14,6 @@ showRowCreateSheet, showCreateColumnSheet, randomDataModalState, - expandTabs, columnsOrder } from './store'; import SpreadSheet from './spreadsheet.svelte'; @@ -33,6 +32,7 @@ import { columnOptions } from './columns/store'; import { EmptySheet, type Field } from '$database/(entity)'; import { Empty as SuggestionsEmptySheet, tableColumnSuggestions } from '../(suggestions)'; + import { expandTabs } from '$database/store'; export let data: PageData; @@ -177,7 +177,7 @@ class="small-button-dimensions" on:click={() => { $expandTabs = !$expandTabs; - preferences.setKey('tableHeaderExpanded', $expandTabs); + preferences.setKey('entityHeaderExpanded', $expandTabs); }}> diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.ts index 0f954c47d4..157abc17f6 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.ts @@ -4,13 +4,12 @@ import { sdk } from '$lib/stores/sdk'; import { Query } from '@appwrite.io/console'; import type { PageLoad } from './$types'; import { queries, queryParamToMap } from '$lib/components/filters'; -import { buildWildcardColumnsQuery } from './rows/store'; +import { buildWildcardEntitiesQuery } from '$database/store'; import type { TagValue } from '$lib/components/filters/store'; import type { Entity } from '$database/(entity)'; export const load: PageLoad = async ({ params, depends, url, route, parent }) => { const { table } = await parent(); - depends(Dependencies.ROW); /* TODO: we could just invalidate the rows maybe? */ depends(Dependencies.ROWS); const page = getPage(url); @@ -71,7 +70,7 @@ function buildGridQueries( queryArray.push(Query.orderDesc('')); } - queryArray.push(...parsedQueries.values(), ...buildWildcardColumnsQuery(table)); + queryArray.push(...parsedQueries.values(), ...buildWildcardEntitiesQuery(table)); return queryArray; } diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/edit.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/edit.svelte index aafd71bd1c..b31b75b942 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/edit.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/edit.svelte @@ -8,12 +8,13 @@ import type { Models } from '@appwrite.io/console'; import { Dependencies } from '$lib/constants'; import { invalidate } from '$app/navigation'; - import { type Columns, PROHIBITED_ROW_KEYS } from '../store'; + import { PROHIBITED_ROW_KEYS } from '../store'; import ColumnItem from './columns/columnItem.svelte'; - import { buildWildcardColumnsQuery, isRelationship, isRelationshipToMany } from './store'; + import { isRelationship, isRelationshipToMany } from './store'; import { Layout, Skeleton } from '@appwrite.io/pink-svelte'; import { deepClone } from '$lib/helpers/object'; import { type Entity, toRelationalField } from '$database/(entity)'; + import { type Columns, buildWildcardEntitiesQuery } from '$database/store'; let { table, @@ -52,7 +53,7 @@ databaseId: table.databaseId, tableId: table.$id, rowId, - queries: buildWildcardColumnsQuery(table) + queries: buildWildcardEntitiesQuery(table) }); } catch (error) { addNotification({ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/editRelated.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/editRelated.svelte index 72a4735868..dbd895eb30 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/editRelated.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/editRelated.svelte @@ -8,9 +8,10 @@ import { type Models, Query } from '@appwrite.io/console'; import { Dependencies } from '$lib/constants'; import { invalidate } from '$app/navigation'; - import { type Columns, PROHIBITED_ROW_KEYS } from '../store'; + import { PROHIBITED_ROW_KEYS } from '../store'; + import { type Columns, buildWildcardEntitiesQuery } from '$database/store'; import ColumnItem from './columns/columnItem.svelte'; - import { buildWildcardColumnsQuery, isRelationship, isRelationshipToMany } from './store'; + import { isRelationship, isRelationshipToMany } from './store'; import { Accordion, Layout, Skeleton } from '@appwrite.io/pink-svelte'; import { deepClone } from '$lib/helpers/object'; import { preferences } from '$lib/stores/preferences'; @@ -66,11 +67,11 @@ databaseId, tableId: tableId, rowId: rows as string, - queries: buildWildcardColumnsQuery(relatedTable) + queries: buildWildcardEntitiesQuery(relatedTable) }); // cannot use page.data.entities! - relatedTable = await databaseSdk.getEntity({ + relatedTable = await databasesSdk.getEntity({ databaseId, entityId: tableId }); @@ -141,7 +142,7 @@ queries: [ Query.equal('$id', rowIds), Query.limit(rowIds.length), - ...buildWildcardColumnsQuery(rowTable) + ...buildWildcardEntitiesQuery(rowTable) ] }); return response.rows; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/store.ts index a007c01fc4..55b77aaebd 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/store.ts @@ -1,8 +1,8 @@ import { page } from '$app/state'; +import type { Field } from '$database/(entity)'; import type { Column } from '$lib/helpers/types'; -import type { Attributes, Columns } from '../store'; -import { type Models, Query } from '@appwrite.io/console'; -import type { Entity, Field } from '$database/(entity)'; +import { type Models } from '@appwrite.io/console'; +import type { Attributes, Columns } from '../../store'; export function isRelationshipToMany(field: Field) { if (!field) return false; @@ -46,19 +46,6 @@ export function isSpatialType( return spatialTypes.includes(field.type.toLowerCase()); } -/** - * Returns select queries for all main and related fields in an `Entity`. - */ -export function buildWildcardColumnsQuery(entity: Entity | null = null): string[] { - return [ - ...(entity?.fields - ?.filter((field) => field.status === 'available' && isRelationship(field)) - ?.map((field) => Query.select([`${field.key}.*`])) ?? []), - - Query.select(['*']) - ]; -} - export function buildRowUrl(rowId: string) { return `${page.url}/row-${rowId}`; } 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 95d946eeed..c6528848bd 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 @@ -13,7 +13,6 @@ import type { PageData } from './$types'; import { buildRowUrl, - buildWildcardColumnsQuery, isRelationship, isRelationshipToMany, isSpatialType, @@ -38,10 +37,8 @@ paginatedRows, paginatedRowsLoading, spreadsheetRenderKey, - expandTabs, databaseRelatedRowSheetOptions, - rowPermissionSheet, - type Columns + rowPermissionSheet } from './store'; import type { Column, ColumnType } from '$lib/helpers/types'; import { @@ -92,6 +89,7 @@ import { formatNumberWithCommas } from '$lib/helpers/numbers'; import { chunks } from '$lib/helpers/array'; import { mapToQueryParams } from '$lib/components/filters/store'; + import { expandTabs, type Columns, buildWildcardEntitiesQuery } from '$database/store'; export let data: PageData; export let showRowCreateSheet: { @@ -655,7 +653,7 @@ Query.limit(SPREADSHEET_PAGE_LIMIT), Query.offset(pageToOffset(pageNumber, SPREADSHEET_PAGE_LIMIT)), ...filterQueries /* filter queries */, - ...buildWildcardColumnsQuery(table) + ...buildWildcardEntitiesQuery(table) ] }); @@ -682,7 +680,7 @@ getCorrectOrderQuery(), Query.limit(SPREADSHEET_PAGE_LIMIT), Query.offset(pageToOffset(targetPageNum, SPREADSHEET_PAGE_LIMIT)), - ...buildWildcardColumnsQuery(table) + ...buildWildcardEntitiesQuery(table) ] }); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts index e7847d113e..08da155872 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts @@ -5,42 +5,7 @@ import { derived, writable } from 'svelte/store'; import type { SortDirection } from '$lib/components'; import { SPREADSHEET_PAGE_LIMIT } from '$lib/constants'; import { createSparsePagedDataStore } from '@appwrite.io/pink-svelte'; - -export type Columns = - | Models.ColumnBoolean - | Models.ColumnEmail - | Models.ColumnEnum - | Models.ColumnFloat - | Models.ColumnInteger - | Models.ColumnIp - | Models.ColumnString - | Models.ColumnUrl - | Models.ColumnPoint - | Models.ColumnLine - | Models.ColumnPolygon - | (Models.ColumnRelationship & { default?: never }); - -export type Attributes = - | Models.AttributeBoolean - | Models.AttributeEmail - | Models.AttributeEnum - | Models.AttributeFloat - | Models.AttributeInteger - | Models.AttributeIp - | Models.AttributeString - | Models.AttributeUrl - | Models.AttributePoint - | Models.AttributeLine - | Models.AttributePolygon - | (Models.AttributeRelationship & { default?: never }); - -export type Collection = Omit & { - attributes: Array; -}; - -export type Table = Omit & { - columns: Array; -}; +import type { Columns } from '$database/store'; export const columns = derived(page, ($page) => $page.data.table.columns as Columns[]); export const indexes = derived(page, ($page) => $page.data.table.indexes as Models.ColumnIndex[]); @@ -194,7 +159,6 @@ export const rowPermissionSheet = writable({ row: null as Models.Row }); -export const expandTabs = writable(null); export const spreadsheetRenderKey = writable('initial'); export const paginatedRowsLoading = writable(false); From 0410b5636d1ff3f9a9f16d11354f09be6a44ff61 Mon Sep 17 00:00:00 2001 From: Darshan Date: Thu, 16 Oct 2025 14:30:14 +0530 Subject: [PATCH 04/60] feat: improved editor. --- package.json | 13 +- pnpm-lock.yaml | 225 +++- src/lib/commandCenter/commandCenter.svelte | 10 +- src/lib/commandCenter/commands.ts | 9 +- src/lib/components/id.svelte | 34 +- src/lib/helpers/faker.ts | 114 +- src/lib/layout/footer.svelte | 8 +- .../(entity)/views/layouts/empty.svelte | 21 +- .../(entity)/views/layouts/sidesheet.svelte | 8 +- .../(entity)/views/layouts/spreadsheet.svelte | 44 +- .../database-[database]/+layout.svelte | 27 +- .../(components)/editor/helpers/constants.ts | 25 + .../(components)/editor/helpers/keymaps.ts | 25 + .../(components)/editor/helpers/theme.ts | 46 + .../(components)/editor/index.ts | 6 + .../(components)/editor/validators/json5.ts | 73 ++ .../(components)/editor/view.svelte | 1102 +++++++++++++++++ .../collection-[collection]/+layout.svelte | 60 +- .../collection-[collection]/+page.svelte | 55 +- .../collection-[collection]/+page.ts | 31 +- .../spreadsheet.svelte | 719 +++++++++++ .../collection-[collection]/store.ts | 21 +- .../databases/database-[database]/store.ts | 54 + .../table-[table]/+layout.svelte | 46 +- .../table-[table]/+page.svelte | 15 +- .../table-[table]/+page.ts | 41 +- .../table-[table]/columns/+page.svelte | 6 +- .../columns/createColumnDropdown.svelte | 4 +- .../table-[table]/spreadsheet.svelte | 10 +- .../table-[table]/store.ts | 15 +- 30 files changed, 2611 insertions(+), 256 deletions(-) create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/constants.ts create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/keymaps.ts create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/theme.ts create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/index.ts create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/validators/json5.ts create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte diff --git a/package.json b/package.json index 67c75ed37c..e82e2cbbd5 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,22 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2659", + "@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@de078ac", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@8f82877", "@appwrite.io/pink-legacy": "^1.0.3", "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8f82877", + "@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", "@popperjs/core": "^2.11.8", "@sentry/sveltekit": "^8.38.0", "@stripe/stripe-js": "^3.5.0", @@ -38,6 +48,7 @@ "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", "plausible-tracker": "^0.3.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4755eb1630..969279c143 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^1.1.24 version: 1.1.24(svelte@5.25.3)(zod@3.24.3) '@appwrite.io/console': - specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2659 - version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2659 + specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@de078ac + version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@de078ac '@appwrite.io/pink-icons': specifier: 0.25.0 version: 0.25.0 @@ -26,9 +26,39 @@ importers: '@appwrite.io/pink-svelte': specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8f82877 version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8f82877(svelte@5.25.3) + '@codemirror/autocomplete': + specifier: ^6.19.0 + version: 6.19.0 + '@codemirror/commands': + specifier: ^6.9.0 + version: 6.9.0 + '@codemirror/lang-javascript': + specifier: ^6.2.4 + version: 6.2.4 + '@codemirror/lang-json': + specifier: ^6.0.2 + version: 6.0.2 + '@codemirror/language': + specifier: ^6.11.3 + version: 6.11.3 + '@codemirror/lint': + specifier: ^6.9.0 + version: 6.9.0 + '@codemirror/search': + specifier: ^6.5.11 + version: 6.5.11 + '@codemirror/state': + specifier: ^6.5.2 + version: 6.5.2 + '@codemirror/view': + specifier: ^6.38.6 + version: 6.38.6 '@faker-js/faker': specifier: ^9.9.0 version: 9.9.0 + '@lezer/highlight': + specifier: ^1.2.1 + version: 1.2.1 '@popperjs/core': specifier: ^2.11.8 version: 2.11.8 @@ -59,6 +89,9 @@ importers: ignore: specifier: ^6.0.2 version: 6.0.2 + json5: + specifier: ^2.2.3 + version: 2.2.3 nanoid: specifier: ^5.1.5 version: 5.1.5 @@ -260,8 +293,8 @@ packages: '@analytics/type-utils@0.6.2': resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==} - '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2659': - resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2659} + '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@de078ac': + resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@de078ac} version: 1.10.0 '@appwrite.io/pink-icons-svelte@2.0.0-RC.1': @@ -377,6 +410,33 @@ packages: resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} + '@codemirror/autocomplete@6.19.0': + resolution: {integrity: sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg==} + + '@codemirror/commands@6.9.0': + resolution: {integrity: sha512-454TVgjhO6cMufsyyGN70rGIfJxJEjcqjBG2x2Y03Y/+Fm99d3O/Kv1QDYWuG6hvxsgmjXmBuATikIIYvERX+w==} + + '@codemirror/lang-javascript@6.2.4': + resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/language@6.11.3': + resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==} + + '@codemirror/lint@6.9.0': + resolution: {integrity: sha512-wZxW+9XDytH3SKvS8cQzMyQCaaazH8XL1EMHleHe00wVzsv7NBQKVW2yzEHrRhmM7ZOhVdItPbvlRBvMp9ej7A==} + + '@codemirror/search@6.5.11': + resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==} + + '@codemirror/state@6.5.2': + resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} + + '@codemirror/view@6.38.6': + resolution: {integrity: sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==} + '@csstools/color-helpers@5.0.2': resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} engines: {node: '>=18'} @@ -659,6 +719,24 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@lezer/common@1.2.3': + resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} + + '@lezer/highlight@1.2.1': + resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.2': + resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==} + + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@melt-ui/pp@0.3.2': resolution: {integrity: sha512-xKkPvaIAFinklLXcQOpwZ8YSpqAFxykjWf8Y/fSJQwsixV/0rcFs07hJ49hJjPy5vItvw5Qa0uOjzFUbXzBypQ==} peerDependencies: @@ -1569,11 +1647,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -1799,6 +1872,9 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} @@ -3168,6 +3244,9 @@ packages: strip-literal@3.0.0: resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + style-mod@4.1.2: + resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} + style-value-types@5.1.2: resolution: {integrity: sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==} @@ -3520,6 +3599,9 @@ packages: typescript: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -3703,7 +3785,7 @@ snapshots: '@analytics/type-utils@0.6.2': {} - '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2659': {} + '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@de078ac': {} '@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)': dependencies: @@ -3861,6 +3943,67 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@codemirror/autocomplete@6.19.0': + dependencies: + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.6 + '@lezer/common': 1.2.3 + + '@codemirror/commands@6.9.0': + dependencies: + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.6 + '@lezer/common': 1.2.3 + + '@codemirror/lang-javascript@6.2.4': + dependencies: + '@codemirror/autocomplete': 6.19.0 + '@codemirror/language': 6.11.3 + '@codemirror/lint': 6.9.0 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.6 + '@lezer/common': 1.2.3 + '@lezer/javascript': 1.5.4 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.11.3 + '@lezer/json': 1.0.3 + + '@codemirror/language@6.11.3': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.6 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + style-mod: 4.1.2 + + '@codemirror/lint@6.9.0': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.6 + crelt: 1.0.6 + + '@codemirror/search@6.5.11': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.6 + crelt: 1.0.6 + + '@codemirror/state@6.5.2': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/view@6.38.6': + dependencies: + '@codemirror/state': 6.5.2 + crelt: 1.0.6 + style-mod: 4.1.2 + w3c-keyname: 2.2.8 + '@csstools/color-helpers@5.0.2': {} '@csstools/css-calc@2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': @@ -4053,6 +4196,30 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@lezer/common@1.2.3': {} + + '@lezer/highlight@1.2.1': + dependencies: + '@lezer/common': 1.2.3 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@lezer/lr@1.4.2': + dependencies: + '@lezer/common': 1.2.3 + + '@marijn/find-cluster-break@1.0.2': {} + '@melt-ui/pp@0.3.2(@melt-ui/svelte@0.86.5(svelte@5.25.3))(svelte@5.25.3)': dependencies: '@melt-ui/svelte': 0.86.5(svelte@5.25.3) @@ -4745,10 +4912,6 @@ snapshots: '@stripe/stripe-js@3.5.0': {} - '@sveltejs/acorn-typescript@1.0.5(acorn@8.14.1)': - dependencies: - acorn: 8.14.1 - '@sveltejs/acorn-typescript@1.0.5(acorn@8.15.0)': dependencies: acorn: 8.15.0 @@ -5176,20 +5339,14 @@ snapshots: '@vue/shared@3.5.13': {} - acorn-import-attributes@1.9.5(acorn@8.14.1): + acorn-import-attributes@1.9.5(acorn@8.15.0): dependencies: - acorn: 8.14.1 - - acorn-jsx@5.3.2(acorn@8.14.1): - dependencies: - acorn: 8.14.1 + acorn: 8.15.0 acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 - acorn@8.14.1: {} - acorn@8.15.0: {} agent-base@6.0.2: @@ -5380,7 +5537,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 '@types/estree': 1.0.7 - acorn: 8.14.1 + acorn: 8.15.0 estree-walker: 3.0.3 periscopic: 3.1.0 @@ -5415,6 +5572,8 @@ snapshots: cookie@0.6.0: {} + crelt@1.0.6: {} + cron-parser@4.9.0: dependencies: luxon: 3.6.0 @@ -5830,8 +5989,8 @@ snapshots: espree@10.3.0: dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 4.2.0 espree@10.4.0: @@ -6081,8 +6240,8 @@ snapshots: import-in-the-middle@1.13.1: dependencies: - acorn: 8.14.1 - acorn-import-attributes: 1.9.5(acorn@8.14.1) + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) cjs-module-lexer: 1.4.3 module-details-from-path: 1.0.3 @@ -6843,6 +7002,8 @@ snapshots: dependencies: js-tokens: 9.0.1 + style-mod@4.1.2: {} + style-value-types@5.1.2: dependencies: hey-listen: 1.0.8 @@ -6915,7 +7076,7 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 '@types/estree': 1.0.7 - acorn: 8.14.1 + acorn: 8.15.0 aria-query: 5.3.2 axobject-query: 4.1.0 code-red: 1.0.4 @@ -6930,9 +7091,9 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@jridgewell/sourcemap-codec': 1.5.0 - '@sveltejs/acorn-typescript': 1.0.5(acorn@8.14.1) + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) '@types/estree': 1.0.7 - acorn: 8.14.1 + acorn: 8.15.0 aria-query: 5.3.2 axobject-query: 4.1.0 clsx: 2.1.1 @@ -7075,7 +7236,7 @@ snapshots: unplugin@1.0.1: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 chokidar: 3.6.0 webpack-sources: 3.2.3 webpack-virtual-modules: 0.5.0 @@ -7197,6 +7358,8 @@ snapshots: optionalDependencies: typescript: 5.8.2 + w3c-keyname@2.2.8: {} + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 diff --git a/src/lib/commandCenter/commandCenter.svelte b/src/lib/commandCenter/commandCenter.svelte index c82a5c0359..5936ad2bb0 100644 --- a/src/lib/commandCenter/commandCenter.svelte +++ b/src/lib/commandCenter/commandCenter.svelte @@ -96,7 +96,15 @@ }, 1000); function isInputEvent(event: KeyboardEvent) { - return ['INPUT', 'TEXTAREA', 'SELECT'].includes((event.target as HTMLElement).tagName); + const element = event.target as HTMLElement | null; + if (!element) return false; + + const tag = element.tagName; + if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag)) return true; + + // Treat contenteditable and CodeMirror editor as input contexts + if (element.isContentEditable) return true; + return !!element.closest?.('.cm-editor'); } const handleKeydown = (e: KeyboardEvent) => { diff --git a/src/lib/commandCenter/commands.ts b/src/lib/commandCenter/commands.ts index a2edb2f962..7fd9cfca68 100644 --- a/src/lib/commandCenter/commands.ts +++ b/src/lib/commandCenter/commands.ts @@ -103,7 +103,14 @@ const commandsEnabled = derived(disabledMap, ($disabledMap) => { }); function isInputEvent(event: KeyboardEvent) { - return ['INPUT', 'TEXTAREA', 'SELECT'].includes((event.target as HTMLElement).tagName); + const element = event.target as HTMLElement | null; + if (!element) return false; + + const tag = element.tagName; + if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag)) return true; + + if (element.isContentEditable) return true; + return !!element.closest?.('.cm-editor'); } function getCommandRank(command: KeyedCommand) { diff --git a/src/lib/components/id.svelte b/src/lib/components/id.svelte index 314e8fac2f..8b31e0bd69 100644 --- a/src/lib/components/id.svelte +++ b/src/lib/components/id.svelte @@ -1,4 +1,4 @@ - {#key value} @@ -106,7 +122,7 @@ style:overflow="hidden" style:word-break="break-all" use:truncateText> - + {@render children()} diff --git a/src/lib/helpers/faker.ts b/src/lib/helpers/faker.ts index 421288bb3b..8764f62899 100644 --- a/src/lib/helpers/faker.ts +++ b/src/lib/helpers/faker.ts @@ -1,9 +1,10 @@ import { faker } from '@faker-js/faker'; -import type { Columns } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store'; import { ID, type Models } from '@appwrite.io/console'; import { sdk } from '$lib/stores/sdk'; import { isWithinSafeRange } from '$lib/helpers/numbers'; import type { NestedNumberArray } from './types'; +import type { Columns } from '$database/store'; +import type { DatabaseType, Field } from '$database/(entity)'; export async function generateColumns( project: Models.Project, @@ -64,58 +65,71 @@ export async function generateColumns( ]); } +function generateDefaultRecord( + id: string +): Record< + string, + string | number | boolean | Array +> { + return { + $id: id, + name: faker.person.fullName(), + email: faker.internet.email(), + age: faker.number.int({ min: 18, max: 80 }), + city: faker.location.city(), + description: faker.lorem.sentence(), + active: faker.datatype.boolean(), + location: [faker.location.longitude(), faker.location.latitude()], + route: Array.from({ length: 5 }, () => [ + faker.location.longitude(), + faker.location.latitude() + ]) + }; +} + export function generateFakeRecords( - columns: Columns[], - count: number + count: number, + type: DatabaseType = 'tablesdb', + field?: Field[] ): { ids: string[]; - rows: Models.Row[]; + records: (Models.Document | Models.Row)[]; } { - if (count <= 0) return { ids: [], rows: [] }; - - const filteredColumns = columns.filter( - (col) => col.type !== 'relationship' && col.status === 'available' - ); + if (count <= 0) return { ids: [], records: [] }; - const ids: string[] = []; - const rows: Models.Row[] = []; + const ids = []; + const records = []; for (let i = 0; i < count; i++) { const id = ID.unique(); ids.push(id); - let row: Record< + let record: Record< string, string | number | boolean | Array - > = { - $id: id - }; - - if (filteredColumns.length === 0) { - row = { - $id: id, - name: faker.person.fullName(), - email: faker.internet.email(), - age: faker.number.int({ min: 18, max: 80 }), - city: faker.location.city(), - description: faker.lorem.sentence(), - active: faker.datatype.boolean(), - location: [faker.location.longitude(), faker.location.latitude()], - route: Array.from({ length: 5 }, () => [ - faker.location.longitude(), - faker.location.latitude() - ]) - }; + >; + + if (type === 'documentsdb') { + record = generateDefaultRecord(id); } else { - for (const column of filteredColumns) { - row[column.key] = generateValueForColumn(column); + const filteredColumns = + field?.filter((col) => col.type !== 'relationship' && col.status === 'available') ?? + []; + + if (filteredColumns.length === 0) { + record = generateDefaultRecord(id); + } else { + record = { $id: id }; + for (const column of filteredColumns) { + record[column.key] = generateValueForColumn(column); + } } } - rows.push(row as unknown as Models.Row); + records.push(record); } - return { ids, rows }; + return { ids, records }; } function generateStringValue(key: string, maxLength: number): string { @@ -141,15 +155,15 @@ function generateStringValue(key: string, maxLength: number): string { } function generateValueForColumn( - column: Columns + field: Field ): string | number | boolean | null | Array { - if (column.array) { + if (field.array) { const arraySize = faker.number.int({ min: 1, max: 5 }); const items: Array = []; for (let i = 0; i < arraySize; i++) { - const itemAttribute = { ...column, array: false }; - const item = generateSingleValue(itemAttribute); + const itemField = { ...field, array: false }; + const item = generateSingleValue(itemField); if (item !== null) { items.push(item); } @@ -158,16 +172,14 @@ function generateValueForColumn( return items; } - return generateSingleValue(column); + return generateSingleValue(field); } -function generateSingleValue( - column: Columns -): string | number | boolean | NestedNumberArray | null { - switch (column.type) { +function generateSingleValue(field: Field): string | number | boolean | NestedNumberArray | null { + switch (field.type) { case 'string': { - if ('format' in column && column.format) { - switch (column.format) { + if ('format' in field && field.format) { + switch (field.format) { case 'email': { return faker.internet.email(); } @@ -181,7 +193,7 @@ function generateSingleValue( } case 'enum': { - const enumAttr = column as Models.ColumnEnum; + const enumAttr = field as Models.ColumnEnum; if (enumAttr.elements?.length > 0) { return faker.helpers.arrayElement(enumAttr.elements); } @@ -190,14 +202,14 @@ function generateSingleValue( } return ''; } else { - const stringAttr = column as Models.ColumnString; + const stringAttr = field as Models.ColumnString; const maxLength = Math.min(stringAttr.size ?? 255, 1000); - return generateStringValue(column.key, maxLength); + return generateStringValue(field.key, maxLength); } } case 'integer': { - const intAttr = column as Models.ColumnInteger; + const intAttr = field as Models.ColumnInteger; const min = isWithinSafeRange(intAttr.min) ? intAttr.min : 0; const fallbackMax = Math.max(min + 100, 100); const max = isWithinSafeRange(intAttr.max) @@ -207,7 +219,7 @@ function generateSingleValue( } case 'double': { - const floatAttr = column as Models.ColumnFloat; + const floatAttr = field as Models.ColumnFloat; const min = isWithinSafeRange(floatAttr.min) ? floatAttr.min : 0; const fallbackMax = Math.max(min + 100, 100); const max = isWithinSafeRange(floatAttr.max) diff --git a/src/lib/layout/footer.svelte b/src/lib/layout/footer.svelte index 54e8ec053a..061720cd69 100644 --- a/src/lib/layout/footer.svelte +++ b/src/lib/layout/footer.svelte @@ -17,7 +17,13 @@ const currentYear = new Date().getFullYear(); const hideFooter = $derived.by(() => { - const endings = ['table-[table]', 'table-[table]/columns', 'table-[table]/indexes']; + const endings = [ + 'collection-[collection]', + 'collection-[collection]/indexes', + 'table-[table]', + 'table-[table]/columns', + 'table-[table]/indexes' + ]; return endings.some((end) => page.route.id?.endsWith(end)); }); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte index 66e6d3a2a7..dcccad3b9b 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte @@ -11,13 +11,13 @@ import { isSmallViewport, isTabletViewport } from '$lib/stores/viewport'; import { SortButton } from '$lib/components'; import type { Column } from '$lib/helpers/types'; - import { SpreadsheetContainer } from '$database/(entity)'; + import { getTerminologies, SpreadsheetContainer } from '$database/(entity)'; import { onDestroy, onMount } from 'svelte'; import { debounce } from '$lib/helpers/debounce'; import { expandTabs } from '$database/store'; import { spreadsheetLoading } from '$database/table-[table]/store'; - type Mode = 'rows' | 'rows-filtered' | 'indexes'; + type Mode = 'records' | 'records-filtered' | 'indexes'; interface Action { text?: string; @@ -52,6 +52,8 @@ const baseColProps = { draggable: false, resizable: false }; + const { terminology } = getTerminologies(); + const updateOverlayHeight = () => { if (!spreadsheetContainer) return; @@ -151,11 +153,13 @@ } ] as Column[]; - const spreadsheetColumns = $derived(mode === 'rows' ? getRowColumns() : getIndexesColumns()); + const spreadsheetColumns = $derived(mode === 'records' ? getRowColumns() : getIndexesColumns()); const emptyCells = $derived( ($isSmallViewport ? 14 : $isTabletViewport ? 17 : 24) + (!$expandTabs ? 2 : 0) ); + + const modeTerminology = $derived(terminology.record.lower.plural);
{ - if (columnActionsById && mode === 'rows') { + if (columnActionsById && mode === 'records') { onOpenCreateColumn?.(); } }}> @@ -232,7 +236,8 @@ style:--dynamic-overlay-height={dynamicOverlayHeight}>
- {title ?? `You have no ${mode} yet`} + {title ?? `You have no ${modeTerminology} yet`} {#if showActions} - {#if mode !== 'rows-filtered'} + {#if mode !== 'records-filtered'} - {#if mode === 'rows'} + {#if mode === 'records'} (show = false)); -
+
@@ -178,6 +180,10 @@ padding-bottom: 15rem; } } + + &.noContentPadding :global(section) { + padding: unset !important; + } } .sheet-footer { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/spreadsheet.svelte index 30483eb680..3ea808667c 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/spreadsheet.svelte @@ -2,11 +2,17 @@ import { debounce } from '$lib/helpers/debounce'; import { scrollStore, sheetHeightStore } from './store'; import { onMount, onDestroy, type Snippet, tick } from 'svelte'; + import { isSmallViewport } from '$lib/stores/viewport'; + import { SideSheet } from '$database/(entity)'; let { - children + children, + noSqlEditor, + showEditorSideSheet = $bindable(false) }: { children: Snippet; + noSqlEditor?: Snippet; + showEditorSideSheet: boolean; } = $props(); let spreadsheetWrapper: HTMLDivElement; @@ -112,12 +118,44 @@ }); -
+
{@render children()} + + {#if !$isSmallViewport} + {@render noSqlEditor?.()} + {:else} + + {@render noSqlEditor?.()} + + {/if}
- 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 c84cc44613..fe319de027 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 @@ -10,7 +10,7 @@ } from '$lib/commandCenter'; import { tablesSearcher } from '$lib/commandCenter/searchers'; import { Dependencies } from '$lib/constants'; - import { showCreateEntity } from './store'; + import { showCreateEntity, randomDataModalState } from './store'; import { TablesPanel } from '$lib/commandCenter/panels'; import { canWriteTables, canWriteDatabases } from '$lib/stores/roles'; import { showCreateBackup, showCreatePolicy } from './backups/store'; @@ -20,12 +20,16 @@ import { noWidthTransition } from '$lib/stores/sidebar'; import { CreateEntity, getTerminologies, setTerminologies } from '$database/(entity)'; import { resolveRoute, withPath } from '$lib/stores/navigation'; + import { Dialog, Layout, Typography } from '@appwrite.io/pink-svelte'; + import { Button, Seekbar } from '$lib/elements/forms'; setTerminologies(page); const project = page.params.project; const databaseId = page.params.database; + const { databasesSdk, terminology } = getTerminologies(); + $: $registerCommands([ { label: 'Create table', @@ -157,8 +161,6 @@ ) ); } - - const { databasesSdk, terminology } = getTerminologies(); @@ -171,3 +173,22 @@ + + + {@const records = terminology.record.lower.singular} + + + Select how many sample {records} to generate for testing. This won't delete or replace any + existing {records}. + + + + + + + + + + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/constants.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/constants.ts new file mode 100644 index 0000000000..c965ac9ce8 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/constants.ts @@ -0,0 +1,25 @@ +// system configuration constants +export const ALLOWED_DOLLAR_PROPS = ['$id', '$createdAt', '$updatedAt'] as const; +export const SYSTEM_KEYS = new Set(['$id:', '$createdAt:', '$updatedAt:']); + +// timing constants +export const DEBOUNCE_DELAY = 200; +export const LINTER_DELAY = 250; + +// regex patterns (compiled once for performance) +export const UNQUOTED_KEY_REGEX = /([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g; +export const INDENT_REGEX = /^[\t ]*/; +export const SCALAR_VALUE_REGEX = + /:\s*(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*$/; +export const TRAILING_COMMA_REGEX = /,\s*$/; +export const WHITESPACE_REGEX = /^\s*/; +export const WHITESPACE_ONLY_REGEX = /^\s+$/; +export const NESTED_KEY_REGEX = /^(\s{4,})([A-Za-z_$][A-Za-z0-9_$]*)\s*:/; + +// pre-computed indent strings for performance (0-20 levels of nesting) +export const INDENT_CACHE = Array.from({ length: 21 }, (_, i) => ' '.repeat(i)); + +// helper to get cached indent string +export function getIndent(level: number): string { + return level < INDENT_CACHE.length ? INDENT_CACHE[level] : ' '.repeat(level); +} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/keymaps.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/keymaps.ts new file mode 100644 index 0000000000..46d4e1d842 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/keymaps.ts @@ -0,0 +1,25 @@ +import { searchKeymap } from '@codemirror/search'; +import { closeBracketsKeymap } from '@codemirror/autocomplete'; +import type { EditorView, KeyBinding } from '@codemirror/view'; +import { defaultKeymap, historyKeymap, indentLess, indentMore } from '@codemirror/commands'; + +// main editor keymaps, +// these require functions from the component +export function createEditorKeymaps( + insertNewlineKeepIndent: (view: EditorView) => boolean +): KeyBinding[] { + return [ + { key: 'Tab', run: indentMore }, + { key: 'Enter', run: insertNewlineKeepIndent }, + { key: 'Shift-Enter', run: insertNewlineKeepIndent }, + { key: 'Shift-Tab', run: indentLess } + ]; +} + +// Secondary keymaps - these are standard CodeMirror keymaps +export const secondaryKeymaps = [ + ...closeBracketsKeymap, + ...defaultKeymap, + ...searchKeymap, + ...historyKeymap +]; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/theme.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/theme.ts new file mode 100644 index 0000000000..533dcca5a2 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/theme.ts @@ -0,0 +1,46 @@ +import { EditorView } from '@codemirror/view'; +import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; +import { tags } from '@lezer/highlight'; + +// custom theme for layout only (colors handled by SCSS) +export const customTheme = EditorView.theme({ + '&': { + height: '100%', + fontFamily: 'var(--font-family-code)', + fontSize: '14px', + lineHeight: '1.6' + }, + '.cm-content': { + fontFamily: 'var(--font-family-code)', + padding: 'var(--space-4) 0' + }, + '.cm-gutters': { + border: 'none', + minWidth: '40px' + }, + '.cm-lineNumbers .cm-gutterElement': { + textAlign: 'right', + minWidth: '40px', + paddingRight: 'var(--space-4)' + }, + '.cm-line': { + padding: '0', + lineHeight: '1.6' + } +}); + +// syntax highlighting style (colors applied via SCSS) +export const customHighlight = HighlightStyle.define([ + { tag: tags.propertyName, class: 'cm-propertyName' }, + { tag: tags.string, class: 'cm-string' }, + { tag: tags.special(tags.string), class: 'cm-string' }, + { tag: tags.escape, class: 'cm-string' }, + { tag: tags.number, class: 'cm-number' }, + { tag: tags.bool, class: 'cm-bool' }, + { tag: tags.null, class: 'cm-null' }, + { tag: tags.punctuation, class: 'cm-punctuation' }, + { tag: tags.bracket, class: 'cm-bracket' } +]); + +// pre-configured syntax highlighting extension +export const customSyntaxHighlighting = syntaxHighlighting(customHighlight); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/index.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/index.ts new file mode 100644 index 0000000000..488726fdb3 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/index.ts @@ -0,0 +1,6 @@ +export { + default as NoSqlEditor, + type JsonValue, + type JsonArray, + type JsonObject +} from './view.svelte'; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/validators/json5.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/validators/json5.ts new file mode 100644 index 0000000000..572e38ebb9 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/validators/json5.ts @@ -0,0 +1,73 @@ +import JSON5 from 'json5'; + +export interface Diagnostic { + from: number; + to: number; + message: string; +} + +export interface ValidatorResult { + ok: boolean; + diagnostics: Diagnostic[]; + parsed?: TParsed; + meta?: Record; +} + +async function validate(text: string): Promise { + try { + const parsed = JSON5.parse(text); + return { ok: true, diagnostics: [], parsed }; + } catch (err) { + const line: number | undefined = err?.lineNumber; + + if (!line) { + return { + ok: false, + diagnostics: [ + { + from: 0, + to: text.length, + message: err?.message || 'Syntax error' + } + ] + }; + } + + const lines = text.split('\n'); + + /** + * we highlight the previous line instead because sometimes, + * the reported line is the NEXT line as that's where the validator encounters an error! + */ + const targetLineIndex = Math.max(0, line - 2); + + // calculate line start position + let lineStartPos = 0; + for (let i = 0; i < targetLineIndex; i++) { + lineStartPos += lines[i].length + 1; + } + + // highlight the whole line (trimmed) + const targetLine = lines[targetLineIndex] || ''; + const trimmedStart = targetLine.trimStart(); + const leadingWhitespace = targetLine.length - trimmedStart.length; + const lineEndPos = lineStartPos + leadingWhitespace + trimmedStart.trimEnd().length; + + const diagnostic: Diagnostic = { + from: lineStartPos + leadingWhitespace, + to: lineEndPos, + message: (err?.message || 'Syntax error').replace(/^JSON5:\s*/i, '') + }; + + return { ok: false, diagnostics: [diagnostic] }; + } +} + +export async function parse(text: string): Promise { + const res = await validate(text); + if (!res.ok) + throw Object.assign(new Error(res.diagnostics[0]?.message || 'Invalid JSON5'), { + diagnostics: res.diagnostics + }); + return res.parsed as T; +} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte new file mode 100644 index 0000000000..08f52ab50f --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte @@ -0,0 +1,1102 @@ + + + + +
+
+ + {#if documentId} +
+ {truncateId(documentId)} +
+ {/if} +
+ + {#if errorMessage && !$isSmallViewport} +
+ {errorMessage} +
+ {/if} + + {#if documentId} + + + + Copy object + + {/if} +
+ + {#if errorMessage && $isSmallViewport} +
+ {errorMessage} +
+ {/if} +
+
+ + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte index f49d3685fd..60b356021e 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte @@ -10,7 +10,7 @@ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte index 675ff6671b..4d2ee9a4ce 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte @@ -1,5 +1,5 @@ {#key page.params.collection} @@ -98,17 +100,40 @@ {#if data.documents.total} - {JSON.stringify( - { - documents: data.documents - }, - 2, - null - )} + {:else if $hasPageQueries} - Nothing here, please go + { + queries.clearAll(); + queries.apply(); + trackEvent(Submit.FilterClear, { + source: 'database_collections' + }); + } + } + }} /> {:else} - Nothing here, please go + { + // some side sheet with a json editor + } + }, + random: { + onClick: () => { + $randomDataModalState.show = true; + } + } + }} /> {/if}
{/key} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.ts index 494e8c3ade..c51e119451 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.ts @@ -1,12 +1,9 @@ import { Dependencies, SPREADSHEET_PAGE_LIMIT } from '$lib/constants'; import { getLimit, getPage, getQuery, getView, pageToOffset, View } from '$lib/helpers/load'; import { sdk } from '$lib/stores/sdk'; -import { Query } from '@appwrite.io/console'; import type { PageLoad } from './$types'; import { queries, queryParamToMap } from '$lib/components/filters'; -import type { TagValue } from '$lib/components/filters/store'; -import type { Entity } from '$database/(entity)'; -import { buildWildcardEntitiesQuery } from '$database/store'; +import { buildGridQueries, extractSortFromQueries } from '$database/store'; export const load: PageLoad = async ({ params, depends, url, route, parent }) => { const { collection } = await parent(); @@ -22,14 +19,14 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) => const parsedQueries = queryParamToMap(paramQueries || '[]'); queries.set(parsedQueries); - // const currentSort = extractSortFromQueries(parsedQueries); + const currentSort = extractSortFromQueries(parsedQueries); return { offset, limit, view, query, - // currentSort, + currentSort, parsedQueries, documents: await sdk.forProject(params.region, params.project).documentsDB.listDocuments({ databaseId: params.database, @@ -38,25 +35,3 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) => }) }; }; - -function buildGridQueries( - limit: number, - offset: number, - parsedQueries: Map, - entity: Entity -) { - const hasOrderQuery = Array.from(parsedQueries.values()).some( - (q) => q.includes('orderAsc') || q.includes('orderDesc') - ); - - const queryArray = [Query.limit(limit), Query.offset(offset)]; - - // don't override if there's a user created sort! - if (!hasOrderQuery) { - queryArray.push(Query.orderDesc('')); - } - - queryArray.push(...parsedQueries.values(), ...buildWildcardEntitiesQuery(entity)); - - return queryArray; -} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte new file mode 100644 index 0000000000..49e973e6fa --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -0,0 +1,719 @@ + + + + {#key $spreadsheetRenderKey} + { + // (showDocumentCreateSheet.show = true) + }} + bind:currentPage + nextPageTriggerOffset={2} + jumpToPageNumber={jumpToPageReactive} + loadingMore={$paginatedDocumentsLoading} + itemsPerPage={SPREADSHEET_PAGE_LIMIT} + loadNextPage={loadPage} + loadPreviousPage={loadPage} + goToPage={handleGoToPage} + bottomActionTooltip={{ + text: 'Create row', + placement: 'top-end' + }}> + + {#each $collectionColumns as column (column.id)} + + {#if !column.isAction} + + + {column.title} + + + {/if} + + {/each} + + + + {@const document = $paginatedDocuments.getItemAtVirtualIndex(index)} + {#if document === null} + + {#each $collectionColumns as col} + + {/each} + + {:else} + + {/if} + + + + + + + + {selectedDocuments.length + ? `${selectedDocuments.length} document${selectedDocuments.length === 1 ? '' : 's'} selected` + : `${formatNumberWithCommas($documents.total)} document${$documents.total === 1 ? '' : 's'}`} + + + +
+
+ + + Page + + ({ + label: `${i + 1}`, + value: i + 1 + }))} + on:change={(e) => (jumpToPageReactive = Number(e.detail))} /> + + + out of {totalPages} + + +
+ + {#if !$isSmallViewport} +
+ { + $randomDataModalState.show = true; + }}>Generate sample data +
+ {/if} +
+
+
+ {/key} + + {#snippet noSqlEditor()} + { + console.log(value); + }} /> + {/snippet} + + {#if selectedDocuments.length > 0} +
+ + +
+ + + + {selectedDocuments.length > 1 ? 'documents' : 'document'} + selected + + +
+
+ + (selectedDocuments = [])} + >Cancel + (showDelete = true)} + >Delete + +
+
+ {/if} +
+ + + {@const isSingle = selectedDocumentForDelete !== null} + +

+ {#if isSingle} + Are you sure you want to delete this row from {collection.name}? + {:else} + Are you sure you want to delete {selectedDocuments.length} + {selectedDocuments.length > 1 ? 'documents' : 'document'} from {collection.name}? + {/if} +

+ +

This action is irreversible.

+
+ + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts index 9d3d04459d..f92a024838 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts @@ -1,5 +1,24 @@ import { page } from '$app/stores'; -import { derived } from 'svelte/store'; +import { derived, writable } from 'svelte/store'; import type { Models } from '@appwrite.io/console'; +import { SPREADSHEET_PAGE_LIMIT } from '$lib/constants'; +import { createSparsePagedDataStore } from '@appwrite.io/pink-svelte'; +import type { Column } from '$lib/helpers/types'; +import type { SortState } from '$database/store'; export const indexes = derived(page, ($page) => $page.data.collection.indexes as Models.Index[]); + +export const spreadsheetRenderKey = writable('initial'); + +export const collectionColumns = writable([]); +export const isCollectionsCsvImportInProgress = writable(false); + +export const spreadsheetLoading = writable(false); +export const paginatedDocumentsLoading = writable(false); +export const paginatedDocuments = + createSparsePagedDataStore(SPREADSHEET_PAGE_LIMIT); + +export const sortState = writable({ + column: null, + direction: 'default' +}); 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 52705eedce..de35ac8e91 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 @@ -6,6 +6,8 @@ import type { Page } from '@sveltejs/kit'; import { type Models, Query } from '@appwrite.io/console'; import type { Entity, Field } from '$database/(entity)'; import { isRelationship } from '$database/table-[table]/rows/store'; +import type { TagValue } from '$lib/components/filters/store'; +import type { SortDirection } from '$lib/components'; export type Columns = | Models.ColumnBoolean @@ -43,6 +45,17 @@ export type Table = Omit & { columns: Array; }; +export type SortState = { + column?: string; + direction: SortDirection; +}; + +export type RandomDataSchema = { + show: boolean; + value: number; + onSubmit?: () => Promise | void; +}; + export const expandTabs = writable(null); export const showCreateEntity = writable(false); @@ -77,6 +90,11 @@ export const databaseSubNavigationItems = [ { title: 'Settings', href: 'settings', icon: IconCog } ]; +export const randomDataModalState = writable({ + show: false, + value: 25 // initial value! +}); + export function buildEntityRoute(page: Page, entityType: string, entityId: string): string { return withPath( resolveRoute( @@ -99,3 +117,39 @@ export function buildWildcardEntitiesQuery(entity: Entity | null = null): string Query.select(['*']) ]; } + +export function extractSortFromQueries(parsedQueries: Map) { + for (const [tagValue, queryString] of parsedQueries.entries()) { + if (queryString.includes('orderAsc') || queryString.includes('orderDesc')) { + const isAsc = queryString.includes('orderAsc'); + return { + column: tagValue.value, + direction: isAsc ? 'asc' : 'desc' + }; + } + } + + return { column: null, direction: 'default' }; +} + +export function buildGridQueries( + limit: number, + offset: number, + parsedQueries: Map, + table: Entity +) { + const hasOrderQuery = Array.from(parsedQueries.values()).some( + (q) => q.includes('orderAsc') || q.includes('orderDesc') + ); + + const queryArray = [Query.limit(limit), Query.offset(offset)]; + + // don't override if there's a user created sort! + if (!hasOrderQuery) { + queryArray.push(Query.orderDesc('')); + } + + queryArray.push(...parsedQueries.values(), ...buildWildcardEntitiesQuery(table)); + + return queryArray; +} 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 a901afaf4e..d7a7c951b6 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 @@ -26,7 +26,6 @@ columnsOrder, databaseColumnSheetOptions, databaseRowSheetOptions, - randomDataModalState, showCreateColumnSheet, showCreateIndexSheet, spreadsheetLoading, @@ -39,7 +38,6 @@ import { addSubPanel, registerCommands, updateCommandGroupRanks } from '$lib/commandCenter'; import CreateColumn from './createColumn.svelte'; import { CreateColumnPanel } from '$lib/commandCenter/panels'; - import { showCreateEntity } from '../store'; import { project } from '../../../store'; import { page } from '$app/state'; import { canWriteTables } from '$lib/stores/roles'; @@ -50,8 +48,7 @@ import EditColumn from './columns/edit.svelte'; import RowActivity from './rows/activity.svelte'; import EditRowPermissions from './rows/editPermissions.svelte'; - import { Dialog, Layout, Typography, Selector } from '@appwrite.io/pink-svelte'; - import { Button, Seekbar } from '$lib/elements/forms'; + import { Layout, Selector } from '@appwrite.io/pink-svelte'; import { generateFakeRecords, generateColumns } from '$lib/helpers/faker'; import { addNotification } from '$lib/stores/notifications'; import { sleep } from '$lib/helpers/promises'; @@ -61,7 +58,7 @@ import { chunks } from '$lib/helpers/array'; import { Submit, trackEvent } from '$lib/actions/analytics'; - import { expandTabs, type Columns } from '../store'; + import { expandTabs, randomDataModalState } from '../store'; import type { LayoutData } from './$types'; @@ -96,6 +93,9 @@ onMount(() => { expandTabs.set(preferences.getKey('entityHeaderExpanded', true)); + // set faker method. + $randomDataModalState.onSubmit = async () => await createFakeData(); + return realtime .forProject(page.params.region, page.params.project) .subscribe(['project', 'console'], (response) => { @@ -256,14 +256,12 @@ $spreadsheetLoading = true; $randomDataModalState.show = false; - let columns: Columns[] = []; + let columns: Field[] = []; const currentFields = table.fields; const hasAnyRelationships = currentFields.some((field: Field) => isRelationship(field)); - const filteredColumns = currentFields.filter( - (field: Field) => field.type !== 'relationship' - ); + columns = currentFields.filter((field: Field) => field.type !== 'relationship'); - if (!filteredColumns.length) { + if (!columns.length) { try { columns = await generateColumns($project, page.params.database, page.params.table); @@ -284,13 +282,17 @@ let rowIds = []; try { - const { rows, ids } = generateFakeRecords(columns, $randomDataModalState.value); + const { records, ids } = generateFakeRecords( + $randomDataModalState.value, + 'tablesdb', + columns + ); rowIds = ids; const tablesSDK = sdk.forProject(page.params.region, page.params.project).tablesDB; if (hasAnyRelationships) { - for (const batch of chunks(rows)) { + for (const batch of chunks(records)) { try { await Promise.all( batch.map((row) => @@ -310,7 +312,7 @@ await tablesSDK.createRows({ databaseId: page.params.database, tableId: page.params.table, - rows + rows: records }); } @@ -479,22 +481,4 @@ - - - - Select how many sample rows to generate for testing. This won't delete or replace any - existing rows. - - - - - - - - - - - - - diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte index 3f2db4fc1d..7b5984c1e9 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte @@ -10,10 +10,9 @@ import type { PageData } from './$types'; import { tableColumns, - isCsvImportInProgress, + isTablesCsvImportInProgress, showRowCreateSheet, showCreateColumnSheet, - randomDataModalState, columnsOrder } from './store'; import SpreadSheet from './spreadsheet.svelte'; @@ -32,7 +31,7 @@ import { columnOptions } from './columns/store'; import { EmptySheet, type Field } from '$database/(entity)'; import { Empty as SuggestionsEmptySheet, tableColumnSuggestions } from '../(suggestions)'; - import { expandTabs } from '$database/store'; + import { expandTabs, randomDataModalState } from '$database/store'; export let data: PageData; @@ -89,7 +88,7 @@ $tableColumnSuggestions.table.id === page.params.table; async function onSelect(file: Models.File, localFile = false) { - $isCsvImportInProgress = true; + $isTablesCsvImportInProgress = true; try { await sdk @@ -114,7 +113,7 @@ message: e.message }); } finally { - $isCsvImportInProgress = false; + $isTablesCsvImportInProgress = false; } } @@ -204,7 +203,7 @@ {:else if $hasPageQueries} {:else} {:else} { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.ts index 157abc17f6..7ccffdf996 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.ts @@ -1,12 +1,9 @@ import { Dependencies, SPREADSHEET_PAGE_LIMIT } from '$lib/constants'; import { getLimit, getPage, getQuery, getView, pageToOffset, View } from '$lib/helpers/load'; import { sdk } from '$lib/stores/sdk'; -import { Query } from '@appwrite.io/console'; import type { PageLoad } from './$types'; import { queries, queryParamToMap } from '$lib/components/filters'; -import { buildWildcardEntitiesQuery } from '$database/store'; -import type { TagValue } from '$lib/components/filters/store'; -import type { Entity } from '$database/(entity)'; +import { buildGridQueries, extractSortFromQueries } from '$database/store'; export const load: PageLoad = async ({ params, depends, url, route, parent }) => { const { table } = await parent(); @@ -38,39 +35,3 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) => }) }; }; - -function extractSortFromQueries(parsedQueries: Map) { - for (const [tagValue, queryString] of parsedQueries.entries()) { - if (queryString.includes('orderAsc') || queryString.includes('orderDesc')) { - const isAsc = queryString.includes('orderAsc'); - return { - column: tagValue.value, - direction: isAsc ? 'asc' : 'desc' - }; - } - } - - return { column: null, direction: 'default' }; -} - -function buildGridQueries( - limit: number, - offset: number, - parsedQueries: Map, - table: Entity -) { - const hasOrderQuery = Array.from(parsedQueries.values()).some( - (q) => q.includes('orderAsc') || q.includes('orderDesc') - ); - - const queryArray = [Query.limit(limit), Query.offset(offset)]; - - // don't override if there's a user created sort! - if (!hasOrderQuery) { - queryArray.push(Query.orderDesc('')); - } - - queryArray.push(...parsedQueries.values(), ...buildWildcardEntitiesQuery(table)); - - return queryArray; -} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/+page.svelte index 182dfb2982..f50d013d76 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/+page.svelte @@ -21,7 +21,7 @@ type Columns, type ColumnsWidth, indexes, - isCsvImportInProgress, + isTablesCsvImportInProgress, reorderItems, showCreateIndexSheet } from '../store'; @@ -282,7 +282,7 @@ + + + +
{/snippet} @@ -331,16 +277,42 @@ {#snippet selectDatabaseType()} {#each databaseTypes as databaseType} - - {databaseType.subtitle} - +
+ + {databaseType.subtitle} + +
{/each}
{/snippet} + + From 4076f5f1ae4be5c92bb01a668261a35904676fc6 Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 17 Oct 2025 13:52:47 +0530 Subject: [PATCH 08/60] add: images. update: db page to svelte5. --- .../database-[database]/+page.svelte | 50 ++++++++----- .../collection-[collection]/+page.svelte | 8 +-- static/images/empty-documents-db-dark.svg | 71 +++++++++++++++++++ static/images/empty-documents-db-light.svg | 60 ++++++++++++++++ 4 files changed, 165 insertions(+), 24 deletions(-) create mode 100644 static/images/empty-documents-db-dark.svg create mode 100644 static/images/empty-documents-db-light.svg 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 e9bf52da23..9098ef271a 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 @@ -31,8 +31,10 @@ return columns; }); + // TODO: get proper images for documentsDB function getImageRoute(type: 'light' | 'dark'): string { - return withPath(resolveRoute('/'), `/images/empty-database-${type}.svg`); + const base = terminology.type === 'documentsdb' ? 'empty-documents-db' : 'empty-database'; + return withPath(resolveRoute('/'), `/images/${base}-${type}.svg`); } const emptyPageText = $derived.by(() => { @@ -94,26 +96,36 @@ {:else} - - {emptyPageText} +
+ + {emptyPageText} - - + + - {#if $canWriteTables} - - {/if} - - + {#if $canWriteTables} + + {/if} + + +
{/if} + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte index 4d2ee9a4ce..32e703f02e 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte @@ -4,7 +4,7 @@ import { Container } from '$lib/layout'; import { preferences } from '$lib/stores/preferences'; import { Icon, Layout, Divider } from '@appwrite.io/pink-svelte'; - import type { PageData } from './$types'; + import type { PageProps } from './$types'; import FilePicker from '$lib/components/filePicker.svelte'; import { page } from '$app/state'; import { sdk } from '$lib/stores/sdk'; @@ -19,11 +19,9 @@ import { canWriteRows } from '$lib/stores/roles'; import SpreadSheet from './spreadsheet.svelte'; - export let data: PageData; + const { data }: PageProps = $props(); - $: collection = data.collection; - - let showImportCSV = false; + let showImportCSV = $state(false); async function onSelect(file: Models.File, localFile = false) { $isCollectionsCsvImportInProgress = true; diff --git a/static/images/empty-documents-db-dark.svg b/static/images/empty-documents-db-dark.svg new file mode 100644 index 0000000000..22c529c9b2 --- /dev/null +++ b/static/images/empty-documents-db-dark.svg @@ -0,0 +1,71 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/static/images/empty-documents-db-light.svg b/static/images/empty-documents-db-light.svg new file mode 100644 index 0000000000..8eabf996ea --- /dev/null +++ b/static/images/empty-documents-db-light.svg @@ -0,0 +1,60 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
From 546f1cb1bf8f9e81e9d8c1704ef5655693d4d851 Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 17 Oct 2025 16:08:51 +0530 Subject: [PATCH 09/60] =?UTF-8?q?feat:=20skeletons=20in=20json=20editor=20?= =?UTF-8?q?=F0=9F=92=AA=20optimize:=20imports,=20singular=20sources=20of?= =?UTF-8?q?=20truth.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/components/csvImportBox.svelte | 2 +- src/lib/components/sortButton.svelte | 4 +- .../(entity)/views/layouts/empty.svelte | 14 +- .../(entity)/views/layouts/spreadsheet.svelte | 30 +++-- .../(suggestions)/indexes.svelte | 2 +- .../(components)/editor/helpers/constants.ts | 12 ++ .../(components)/editor/view.svelte | 126 ++++++++++++++---- .../collection-[collection]/+layout.svelte | 8 +- .../collection-[collection]/+page.svelte | 1 + .../spreadsheet.svelte | 9 +- .../collection-[collection]/store.ts | 3 - .../databases/database-[database]/store.ts | 4 + .../table-[table]/+layout.svelte | 10 +- .../table-[table]/columns/+page.svelte | 2 +- .../table-[table]/columns/deleteColumn.svelte | 2 +- .../table-[table]/columns/edit.svelte | 3 +- .../table-[table]/columns/store.ts | 2 +- .../table-[table]/createColumn.svelte | 3 +- .../table-[table]/rows/cell/edit.svelte | 2 +- .../rows/columns/columnForm.svelte | 6 +- .../rows/columns/columnItem.svelte | 2 +- .../table-[table]/rows/create.svelte | 4 +- .../table-[table]/spreadsheet.svelte | 6 +- .../table-[table]/store.ts | 4 - 24 files changed, 182 insertions(+), 79 deletions(-) diff --git a/src/lib/components/csvImportBox.svelte b/src/lib/components/csvImportBox.svelte index ec7a2c9a3e..9910e193f7 100644 --- a/src/lib/components/csvImportBox.svelte +++ b/src/lib/components/csvImportBox.svelte @@ -13,7 +13,7 @@ // re-render the key for sheet UI. import { hash } from '$lib/helpers/string'; - import { spreadsheetRenderKey } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store'; + import { spreadsheetRenderKey } from '$database/store'; type ImportItem = { status: string; diff --git a/src/lib/components/sortButton.svelte b/src/lib/components/sortButton.svelte index d202cf664b..bb24a2bb24 100644 --- a/src/lib/components/sortButton.svelte +++ b/src/lib/components/sortButton.svelte @@ -5,10 +5,10 @@ @@ -40,8 +40,8 @@ import type { Text } from '@codemirror/state'; import { onMount, onDestroy } from 'svelte'; import Id, { truncateId } from '$lib/components/id.svelte'; - import { Icon, Layout, Skeleton, Tooltip, Typography } from '@appwrite.io/pink-svelte'; - import { IconDuplicate } from '@appwrite.io/pink-icons-svelte'; + import { Icon, Layout, Skeleton, Spinner, Tooltip, Typography } from '@appwrite.io/pink-svelte'; + import { IconCheck, IconDuplicate } from '@appwrite.io/pink-icons-svelte'; import { Button } from '$lib/elements/forms'; import { copy } from '$lib/helpers/copy'; import { isSmallViewport } from '$lib/stores/viewport'; @@ -55,7 +55,6 @@ SYSTEM_KEYS, DEBOUNCE_DELAY, LINTER_DELAY, - UNQUOTED_KEY_REGEX, INDENT_REGEX, SCALAR_VALUE_REGEX, TRAILING_COMMA_REGEX, @@ -66,19 +65,28 @@ getIndent } from './helpers/constants'; import { toLocaleDateTime } from '$lib/helpers/date'; + import { ID } from '@appwrite.io/console'; + import { areObjectsSame } from '$lib/helpers/object'; + import { sleep } from '$lib/helpers/promises'; interface Props { + isNew?: boolean; data?: JsonValue; + isSaving?: boolean; loading?: boolean; - onchange?: (newData: JsonValue) => void; + onChange?: (newData: JsonValue) => Promise | void; + onSave?: (newData: JsonValue) => Promise | void; readonly?: boolean; wrapLines?: boolean; errorInPlace?: boolean; } let { + isNew = false, data = $bindable(), - onchange, + onChange, + onSave, + isSaving = $bindable(false), loading = false, readonly = false, wrapLines = true, @@ -89,7 +97,7 @@ let editorView: EditorView | null = null; let errorMessage = $state(null); - let changeTimer: number | null = null; // debounce timer for parse + onchange + let changeTimer: ReturnType | null = null; // debounce timer for parse + onChange let pendingCanonicalize = false; // set when a full-document replace (paste-all) occurs let lastExpectedContent = ''; // track latest serialized data to avoid spurious rewrites let lastDocId: string | null = null; // track current document identity for history reset @@ -100,16 +108,24 @@ let tooltipMessage = $state('Copy document'); // Store the original data to preserve system values - let originalData = $state(data); + let originalData = $state($state.snapshot(data)); + + // Check for enable, disable save button. + const hasDataChanged = $derived(!areObjectsSame(data, originalData)); - // Track if we're currently updating from editor to prevent loops let isUpdatingFromEditor = false; + // Track previous isNew state to detect transitions + let wasNew = isNew; + + // Generate a stable ID once for new documents + const generatedId = ID.unique(); + // Get $id from data const documentId = $derived( - data && typeof data === 'object' && !Array.isArray(data) && '$id' in data + data && typeof data === 'object' && !Array.isArray(data) && '$id' in data && data.$id ? String(data.$id) - : null + : generatedId ); // Convert data to formatted JavaScript object notation (no quotes on keys) @@ -200,7 +216,11 @@ } // Find ranges of system keys (lines starting with $id, $createdAt, $updatedAt) + // When isNew=true, skip all readonly range detection since we don't have timestamps yet function findReadOnlyRanges(doc: Text): Array<{ from: number; to: number }> { + // When creating a new document, allow editing everything + if (isNew) return []; + const ranges: Array<{ from: number; to: number }> = []; let found = 0; @@ -234,12 +254,15 @@ const originalObj = originalData as JsonObject; // Restore only the editor-visible system fields from the original document - if (originalObj.$id !== undefined) { + // Skip $id preservation when creating a new document to allow user edits + if (!isNew && originalObj.$id !== undefined) { parsedObj.$id = originalObj.$id; } + if (originalObj.$createdAt !== undefined) { parsedObj.$createdAt = originalObj.$createdAt; } + if (originalObj.$updatedAt !== undefined) { parsedObj.$updatedAt = originalObj.$updatedAt; } @@ -635,27 +658,12 @@ ); // Safe parse variant that indicates success without mutating editor on failure - function tryParseEditorContent(content: string): { ok: boolean; value: JsonValue } { - // 1) Strict JSON + async function tryParseEditorContent( + content: string + ): Promise<{ ok: boolean; value: JsonValue }> { try { - return { ok: true, value: JSON.parse(content) }; - } catch { - /* empty */ - } - - // 2) JSON with unquoted keys - try { - const withQuotedKeys = content.replace(UNQUOTED_KEY_REGEX, '$1"$2":'); - return { ok: true, value: JSON.parse(withQuotedKeys) }; - } catch { - /* empty */ - } - - // 3) JSON with unquoted keys and trailing commas removed - try { - const withQuotedKeys = content.replace(UNQUOTED_KEY_REGEX, '$1"$2":'); - const noTrailingCommas = withQuotedKeys.replace(/,\s*([}\]])/g, '$1'); - return { ok: true, value: JSON.parse(noTrailingCommas) }; + const value = await parse(content); + return { ok: true, value }; } catch { /* empty */ } @@ -742,15 +750,16 @@ } }); - // Debounce parse + onchange work + // Debounce parse + onChange work if (changeTimer) { clearTimeout(changeTimer); changeTimer = null; } - changeTimer = window.setTimeout(() => { + + changeTimer = setTimeout(async () => { const state = update.view.state; const newContent = state.doc.toString(); - const res = tryParseEditorContent(newContent); + const res = await tryParseEditorContent(newContent); if (!res.ok) { return; // linter will surface the error } @@ -764,7 +773,7 @@ } data = parsed; - onchange?.(parsed); + onChange?.(parsed); lastExpectedContent = dataToString(parsed); }, DEBOUNCE_DELAY); }), @@ -791,28 +800,44 @@ editorView = null; }); + // Reset originalData when transitioning to new document mode + $effect(() => { + if (isNew && !wasNew) { + originalData = $state.snapshot(data); + } + wasNew = isNew; + }); + // Update originalData and editor when data or document changes externally $effect(() => { if (!editorView) return; - // Capture new system values from the external document - originalData = data; - - // Detect document switch and reset history/state entirely + // Detect document switch if (documentId !== lastDocId) { lastDocId = documentId; - const expected = dataToString(data); - lastExpectedContent = expected; - if (changeTimer) { - clearTimeout(changeTimer); - changeTimer = null; + + // For existing documents only: + // capture snapshot and reset editor state/history + if (!isNew) { + // Capture original data snapshot when switching documents + originalData = $state.snapshot(data); + const expected = dataToString(data); + + lastExpectedContent = expected; + + if (changeTimer) { + clearTimeout(changeTimer); + changeTimer = null; + } + + pendingCanonicalize = false; + isUpdatingFromEditor = true; + + const newState = EditorState.create({ doc: expected, extensions: baseExtensions }); + editorView.setState(newState); + queueMicrotask(() => (isUpdatingFromEditor = false)); + return; } - pendingCanonicalize = false; - isUpdatingFromEditor = true; - const newState = EditorState.create({ doc: expected, extensions: baseExtensions }); - editorView.setState(newState); - queueMicrotask(() => (isUpdatingFromEditor = false)); - return; } // Only react when the external data actually changed @@ -868,22 +893,62 @@ {/if} {#if documentId} - - - - {tooltipMessage} - + + + + + Save + + + + + + {tooltipMessage} + + {/if} {/if}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte index 1803a53a3b..255f335630 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte @@ -15,7 +15,7 @@ import type { Models } from '@appwrite.io/console'; import { expandTabs, randomDataModalState } from '$database/store'; import { EmptySheet } from '$database/(entity)'; - import { isCollectionsCsvImportInProgress } from './store'; + import { isCollectionsCsvImportInProgress, noSqlDocument } from './store'; import { canWriteRows } from '$lib/stores/roles'; import SpreadSheet from './spreadsheet.svelte'; @@ -66,7 +66,20 @@ Import CSV {#if !$isSmallViewport} - diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index bb82459515..106ce64861 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -40,6 +40,7 @@ import { expandTabs, buildWildcardEntitiesQuery } from '$database/store'; import { collectionColumns, + noSqlDocument, paginatedDocuments, paginatedDocumentsLoading, sortState @@ -50,7 +51,7 @@ spreadsheetRenderKey, spreadsheetLoading } from '$database/store'; - import { NoSqlEditor, type JsonValue } from './(components)/editor'; + import { type JsonValue, NoSqlEditor } from './(components)/editor'; export let data: PageData; @@ -64,14 +65,6 @@ const databaseId = page.params.database; const collectionId = page.params.collection; - let jsonEditorDocument = writable<{ - show: boolean; - document?: Models.Document; - }>({ - show: false, - document: null - }); - const emptyCellsLimit = $spreadsheetLoading ? 30 : $isSmallViewport @@ -104,7 +97,7 @@ const firstDocument = $documents?.documents?.[0]; if (firstDocument) { - $jsonEditorDocument.document = firstDocument; + $noSqlDocument.document = firstDocument; } }); @@ -325,32 +318,72 @@ } // possibly for auto-save! - async function updateDocumentContents(document: Models.Document) { + async function createOrUpdateDocument(jsonValue: JsonValue) { + const document = jsonValue as Models.Document; + const documentsDB = sdk.forProject(page.params.region, page.params.project).documentsDB; + + /** + * remove dates because + * console can override timestamps! + */ + const { $createdAt, $updatedAt, $id, ...documentWithoutDates } = document; + try { - await sdk - .forProject(page.params.region, page.params.project) - .documentsDB.updateDocument({ + if ($noSqlDocument.isNew) { + // create + await documentsDB.createDocument({ databaseId, collectionId, - documentId: document.$id, - data: document, - permissions: document.$permissions + documentId: $id, + data: documentWithoutDates ?? [] }); - invalidate(Dependencies.DOCUMENT); - trackEvent(Submit.DocumentUpdate); - addNotification({ - message: 'Document has been updated', - type: 'success' - }); - return true; + await invalidate(Dependencies.DOCUMENTS); + trackEvent(Submit.DocumentCreate); + addNotification({ + message: 'Document has been created', + type: 'success' + }); + + noSqlDocument.update(() => { + return { + isNew: false, + show: false, + document: {} + }; + }); + + spreadsheetRenderKey; + } else { + // update + await documentsDB.updateDocument({ + databaseId, + collectionId, + documentId: $id, + data: documentWithoutDates, + permissions: document.$permissions ?? [] + }); + + await invalidate(Dependencies.DOCUMENT); + trackEvent(Submit.DocumentUpdate); + addNotification({ + message: 'Document has been updated', + type: 'success' + }); + } + + // re-render spreadsheet! + spreadsheetRenderKey.set(hash($id)); + const firstDocument = $documents?.documents?.[0]; + if (firstDocument) { + $noSqlDocument.document = firstDocument; + } } catch (error) { addNotification({ message: error.message, type: 'error' }); trackError(error, Submit.DocumentUpdatePermissions); - return false; } } @@ -436,7 +469,7 @@ + bind:showEditorSideSheet={$noSqlDocument.show}> {#key $spreadsheetRenderKey} { - $jsonEditorDocument.show = true; - $jsonEditorDocument.document = document; + $noSqlDocument.show = true; + $noSqlDocument.isNew = false; + $noSqlDocument.document = document; }} style:cursor="pointer"> + isSelected={$noSqlDocument?.document?.$id === document.$id}> {#each $collectionColumns as { id: columnId } (columnId)} {#if columnId === '$id'} @@ -617,10 +651,9 @@ {#snippet noSqlEditor()} { - console.log(value); - }} /> + isNew={$noSqlDocument.isNew} + bind:data={$noSqlDocument.document} + onSave={async (document) => await createOrUpdateDocument(document)} /> {/snippet} {#if selectedDocuments.length > 0} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts index 4055094b0d..9f96cd489a 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts @@ -19,3 +19,13 @@ export const sortState = writable({ column: null, direction: 'default' }); + +export const noSqlDocument = writable<{ + show: boolean; + document?: Models.Document | object; + isNew?: boolean; +}>({ + show: false, + document: null, + isNew: false +}); From 5cdf6e55bbd58bb9bcdb4dc82447c8de2ba13944 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 18 Oct 2025 13:40:34 +0530 Subject: [PATCH 14/60] add: ctrl/cmd + s to save the document. --- .../(components)/editor/helpers/keymaps.ts | 19 ++++++- .../(components)/editor/view.svelte | 55 ++++++++++--------- .../spreadsheet.svelte | 1 + 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/keymaps.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/keymaps.ts index 46d4e1d842..5fa8123ed2 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/keymaps.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/keymaps.ts @@ -6,14 +6,29 @@ import { defaultKeymap, historyKeymap, indentLess, indentMore } from '@codemirro // main editor keymaps, // these require functions from the component export function createEditorKeymaps( - insertNewlineKeepIndent: (view: EditorView) => boolean + insertNewlineKeepIndent: (view: EditorView) => boolean, + onSave?: () => Promise | void ): KeyBinding[] { - return [ + const keymaps: KeyBinding[] = [ { key: 'Tab', run: indentMore }, { key: 'Enter', run: insertNewlineKeepIndent }, { key: 'Shift-Enter', run: insertNewlineKeepIndent }, { key: 'Shift-Tab', run: indentLess } ]; + + // Add Cmd/Ctrl+S save shortcut if save handler is provided + if (onSave) { + keymaps.push({ + key: 'Mod-s', + preventDefault: true, + run: () => { + onSave(); + return true; + } + }); + } + + return keymaps; } // Secondary keymaps - these are standard CodeMirror keymaps diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte index fa20bf5d38..4b89132b19 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte @@ -79,6 +79,7 @@ readonly?: boolean; wrapLines?: boolean; errorInPlace?: boolean; + ctrlSave?: boolean; } let { @@ -90,7 +91,8 @@ loading = false, readonly = false, wrapLines = true, - errorInPlace = true + errorInPlace = true, + ctrlSave = false }: Props = $props(); let editorContainer: HTMLDivElement = $state(null); @@ -119,7 +121,7 @@ let wasNew = isNew; // Generate a stable ID once for new documents - const generatedId = ID.unique(); + let generatedId = $state(ID.unique()); // Get $id from data const documentId = $derived( @@ -694,6 +696,25 @@ } } + // Handle save logic - called from both button and keyboard shortcut + async function handleSave(): Promise { + if (!hasDataChanged) return; + + isSaving = true; + + let dataToSave = data; + if (isNew && typeof data === 'object' && data !== null && !Array.isArray(data)) { + const dataObj = data; + if (!dataObj['$id']) { + dataToSave = { $id: generatedId, ...dataObj }; + } + } + + await sleep(2500); + await onSave?.(dataToSave); + isSaving = false; + } + onMount(() => { if (!editorContainer) return; @@ -729,7 +750,9 @@ } }), // Override Enter and Shift-Enter to keep current indent, no extra +indentUnit - keymap.of(createEditorKeymaps(insertNewlineKeepIndent)), + keymap.of( + createEditorKeymaps(insertNewlineKeepIndent, ctrlSave ? handleSave : undefined) + ), keymap.of(secondaryKeymaps), javascript(), customSyntaxHighlighting, @@ -804,6 +827,7 @@ $effect(() => { if (isNew && !wasNew) { originalData = $state.snapshot(data); + generatedId = ID.unique(); } wasNew = isNew; }); @@ -857,10 +881,11 @@ }); // React to read-only prop changes via Compartment reconfigure + // Also make editor read-only while saving to prevent concurrent edits $effect(() => { if (!editorView) return; editorView.dispatch({ - effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(readonly)) + effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(readonly || isSaving)) }); }); @@ -901,27 +926,7 @@ size="xs" disabled={!hasDataChanged} class="icon-button" - on:click={async () => { - isSaving = true; - - // For new documents, ensure $id is added to data before saving - let dataToSave = data; - if ( - isNew && - typeof data === 'object' && - data !== null && - !Array.isArray(data) - ) { - const dataObj = data; - if (!dataObj['$id']) { - dataToSave = { $id: generatedId, ...dataObj }; - } - } - - await sleep(2500); - await onSave?.(dataToSave); - isSaving = false; - }}> + on:click={handleSave}> {#if isSaving} {:else} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index 106ce64861..0ad75696c7 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -651,6 +651,7 @@ {#snippet noSqlEditor()} await createOrUpdateDocument(document)} /> From dd2f8fa9af32d612eea8c9a39cc8dc09a2388a90 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 18 Oct 2025 13:56:12 +0530 Subject: [PATCH 15/60] add: partial options support. fix: height issues on mobile. --- .../(entity)/views/layouts/spreadsheet.svelte | 6 +- .../(components)/editor/view.svelte | 2 - .../spreadsheet.svelte | 61 ++++++++++++------- .../table-[table]/+layout.svelte | 7 ++- .../table-[table]/rows/store.ts | 4 +- .../table-[table]/sheetOptions.svelte | 2 +- .../table-[table]/spreadsheet.svelte | 4 +- 7 files changed, 52 insertions(+), 34 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/spreadsheet.svelte index f2a32167a8..89b385be69 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/spreadsheet.svelte @@ -125,9 +125,11 @@ class:has-json-editor={typeof noSqlEditor !== 'undefined'}> {@render children()} -
+
{#if !$isSmallViewport} - {@render noSqlEditor?.()} +
+ {@render noSqlEditor?.()} +
{:else} {:else if columnId === 'actions'} - - - - - - - - - - - - - - + { + onSelectSheetOption(option, document); + }} + onVisibilityChanged={(visible) => { + canShowDatetimePopover = !visible; + }}> + + {#snippet children(toggle)} + + + + {/snippet} + {/if} {/each} 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 ec49443cd2..f88e0d99f5 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 @@ -52,7 +52,7 @@ import { sleep } from '$lib/helpers/promises'; import { hash } from '$lib/helpers/string'; import { preferences } from '$lib/stores/preferences'; - import { buildRowUrl, isRelationship } from './rows/store'; + import { buildFieldUrl, isRelationship } from './rows/store'; import { chunks } from '$lib/helpers/array'; import { Submit, trackEvent } from '$lib/actions/analytics'; import { @@ -413,7 +413,10 @@ mode: 'copy-tag', text: 'Row URL', show: !!($databaseRowSheetOptions.rowId ?? $databaseRowSheetOptions.row?.$id), - value: buildRowUrl($databaseRowSheetOptions.rowId ?? $databaseRowSheetOptions.row?.$id) + value: buildFieldUrl( + 'row', + $databaseRowSheetOptions.rowId ?? $databaseRowSheetOptions.row?.$id + ) }}> void; 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 11fc82d47b..b3a436c3a1 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 @@ -12,7 +12,7 @@ import { type ComponentType, onDestroy, onMount } from 'svelte'; import type { PageData } from './$types'; import { - buildRowUrl, + buildFieldUrl, isRelationship, isRelationshipToMany, isSpatialType, @@ -562,7 +562,7 @@ if (action === 'copy-url') { try { - await copy(buildRowUrl(row.$id)); + await copy(buildFieldUrl('row', row.$id)); addNotification({ type: 'success', message: 'Row url copied' From abbc0ae76080819dd5659a7d7ad82880cd92e5cb Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 18 Oct 2025 15:25:40 +0530 Subject: [PATCH 16/60] update: handle document/row direct url hits. --- .../database-[database]/[...rest]/+page.ts | 50 +++++++ .../(components)/editor/view.svelte | 137 ++++++++++-------- .../spreadsheet.svelte | 61 ++++++-- .../collection-[collection]/store.ts | 6 +- .../table-[table]/+layout.svelte | 2 +- 5 files changed, 178 insertions(+), 78 deletions(-) create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/[...rest]/+page.ts diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/[...rest]/+page.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/[...rest]/+page.ts new file mode 100644 index 0000000000..344f772e90 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/[...rest]/+page.ts @@ -0,0 +1,50 @@ +import type { PageLoad } from './$types'; +import { redirect } from '@sveltejs/kit'; +import { AppwriteException } from '@appwrite.io/console'; +import { databaseRowSheetOptions } from '../table-[table]/store'; +import { noSqlDocument } from '../collection-[collection]/store'; +import { resolveRoute } from '$lib/stores/navigation'; + +export const load: PageLoad = async ({ params, url }) => { + const restSegments = params.rest ? params.rest.split('/').filter(Boolean) : []; + const baseUrl = resolveRoute( + '/(console)/project-[region]-[project]/databases/database-[database]', + params + ); + + if (restSegments.length === 0) { + throw new AppwriteException('Not Found', 404); + } + + const lastSegment = restSegments[restSegments.length - 1]; + + const rowMatch = lastSegment.match(/^row-([^/]+)$/); + if (rowMatch) { + const rowId = rowMatch[1]; + databaseRowSheetOptions.update((options) => ({ + ...options, + rowId, + show: true, + title: 'Update row' + })); + + const parentSegments = restSegments.slice(0, -1); + const newPath = `${baseUrl}/${parentSegments.join('/')}`; + redirect(308, newPath + url.search); + } + + const documentMatch = lastSegment.match(/^document-([^/]+)$/); + if (documentMatch) { + const documentId = documentMatch[1]; + noSqlDocument.update((options) => ({ + ...options, + documentId + })); + + const parentSegments = restSegments.slice(0, -1); + const newPath = `${baseUrl}/${parentSegments.join('/')}`; + redirect(308, newPath + url.search); + } + + throw new AppwriteException('Not Found', 404); +}; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte index 2f39b71862..d664fbef2f 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte @@ -896,63 +896,64 @@ }); -
+
{#if loading} - {:else} - - {#if documentId} -
- {truncateId(documentId)} -
- {/if} -
+ {/if} - {#if errorMessage && !$isSmallViewport} -
- {errorMessage} + + {#if documentId && !loading} +
+ {truncateId(documentId)}
{/if} +
- {#if documentId} - - - - - Save - - - - - - {tooltipMessage} - - - {/if} + {#if errorMessage && !$isSmallViewport} +
+ {errorMessage} +
+ {/if} + + {#if documentId} + + + + + Save + + + + + + {tooltipMessage} + + {/if}
@@ -966,7 +967,7 @@ {#if loading}
- {#each Array.from({ length: 16 }) as _, index (index)} + {#each Array.from({ length: $isSmallViewport ? 14 : 16 }) as _, index (index)}
{index + 1}
{/each}
@@ -984,33 +985,36 @@ {/each}
- {:else} -
{/if} + +
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/field/editPermissions.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/field/editPermissions.svelte index aebe3c95ad..5f42d2c86d 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/field/editPermissions.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/field/editPermissions.svelte @@ -3,19 +3,23 @@ import { sdk } from '$lib/stores/sdk'; import { invalidate } from '$app/navigation'; import { Alert } from '@appwrite.io/pink-svelte'; - import type { Models } from '@appwrite.io/console'; import { Permissions } from '$lib/components/permissions'; import { addNotification } from '$lib/stores/notifications'; import { symmetricDifference } from '$lib/helpers/array'; import { trackEvent, trackError } from '$lib/actions/analytics'; - import { type Entity, getTerminologies } from '$database/(entity)'; + import { + type Entity, + type Record, + getTerminologies, + toSupportiveRecord + } from '$database/(entity)'; let { entity, record = $bindable(null) }: { entity: Entity; - record: Models.DefaultDocument | Models.Document | Models.DefaultRow | Models.Row; + record: Record; } = $props(); let permissions = $state(record.$permissions); @@ -36,23 +40,21 @@ export async function updatePermissions() { try { - const { $databaseId: databaseId, $id: recordId } = record; + const { $databaseId: databaseId, $id: recordId, entityId } = toSupportiveRecord(record); if (terminology.type === 'documentsdb') { - const collectionId = (record as Models.Document).$collectionId; await sdk .forProject(page.params.region, page.params.project) .documentsDB.updateDocument({ databaseId, - collectionId, + collectionId: entityId, documentId: recordId, permissions }); } else { - const tableId = (record as Models.Row).$tableId; await sdk.forProject(page.params.region, page.params.project).tablesDB.updateRow({ databaseId, - tableId, + tableId: entityId, rowId: recordId, permissions }); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/field/index.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/field/index.ts index 5ad0a27296..640842e601 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/field/index.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/field/index.ts @@ -1,2 +1,3 @@ +export { default as RecordActivity } from './activity.svelte'; export { default as CsvDisabled } from './csvDisabled.svelte'; export { default as EditRecordPermissions } from './editPermissions.svelte'; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte index 079c2efaa3..72b98ebede 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte @@ -30,8 +30,8 @@ import { addNotification } from '$lib/stores/notifications'; import { sleep } from '$lib/helpers/promises'; import { hash } from '$lib/helpers/string'; - import { documentPermissionSheet } from './store'; - import { SideSheet, EditRecordPermissions } from '$database/(entity)'; + import { documentActivitySheet, documentPermissionSheet } from './store'; + import { SideSheet, EditRecordPermissions, RecordActivity } from '$database/(entity)'; export let data: LayoutData; @@ -241,3 +241,7 @@ bind:this={editRecordPermissions} bind:record={$documentPermissionSheet.document} /> + + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index 222bd3e81b..6b97a1e080 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -39,6 +39,7 @@ import { expandTabs, buildWildcardEntitiesQuery } from '$database/store'; import { collectionColumns, + documentActivitySheet, documentPermissionSheet, noSqlDocument, paginatedDocuments, @@ -358,8 +359,8 @@ } if (action === 'activity') { - // $rowActivitySheet.row = row; - // $rowActivitySheet.show = true; + $documentActivitySheet.show = true; + $documentActivitySheet.document = document; } } diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts index 68db665ce2..c4057c0181 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts @@ -1,10 +1,10 @@ import { page } from '$app/stores'; +import type { Column } from '$lib/helpers/types'; +import type { SortState } from '$database/store'; import { derived, writable } from 'svelte/store'; import type { Models } from '@appwrite.io/console'; import { SPREADSHEET_PAGE_LIMIT } from '$lib/constants'; import { createSparsePagedDataStore } from '@appwrite.io/pink-svelte'; -import type { Column } from '$lib/helpers/types'; -import type { SortState } from '$database/store'; export const indexes = derived(page, ($page) => $page.data.collection.indexes as Models.Index[]); @@ -38,3 +38,8 @@ export const documentPermissionSheet = writable({ show: false, document: null as Models.Document }); + +export const documentActivitySheet = writable({ + show: false, + document: null as Models.Document +}); 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 31e48b5069..a72ed6f761 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 @@ -43,7 +43,6 @@ import EditRow from './rows/edit.svelte'; import EditRelatedRow from './rows/editRelated.svelte'; import EditColumn from './columns/edit.svelte'; - import RowActivity from './rows/activity.svelte'; import { Layout, Selector } from '@appwrite.io/pink-svelte'; import { generateFakeRecords, generateColumns } from '$lib/helpers/faker'; import { addNotification } from '$lib/stores/notifications'; @@ -62,7 +61,13 @@ import type { LayoutData } from './$types'; - import { CreateIndex, EditRecordPermissions, type Field, SideSheet } from '$database/(entity)'; + import { + CreateIndex, + EditRecordPermissions, + type Field, + SideSheet, + RecordActivity + } from '$database/(entity)'; import { resolveRoute, withPath } from '$lib/stores/navigation'; import IndexesSuggestions from '../(suggestions)/indexes.svelte'; import { showIndexesSuggestions, tableColumnSuggestions } from '../(suggestions)'; @@ -484,7 +489,7 @@ - + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/activity.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/activity.svelte deleted file mode 100644 index d7d37451f1..0000000000 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/activity.svelte +++ /dev/null @@ -1,68 +0,0 @@ - - -{#if loading} -
- - -
-{:else if rowActivityLogs} -
- -
-{/if} - - diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts index 9fbbf26a4c..01c8c9d9b7 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts @@ -134,8 +134,6 @@ export enum Deletion { restrict = 'Row can not be deleted' } -export const scrollStore = writable(null); - export const rowActivitySheet = writable({ show: false, row: null as Models.Row From 83b60a0b7d16250669b0c6c98beccbf59dcce010 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 18 Oct 2025 18:02:58 +0530 Subject: [PATCH 21/60] fix: var name. --- .../database-[database]/(entity)/views/field/activity.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/field/activity.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/field/activity.svelte index 1bb6ab64f4..a9fc197224 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/field/activity.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/field/activity.svelte @@ -47,7 +47,7 @@ .tablesDB.listRowLogs({ databaseId: databaseId, tableId: entityId, - rowId: $recordId, + rowId: recordId, queries: [Query.limit(limit), Query.offset(offset)] }); } From 623da44f9b910f2523890f8436a5f0657849e6b6 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 18 Oct 2025 18:05:47 +0530 Subject: [PATCH 22/60] add: sort to headers. --- .../collection-[collection]/spreadsheet.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index 6b97a1e080..d13bf2068e 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -2,7 +2,7 @@ import { page } from '$app/state'; import { goto, invalidate } from '$app/navigation'; import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics'; - import { Confirm, Id } from '$lib/components'; + import { Confirm, Id, SortButton } from '$lib/components'; import { Dependencies, SPREADSHEET_PAGE_LIMIT } from '$lib/constants'; import { Button as ConsoleButton, InputSelect } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; @@ -231,6 +231,7 @@ spreadsheetContainer.restoreGridSheetScroll(); $spreadsheetLoading = false; + markFirstDocumentSelected(); } async function handleDelete() { @@ -559,6 +560,8 @@ {column.title} + + {/if} From a6bce82fc6019b12140da84bd888b3cca4cb654a Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 18 Oct 2025 18:08:39 +0530 Subject: [PATCH 23/60] fix: empty state on tablesdb. --- .../(entity)/views/layouts/spreadsheet.svelte | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/spreadsheet.svelte index 89b385be69..aac0d131e0 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/spreadsheet.svelte @@ -127,7 +127,7 @@
{#if !$isSmallViewport} -
+
{@render noSqlEditor?.()}
{:else} @@ -161,7 +161,8 @@ grid-template-columns: 1fr; } - &:has(.no-sql-editor:empty) { + &:has(.no-sql-editor:empty), + &:has(.no-sql-editor.desktop:empty) { grid-template-columns: 1fr; } } From 0d74a899513eb89d33d4541241b80dd8c1795eca Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 18 Oct 2025 18:12:46 +0530 Subject: [PATCH 24/60] fix: errors shown during loading mode. --- .../collection-[collection]/(components)/editor/view.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte index d664fbef2f..96a0e8b72f 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte @@ -910,7 +910,7 @@ {/if} - {#if errorMessage && !$isSmallViewport} + {#if errorMessage && !$isSmallViewport && !loading}
{errorMessage}
@@ -957,7 +957,7 @@ {/if}
- {#if errorMessage && $isSmallViewport} + {#if errorMessage && $isSmallViewport && !loading}
{errorMessage} From 4fd5f53bf29929b6ddb67cdbe7ad39726177e650 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 18 Oct 2025 18:17:22 +0530 Subject: [PATCH 25/60] fix: updates. --- .../spreadsheet.svelte | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index d13bf2068e..4620f326f5 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -386,22 +386,11 @@ data: documentWithoutDates ?? [] }); - await invalidate(Dependencies.DOCUMENTS); trackEvent(Submit.DocumentCreate); addNotification({ message: 'Document has been created', type: 'success' }); - - noSqlDocument.update(() => { - return { - isNew: false, - show: false, - document: {} - }; - }); - - spreadsheetRenderKey; } else { // update await documentsDB.updateDocument({ @@ -412,7 +401,6 @@ permissions: document.$permissions ?? [] }); - await invalidate(Dependencies.DOCUMENT); trackEvent(Submit.DocumentUpdate); addNotification({ message: 'Document has been updated', @@ -420,8 +408,17 @@ }); } + await invalidate(Dependencies.DOCUMENTS); + noSqlDocument.update(() => { + return { + isNew: false, + show: false, + document: {} + }; + }); + // re-render spreadsheet! - spreadsheetRenderKey.set(hash($id)); + spreadsheetRenderKey.set(hash(Date.now().toString())); const firstDocument = $documents?.documents?.[0]; if (firstDocument) { $noSqlDocument.document = firstDocument; @@ -431,7 +428,7 @@ message: error.message, type: 'error' }); - trackError(error, Submit.DocumentUpdatePermissions); + trackError(error, Submit.DocumentUpdate); } } From 40704b87b00971e17791e16c281b31992110e647 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 19 Oct 2025 11:42:29 +0530 Subject: [PATCH 26/60] updates: better empty state. --- .../(entity)/views/layouts/empty.svelte | 72 +++++++++++++++++-- .../spreadsheet.svelte | 4 +- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte index e25730dc4a..eb5732d146 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte @@ -169,6 +169,51 @@ } ]; + const getDocumentsDbColumns = (): Column[] => [ + { + id: '$id', + title: '$id', + width: 225, + minimumWidth: 225, + draggable: false, + type: 'string', + icon: IconFingerPrint, + isEditable: false, + isPrimary: false + }, + { + id: '$createdAt', + title: '$createdAt', + width: 200, + minimumWidth: 200, + draggable: false, + type: 'datetime', + icon: IconCalendar, + isEditable: false + }, + { + id: '$updatedAt', + title: '$updatedAt', + width: 200, + minimumWidth: 200, + draggable: false, + type: 'datetime', + icon: IconCalendar, + isEditable: false + }, + { + id: 'actions', + title: '', + width: 40, + isAction: true, + draggable: false, + type: 'string', + resizable: false, + isEditable: false, + hide: false + } + ]; + const getIndexesColumns = (): Column[] => [ { id: 'key', title: 'Key', icon: null, isPrimary: false }, @@ -183,7 +228,13 @@ } ] as Column[]; - const spreadsheetColumns = $derived(mode === 'records' ? getRowColumns() : getIndexesColumns()); + const spreadsheetColumns = $derived.by(() => { + return mode === 'records' + ? type !== 'documentsdb' + ? getRowColumns() + : getDocumentsDbColumns() + : getIndexesColumns(); + }); const emptyCells = $derived( ($isSmallViewport ? 14 : $isTabletViewport ? 17 : 24) + (!$expandTabs ? 2 : 0) @@ -193,10 +244,12 @@
- + data-type={type} + data-loading={$spreadsheetLoading} + bind:this={spreadsheetContainer} + class="databases-spreadsheet spreadsheet-container-outer"> + {#snippet noSqlEditor()} - {#if type === 'documentsdb' && $spreadsheetLoading} - - {/if} + {/snippet} @@ -351,6 +402,13 @@ opacity: 0.85; pointer-events: none; } + + &[data-mode='records'][data-type='documentsdb'] { + position: unset; + &[data-loading='false'] :global(.skeleton) { + animation: none; + } + } } .spreadsheet-fade-bottom { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index 4620f326f5..ef6c787314 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -163,7 +163,7 @@ title: '$createdAt', width: 200, minimumWidth: 200, - draggable: true, + draggable: false, type: 'datetime', icon: IconCalendar, isEditable: false, @@ -174,7 +174,7 @@ title: '$updatedAt', width: 200, minimumWidth: 200, - draggable: true, + draggable: false, type: 'datetime', icon: IconCalendar, isEditable: false, From 2e7ff2279ed515766ddb671f2386cff3beb43723 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 19 Oct 2025 11:48:41 +0530 Subject: [PATCH 27/60] fix: editor shown in other screens. --- .../(entity)/views/layouts/empty.svelte | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte index eb5732d146..ca57518379 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte @@ -249,7 +249,7 @@ data-loading={$spreadsheetLoading} bind:this={spreadsheetContainer} class="databases-spreadsheet spreadsheet-container-outer"> - + {#snippet noSqlEditor()} - + {#if type === 'documentsdb'} + + {/if} {/snippet} @@ -377,7 +379,7 @@ position: fixed; overflow: hidden; - &[data-mode='records'] { + &[data-mode='records'][data-type='tablesdb'] { & :global([role='rowheader'] :nth-last-child(2) [role='presentation']) { display: none; } @@ -393,6 +395,14 @@ } } + &[data-mode='records'][data-type='documentsdb'] { + position: unset; + // disable animation when not loading! + &[data-loading='false'] :global(.skeleton) { + animation: none; + } + } + & :global(.spreadsheet-container) { overflow-x: hidden; overflow-y: hidden; @@ -402,13 +412,6 @@ opacity: 0.85; pointer-events: none; } - - &[data-mode='records'][data-type='documentsdb'] { - position: unset; - &[data-loading='false'] :global(.skeleton) { - animation: none; - } - } } .spreadsheet-fade-bottom { From e2600d73bac79f9d4e30bd6d77196ac6de6d137a Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 19 Oct 2025 13:18:46 +0530 Subject: [PATCH 28/60] add: editor support on mobile. --- .../(entity)/views/layouts/sidesheet.svelte | 4 + .../(entity)/views/layouts/spreadsheet.svelte | 23 +++- .../(components)/editor/view.svelte | 124 +++++++++--------- .../spreadsheet.svelte | 39 +++++- .../collection-[collection]/store.ts | 6 +- 5 files changed, 126 insertions(+), 70 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/sidesheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/sidesheet.svelte index 3f38779eb9..1e7c3adb6c 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/sidesheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/sidesheet.svelte @@ -11,6 +11,7 @@ let { show = $bindable(false), title, + headerEnd, closeOnBlur = false, submit, cancel, @@ -51,6 +52,7 @@ | undefined; children?: Snippet; footer?: Snippet | null; + headerEnd?: Snippet | null; } & HTMLAttributes = $props(); let form: Form; @@ -90,6 +92,8 @@ {/if} {/if} + + {@render headerEnd?.()}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/spreadsheet.svelte index aac0d131e0..7dfc292157 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/spreadsheet.svelte @@ -8,11 +8,24 @@ let { children, noSqlEditor, + sideSheetHeaderAction, + sideSheetOptions = null, showEditorSideSheet = $bindable(false) }: { children: Snippet; noSqlEditor?: Snippet; + sideSheetHeaderAction?: Snippet; showEditorSideSheet?: boolean; + sideSheetOptions?: { + sideSheetTitle?: string; + submit?: + | { + text: string; + disabled?: boolean; + onClick?: () => boolean | void | Promise; + } + | undefined; + }; } = $props(); let spreadsheetWrapper: HTMLDivElement; @@ -133,12 +146,14 @@ {:else} + submit={sideSheetOptions?.submit} + title={sideSheetOptions?.sideSheetTitle ?? 'Edit document'}> {@render noSqlEditor?.()} + + {#snippet headerEnd()} + {@render sideSheetHeaderAction?.()} + {/snippet} {/if}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte index 96a0e8b72f..cd6f8de036 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte @@ -73,12 +73,13 @@ data?: JsonValue; isSaving?: boolean; loading?: boolean; - onChange?: (newData: JsonValue) => Promise | void; + onChange?: (newData: JsonValue, hasChanged: boolean) => Promise | void; onSave?: (newData: JsonValue) => Promise | void; readonly?: boolean; wrapLines?: boolean; errorInPlace?: boolean; ctrlSave?: boolean; + showHeaderActions?: boolean; } let { @@ -91,7 +92,8 @@ readonly = false, wrapLines = true, errorInPlace = true, - ctrlSave = false + ctrlSave = false, + showHeaderActions = true }: Props = $props(); let editorContainer: HTMLDivElement = $state(null); @@ -794,7 +796,7 @@ } data = parsed; - onChange?.(parsed); + onChange?.(parsed, hasDataChanged); lastExpectedContent = dataToString(parsed); }, DEBOUNCE_DELAY); }), @@ -897,65 +899,67 @@
-
- {#if loading} - - {/if} - - - {#if documentId && !loading} -
- {truncateId(documentId)} -
+ {#if showHeaderActions} +
+ {#if loading} + {/if} - - {#if errorMessage && !$isSmallViewport && !loading} -
- {errorMessage} -
- {/if} - - {#if documentId} - - - - - Save - - - - - - {tooltipMessage} - + + {#if documentId && !loading} +
+ {truncateId(documentId)} +
+ {/if}
- {/if} -
+ + {#if errorMessage && !$isSmallViewport && !loading} +
+ {errorMessage} +
+ {/if} + + {#if documentId} + + + + + Save + + + + + + {tooltipMessage} + + + {/if} +
+ {/if} {#if errorMessage && $isSmallViewport && !loading}
@@ -1053,7 +1057,7 @@ border-bottom: 1px solid var(--border-neutral); &.mobile { - background: var(--bgcolor-error); + background: var(--bgcolor-error-weak); } .error-message { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index ef6c787314..a8e0122107 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -25,6 +25,7 @@ import { IconCalendar, IconDotsHorizontal, + IconDuplicate, IconFingerPrint } from '@appwrite.io/pink-icons-svelte'; import { isSmallViewport, isTabletViewport } from '$lib/stores/viewport'; @@ -411,9 +412,10 @@ await invalidate(Dependencies.DOCUMENTS); noSqlDocument.update(() => { return { - isNew: false, show: false, - document: {} + isNew: false, + document: {}, + hasDataChanged: false }; }); @@ -514,7 +516,15 @@ + bind:showEditorSideSheet={$noSqlDocument.show} + sideSheetOptions={{ + sideSheetTitle: $noSqlDocument.document?.$id, + submit: { + text: 'Update', + disabled: !$noSqlDocument.hasDataChanged, + onClick: async () => await createOrUpdateDocument($noSqlDocument.document) + } + }}> {#key $spreadsheetRenderKey} + isSelected={$noSqlDocument?.document?.$id === document.$id}> {#each $collectionColumns as { id: columnId } (columnId)} {#if columnId === '$id'} @@ -705,9 +715,30 @@ isNew={$noSqlDocument.isNew} loading={$noSqlDocument.loading} bind:data={$noSqlDocument.document} + showHeaderActions={!$isSmallViewport} + onChange={(_, hasDataChanged) => { + $noSqlDocument.hasDataChanged = hasDataChanged; + }} onSave={async (document) => await createOrUpdateDocument(document)} /> {/snippet} + {#snippet sideSheetHeaderAction()} + { + await copy(JSON.stringify($noSqlDocument.document, null, 2)); + addNotification({ + type: 'success', + message: 'Document copied', + timeout: 1250 + }); + }}> + + + {/snippet} + {#if selectedDocuments.length > 0}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts index c4057c0181..0b868b69ee 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts @@ -22,16 +22,18 @@ export const sortState = writable({ export const noSqlDocument = writable<{ show: boolean; - document?: Models.Document | object; + document?: Models.Document | (object & { $id?: string }); isNew?: boolean; loading?: boolean; documentId?: string /* for loading from a given id */; + hasDataChanged?: boolean; }>({ show: false, document: null, isNew: false, loading: false, - documentId: null + documentId: null, + hasDataChanged: false }); export const documentPermissionSheet = writable({ From 6e18d0a5e614c0f99a83e9f5f21e0b19d083671f Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 9 Jan 2026 17:43:13 +0530 Subject: [PATCH 29/60] update: misc changes. --- .../(entity)/helpers/sdk.ts | 98 ++++++++++++++++++- .../views/field/editPermissions.svelte | 30 ++---- .../database-[database]/+page.svelte | 4 +- .../collection-[collection]/+layout.svelte | 2 +- .../indexes/+page.svelte | 4 - .../spreadsheet.svelte | 6 +- .../table-[table]/rows/editPermissions.svelte | 0 .../database-[database]/table.svelte | 25 ++--- 8 files changed, 126 insertions(+), 43 deletions(-) delete mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/editPermissions.svelte 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 dc4bdf05b5..9165766320 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 @@ -1,7 +1,13 @@ import { sdk } from '$lib/stores/sdk'; import type { Page } from '@sveltejs/kit'; import type { TerminologyResult } from './types'; -import { type DatabaseType, type Entity, type EntityList, toSupportiveEntity } from './terminology'; +import { + type DatabaseType, + type Entity, + type EntityList, + type Record, + toSupportiveEntity +} from './terminology'; import type { Models } from '@appwrite.io/console'; export type DatabaseSdkResult = { @@ -32,6 +38,26 @@ export type DatabaseSdkResult = { databaseType?: DatabaseType; }) => Promise; delete: (params: { databaseId: string; databaseType?: DatabaseType }) => Promise<{}>; + deleteEntity: (params: { + databaseId: string; + entityId: string; + databaseType?: DatabaseType; + }) => Promise<{}>; + updateRecord: (params: { + databaseId: string; + entityId: string; + recordId: string; + data?: object; + permissions?: string[]; + databaseType?: DatabaseType; + }) => Promise; + updateRecordPermissions: (params: { + databaseId: string; + entityId: string; + recordId: string; + permissions: string[]; + databaseType?: DatabaseType; + }) => Promise; }; export function useDatabasesSdk( @@ -169,6 +195,76 @@ export function useDatabasesSdk( default: throw new Error(`Unknown database type`); } + }, + + async deleteEntity(params) { + switch (type ?? params.databaseType) { + case 'legacy': /* databases api */ + case 'tablesdb': + return await baseSdk.tablesDB.deleteTable({ + databaseId: params.databaseId, + tableId: params.entityId + }); + case 'documentsdb': + return await baseSdk.documentsDB.deleteCollection({ + databaseId: params.databaseId, + collectionId: params.entityId + }); + case 'vectordb': + throw new Error('Database type not supported yet'); + default: + throw new Error(`Unknown database type`); + } + }, + + async updateRecord(params) { + switch (type ?? params.databaseType) { + case 'legacy': /* databases api */ + case 'tablesdb': + return await baseSdk.tablesDB.updateRow({ + databaseId: params.databaseId, + tableId: params.entityId, + rowId: params.recordId, + data: params.data, + permissions: params.permissions + }); + case 'documentsdb': + return await baseSdk.documentsDB.updateDocument({ + databaseId: params.databaseId, + collectionId: params.entityId, + documentId: params.recordId, + data: params.data, + permissions: params.permissions + }); + case 'vectordb': + throw new Error('Database type not supported yet'); + default: + throw new Error(`Unknown database type`); + } + }, + + async updateRecordPermissions(params) { + switch (type ?? params.databaseType) { + case 'legacy': /* databases api */ + case 'tablesdb': + return await baseSdk.tablesDB.updateRow({ + databaseId: params.databaseId, + tableId: params.entityId, + rowId: params.recordId, + permissions: params.permissions + }); + case 'documentsdb': + return await baseSdk.documentsDB.updateDocument({ + databaseId: params.databaseId, + collectionId: params.entityId, + documentId: params.recordId, + permissions: params.permissions + }); + case 'vectordb': + throw new Error('Database type not supported yet'); + default: + throw new Error(`Unknown database type`); + } } }; } diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/field/editPermissions.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/field/editPermissions.svelte index 1d3b6c9834..95040da17f 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/field/editPermissions.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/field/editPermissions.svelte @@ -1,6 +1,4 @@ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index 04f1d642db..ed3550a958 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -359,8 +359,8 @@ } if (action === 'delete') { - // showDelete = true; - // selectedRowForDelete = document.$id; + selectedDocumentForDelete = document.$id; + showDelete = true; } if (action === 'activity') { @@ -776,7 +776,7 @@

{#if isSingle} - Are you sure you want to delete this row from {collection.name}? + Are you sure you want to delete this document from {collection.name}? {:else} Are you sure you want to delete {selectedDocuments.length} {selectedDocuments.length > 1 ? 'documents' : 'document'} from {collection.name}? diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/editPermissions.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/editPermissions.svelte deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table.svelte index 3768e05434..bb3ce44df5 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table.svelte @@ -11,28 +11,31 @@ import { Dependencies } from '$lib/constants'; import DualTimeView from '$lib/components/dualTimeView.svelte'; import { canWriteTables } from '$lib/stores/roles'; - import { sdk } from '$lib/stores/sdk'; - import type { PageData } from './$types'; import { Table } from '@appwrite.io/pink-svelte'; import { tableViewColumns, buildEntityRoute } from './store'; import { subNavigation } from '$lib/stores/database'; - import { type TerminologyResult } from '$database/(entity)'; + import { + type DatabaseSdkResult, + type EntityList, + type TerminologyResult + } from '$database/(entity)'; const { - data, - terminology + entities, + terminology, + databasesSdk }: { - data: PageData; + entities: EntityList; terminology: TerminologyResult; + databasesSdk: DatabaseSdkResult; } = $props(); const entitySingular = $derived(terminology.entity.lower.singular); - async function onDelete(batchDelete: DeleteOperation): Promise { - const result = await batchDelete((tableId) => - sdk.forProject(page.params.region, page.params.project).tablesDB.deleteTable({ + const result = await batchDelete((entityId) => + databasesSdk.deleteEntity({ databaseId: page.params.database, - tableId + entityId }) ); @@ -63,7 +66,7 @@ {/snippet} {#snippet children(root)} - {#each data.entities.entities as entity (entity.$id)} + {#each entities.entities as entity (entity.$id)} Date: Fri, 9 Jan 2026 18:32:11 +0530 Subject: [PATCH 30/60] update: proper navigation. --- .../database-[database]/(entity)/helpers/navigation.ts | 6 ++++++ .../database-[database]/(entity)/helpers/terminology.ts | 5 +++++ .../collection-[collection]/spreadsheet.svelte | 2 +- .../database-[database]/table-[table]/+layout.svelte | 3 ++- .../database-[database]/table-[table]/rows/store.ts | 5 ----- .../database-[database]/table-[table]/spreadsheet.svelte | 2 +- 6 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/navigation.ts diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/navigation.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/navigation.ts new file mode 100644 index 0000000000..601e4999d7 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/navigation.ts @@ -0,0 +1,6 @@ +import { page } from '$app/state'; +import type { RecordType } from '$database/(entity)'; + +export function buildFieldUrl(recordType: RecordType, recordId: string) { + return `${page.url}/${recordType}-${recordId}`; +} 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 cccb2405a3..f950f1ef6f 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 @@ -5,8 +5,13 @@ import { AppwriteException, 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 RecordType = ImplementedDBTypes[keyof ImplementedDBTypes]['record']; + export type Entity = Partial & { indexes?: Index[]; fields?: (Attributes | Columns)[]; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index ed3550a958..440c28d7a2 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -58,7 +58,7 @@ NoSqlEditor } from '$database/collection-[collection]/(components)/editor'; - import { buildFieldUrl } from '$database/table-[table]/rows/store'; + import { buildFieldUrl } from '$database/(entity)/helpers/navigation'; import { SpreadsheetOptions, type HeaderCellAction, 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 e61e0a7d9d..9df9a085c9 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 @@ -65,7 +65,8 @@ import { addNotification } from '$lib/stores/notifications'; import { hash } from '$lib/helpers/string'; import { preferences } from '$lib/stores/preferences'; - import { buildFieldUrl, isRelationship } from '$database/table-[table]/rows/store'; + import { buildFieldUrl } from '$database/(entity)/helpers/navigation'; + import { isRelationship } from '$database/table-[table]/rows/store'; import { chunks } from '$lib/helpers/array'; import { Submit, trackEvent } from '$lib/actions/analytics'; import { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/store.ts index ce3249db20..46ff2519b9 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/store.ts @@ -1,4 +1,3 @@ -import { page } from '$app/state'; import type { Field } from '$database/(entity)'; import type { Column } from '$lib/helpers/types'; import { type Models } from '@appwrite.io/console'; @@ -45,7 +44,3 @@ export function isSpatialType( return spatialTypes.includes(field.type.toLowerCase()); } - -export function buildFieldUrl(recordType: 'row' | 'document', recordId: string) { - return `${page.url}/${recordType}-${recordId}`; -} 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 da51a1281d..0ae128a92d 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 @@ -12,7 +12,6 @@ import { type ComponentType, onDestroy, onMount } from 'svelte'; import type { PageData } from './$types'; import { - buildFieldUrl, isRelationship, isRelationshipToMany, isSpatialType, @@ -36,6 +35,7 @@ databaseRelatedRowSheetOptions, rowPermissionSheet } from '$database/table-[table]/store'; + import { buildFieldUrl } from '$database/(entity)/helpers/navigation'; import type { Column, ColumnType } from '$lib/helpers/types'; import { Alert, From 5e345072a434b2e12c2fbc1f545e7475935c7a57 Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 9 Jan 2026 19:03:57 +0530 Subject: [PATCH 31/60] fix: ui inconsistencies. --- .../(entity)/views/layouts/empty.svelte | 11 +++-------- .../collection-[collection]/+page.svelte | 2 +- .../collection-[collection]/spreadsheet.svelte | 1 - 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte index 59735748fb..418c58cbdc 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte @@ -307,8 +307,8 @@

0} - class:no-custom-columns={customColumns.length <= 0} + class:custom-columns={spreadsheetColumns.length > 0} + class:no-custom-columns={spreadsheetColumns.length <= 0} data-loading={$spreadsheetLoading} bind:this={spreadsheetContainer} class="databases-spreadsheet spreadsheet-container-outer"> @@ -387,7 +387,7 @@ {#if !$spreadsheetLoading}
0} + class:custom-columns={spreadsheetColumns.length > 0} data-collapsed-tabs={!$expandTabs} style:--overlay-top={overlayTopOffset} style:--overlay-left={overlayLeftOffset} @@ -500,11 +500,6 @@ } } - & :global(.spreadsheet-container) { - overflow-x: hidden; - overflow-y: hidden; - } - & :global([data-select='true']) { opacity: 0.85; pointer-events: none; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte index b048786efe..8fb79126dc 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte @@ -46,7 +46,7 @@ addNotification({ type: 'success', - message: 'Rows import from csv has started' + message: 'Documents import from csv has started' }); trackEvent(Submit.DatabaseImportCsv); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index 440c28d7a2..a606c5f333 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -57,7 +57,6 @@ type JsonValue, NoSqlEditor } from '$database/collection-[collection]/(components)/editor'; - import { buildFieldUrl } from '$database/(entity)/helpers/navigation'; import { SpreadsheetOptions, From 70b0a9b0a61c6b574ba710e651a81ce9f571b9cd Mon Sep 17 00:00:00 2001 From: Darshan Date: Fri, 9 Jan 2026 20:06:31 +0530 Subject: [PATCH 32/60] update: efficient parsing. --- .../(components)/editor/view.svelte | 70 ++++++++++++++----- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte index cd6f8de036..7900e1a71f 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte @@ -2,6 +2,8 @@ export type JsonValue = string | number | boolean | null | JsonObject | JsonArray | object; export type JsonObject = { [key: string]: JsonValue }; export type JsonArray = JsonValue[]; + + type ParseResult = { ok: true; value: JsonValue } | { ok: false; error: unknown };
From 0b1e340826fe06d3adb26e75257fca8db53c7587 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sat, 10 Jan 2026 15:42:27 +0530 Subject: [PATCH 36/60] update: cleanup and improvements. --- .../(components)/editor/view.svelte | 421 +++++++++++++----- 1 file changed, 303 insertions(+), 118 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte index 9df1a55aec..694d82d399 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte @@ -3,7 +3,23 @@ export type JsonObject = { [key: string]: JsonValue }; export type JsonArray = JsonValue[]; - type ParseResult = { ok: true; value: JsonValue } | { ok: false; error: unknown }; + type ParseResult = + | { + ok: true; + value: JsonValue; + } + | { + ok: false; + error: unknown; + }; + + type Hit = { + key: string; + valueFrom: number; + valueTo: number; + lineFrom: number; + lineTo: number; + };
{ + // fires state callback. + showEditorSideSheet = false; + } + }} title={sideSheetOptions?.sideSheetTitle ?? 'Edit document'}> {@render noSqlEditor?.()} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/[...rest]/+page.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/[...rest]/+page.ts index 344f772e90..0543bf4f1e 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/[...rest]/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/[...rest]/+page.ts @@ -36,10 +36,7 @@ export const load: PageLoad = async ({ params, url }) => { const documentMatch = lastSegment.match(/^document-([^/]+)$/); if (documentMatch) { const documentId = documentMatch[1]; - noSqlDocument.update((options) => ({ - ...options, - documentId - })); + noSqlDocument.update({ documentId }); const parentSegments = restSegments.slice(0, -1); const newPath = `${baseUrl}/${parentSegments.join('/')}`; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte index 694d82d399..a65b82606e 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte @@ -59,7 +59,7 @@ import { onMount, onDestroy } from 'svelte'; import Id, { truncateId } from '$lib/components/id.svelte'; import { Icon, Layout, Skeleton, Spinner, Tooltip, Typography } from '@appwrite.io/pink-svelte'; - import { IconCheck, IconDuplicate } from '@appwrite.io/pink-icons-svelte'; + import { IconCheck, IconDuplicate, IconX } from '@appwrite.io/pink-icons-svelte'; import { Button } from '$lib/elements/forms'; import { copy } from '$lib/helpers/copy'; import { isSmallViewport } from '$lib/stores/viewport'; @@ -92,6 +92,7 @@ loading?: boolean; onChange?: (newData: JsonValue, hasChanged: boolean) => Promise | void; onSave?: (newData: JsonValue) => Promise | void; + onCancel?: () => void; readonly?: boolean; wrapLines?: boolean; errorInPlace?: boolean; @@ -104,6 +105,7 @@ data = $bindable(), onChange, onSave, + onCancel, isSaving = $bindable(false), loading = false, readonly = false, @@ -145,8 +147,8 @@ let lastParsePromise: Promise | null = null; // Serialized data cache - let originalSerialized = ''; let lastSerializedText = ''; + let originalSerialized = $state(''); let lastSerializedData: JsonValue | null = null; // Get $id from data @@ -1164,6 +1166,22 @@ {#if documentId} + {#if isNew && onCancel} + + + + Cancel + + {/if} + @@ -116,7 +127,7 @@
- {#if data.documents.total} + {#if data.documents.total || $noSqlDocument.isDirty} @@ -147,13 +158,7 @@ title="Create documents" subtitle="Create documents manually" onClick={() => { - if (!$noSqlDocument.isNew) { - noSqlDocument.update(() => ({ - show: true, - isNew: true, - document: {} - })); - } + noSqlDocument.create(buildInitDoc()); }} /> { - return { - show: false, - isNew: false, - document: {}, - hasDataChanged: false - }; - }); + noSqlDocument.reset(); // re-render spreadsheet! spreadsheetRenderKey.set(hash(Date.now().toString())); const firstDocument = $documents?.documents?.[0]; if (firstDocument) { - $noSqlDocument.document = firstDocument; + noSqlDocument.update({ document: firstDocument }); } } catch (error) { addNotification({ @@ -526,6 +525,11 @@ disabled: !$noSqlDocument.hasDataChanged, onClick: async () => await createOrUpdateDocument($noSqlDocument.document) } + }} + sideSheetStateCallbacks={{ + onClose() { + noSqlDocument.reset(); + } }}> {#key $spreadsheetRenderKey} {:else} + {@const selection = + $noSqlDocument.isDirty && document.$id === $noSqlDocument.document?.$id + ? 'disabled' + : rowSelection} + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index e7ae7b3de1..9c2da79e23 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -24,6 +24,7 @@ import DualTimeView from '$lib/components/dualTimeView.svelte'; import { IconCalendar, + IconCode, IconDotsHorizontal, IconDuplicate, IconFingerPrint @@ -144,6 +145,8 @@ paginatedDocuments.setPage(1, data.documents.documents); } + makeCollectionColumns(); + // documentId exists! if ($noSqlDocument.documentId) { await loadRemoteDocument(); @@ -154,6 +157,23 @@ function makeCollectionColumns() { const selectedColumnsToHide = preferences.getCustomTableColumns(collectionId); + + const customKeys = ( + preferences.getDisplayNames(collectionId, data.database.type) ?? [] + ).filter((name) => !name.startsWith('$')); + + const customColumns: Column[] = customKeys.map((key) => ({ + id: key, + title: key, + width: 225, + minimumWidth: 225, + draggable: false, + type: 'dynamic', + icon: IconCode /* fuzzy search based Icon later */, + isEditable: false, + hide: false + })); + const staticColumns: Column[] = [ { id: '$id', @@ -167,6 +187,7 @@ isPrimary: false, hide: !!selectedColumnsToHide?.includes('$id') }, + ...customColumns, { id: '$createdAt', title: '$createdAt', @@ -505,10 +526,6 @@ $: canShowDatetimePopover = true; - $: if ($documents.documents) { - makeCollectionColumns(); - } - $: totalPages = Math.ceil($documents.total / SPREADSHEET_PAGE_LIMIT) || 1; $: rowSelection = @@ -645,6 +662,13 @@ {/snippet} + {:else} + {@const value = document[columnId]} + {#if value} + {value} + {:else} + + {/if} {/if} {/each} From d28a5e6795cf8be718d09f028cd4704fad7d691e Mon Sep 17 00:00:00 2001 From: Darshan Date: Thu, 15 Jan 2026 16:54:48 +0530 Subject: [PATCH 40/60] Merge branch '8.x' into 'feat-documentsdb'. --- src/lib/components/columnSelector.svelte | 39 +++++- src/lib/components/viewSelector.svelte | 5 +- src/lib/helpers/types.ts | 1 + .../(components)/customColumnsEditor.svelte | 76 +++++++++++ .../collection-[collection]/+page.svelte | 128 ++++++++++++++---- .../settings/displayName.svelte | 88 ++++-------- .../spreadsheet.svelte | 11 +- 7 files changed, 246 insertions(+), 102 deletions(-) create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/customColumnsEditor.svelte diff --git a/src/lib/components/columnSelector.svelte b/src/lib/components/columnSelector.svelte index 506735df38..9bb6c23c07 100644 --- a/src/lib/components/columnSelector.svelte +++ b/src/lib/components/columnSelector.svelte @@ -10,8 +10,10 @@ Layout, Popover, Selector, - Typography + Typography, + Icon } from '@appwrite.io/pink-svelte'; + import { IconPlus } from '@appwrite.io/pink-icons-svelte'; import { Button } from '$lib/elements/forms'; let { @@ -21,7 +23,8 @@ allowNoColumns = false, showAnyway = false, children, - onPreferencesUpdated = null + onPreferencesUpdated = null, + onCustomOptionClick = null }: { columns: Writable; isCustomTable?: boolean; @@ -30,6 +33,7 @@ showAnyway?: boolean; children: Snippet<[toggle: () => void, selectedColumnsNumber: number]>; onPreferencesUpdated?: () => void; + onCustomOptionClick?: () => void; } = $props(); let search = $state(''); @@ -115,7 +119,7 @@ cols.map((col) => col.exclude ? col - : filteredColumns.some((fc) => fc.id === col.id) + : filteredColumns.some((fc) => fc.id === col.id && !col.disable) ? { ...col, hide: false } : col ) @@ -126,7 +130,9 @@ function deselectAll() { columns.update((cols) => { const realColumns = cols.filter((col) => !col.exclude && !col.isAction); - const filtered = filteredColumns.filter((col) => !col.exclude && !col.isAction); + const filtered = filteredColumns.filter( + (col) => !col.exclude && !col.isAction && !col.disable + ); if (filtered.length === 0) return cols; @@ -187,7 +193,7 @@ {@const placement = isNewStyle ? 'bottom-start' : 'bottom-end'} {@render children(toggle, selectedColumnsNumber)} - +
{#if isNewStyle && showActions} @@ -231,7 +237,8 @@ on:click={() => toggleColumn(column)} disabled={allowNoColumns ? false - : visibleRealColumns.length <= 1 && !column.hide}> + : (visibleRealColumns.length <= 1 && !column.hide) || + column.disable}> + + {#if onCustomOptionClick && isCustomTable} + + + + + {/if}
diff --git a/src/lib/components/viewSelector.svelte b/src/lib/components/viewSelector.svelte index e20b8ca1fa..ec2be986fa 100644 --- a/src/lib/components/viewSelector.svelte +++ b/src/lib/components/viewSelector.svelte @@ -22,6 +22,7 @@ allowNoColumns?: boolean; showAnyway?: boolean; disableButton?: boolean; + onCustomOptionClick?: () => void; } let { @@ -34,7 +35,8 @@ hideColumns = false, allowNoColumns = false, showAnyway = false, - disableButton = false + disableButton = false, + onCustomOptionClick = null }: Props = $props(); let showCountBadge = $state(false); @@ -65,6 +67,7 @@ {showAnyway} {isCustomTable} {allowNoColumns} + {onCustomOptionClick} onPreferencesUpdated={updateBadgeState}> {#snippet children(toggle, selectedColumnsNumber)} + import { InputTags } from '$lib/elements/forms'; + import { symmetricDifference } from '$lib/helpers/array'; + import { preferences } from '$lib/stores/preferences'; + import { Input, Layout } from '@appwrite.io/pink-svelte'; + import { organization } from '$lib/stores/organization'; + + let { + collectionId, + databaseType, + inModal = false, + onSuccess = null, + onFailure = null + }: { + collectionId: string; + databaseType: string; + inModal?: boolean; + onSuccess?: () => Promise | void; + onFailure?: (error: Error) => Promise | void; + } = $props(); + + let names = $state(getDisplayNames()); + + const isDisabled = $derived( + !symmetricDifference(names, getDisplayNames()).length || names.length > 5 + ); + + function getDisplayNames() { + const displayNames = preferences.getDisplayNames(collectionId, databaseType) ?? []; + return displayNames.filter((name) => !name.startsWith('$')); + } + + export function hasChanged() { + return isDisabled; + } + + export async function updateDisplayNames() { + try { + const regularArray = [...names]; + + await preferences.setDisplayNames( + $organization.$id, + collectionId, + regularArray, + databaseType + ); + + await onSuccess?.(); + + // reset with new values! + names = getDisplayNames(); + } catch (error) { + await onFailure?.(error); + } + } + + $effect(() => { + names = getDisplayNames(); + }); + + + + + {#key names.length} + + {/key} + + + ID, createdAt, and updatedAt are always included and cannot be modified + + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte index 6ca6d18a6d..9514fc1ad2 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte @@ -1,9 +1,10 @@ -
+ { + await customColumnsEditor?.updateDisplayNames(); + }}> Display name Add up to 5 document fields to display as columns in the collection view. - - {#key names.length} - - {/key} - - ID, createdAt, and updatedAt are always included and cannot be modified - - + { + await invalidate(Dependencies.TEAM); + addNotification({ + message: 'Display names have been updated', + type: 'success' + }); + trackEvent(Submit.CollectionUpdateDisplayNames); + }} + onFailure={(error) => { + addNotification({ + message: error.message, + type: 'error' + }); + trackError(error, Submit.CollectionUpdateDisplayNames); + }} /> - + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index 9c2da79e23..e0b3b6903d 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -171,7 +171,7 @@ type: 'dynamic', icon: IconCode /* fuzzy search based Icon later */, isEditable: false, - hide: false + hide: !!selectedColumnsToHide?.includes(key) })); const staticColumns: Column[] = [ @@ -185,7 +185,8 @@ icon: IconFingerPrint, isEditable: false, isPrimary: false, - hide: !!selectedColumnsToHide?.includes('$id') + hide: false, + disable: true }, ...customColumns, { @@ -197,7 +198,8 @@ type: 'datetime', icon: IconCalendar, isEditable: false, - hide: !!selectedColumnsToHide?.includes('$createdAt') + hide: false, + disable: true }, { id: '$updatedAt', @@ -208,7 +210,8 @@ type: 'datetime', icon: IconCalendar, isEditable: false, - hide: !!selectedColumnsToHide?.includes('$updatedAt') + hide: false, + disable: true }, { id: 'actions', From d3277744c6193c71a1bb419bd365dfb6c29983e4 Mon Sep 17 00:00:00 2001 From: Darshan Date: Thu, 15 Jan 2026 18:29:30 +0530 Subject: [PATCH 41/60] add: sdk methods; add: view selector for custom columns; update; organize the filters for code-mirror; update: disable search/find for now on the editor; --- .../(entity)/helpers/sdk.ts | 106 ++++++++++- .../(entity)/helpers/terminology.ts | 5 + .../editor/extensions/highlighting.ts | 47 +++++ .../(components)/editor/extensions/index.ts | 8 + .../editor/extensions/readonly.ts | 123 +++++++++++++ .../(components)/editor/helpers/constants.ts | 4 +- .../(components)/editor/helpers/keymaps.ts | 11 +- .../(components)/editor/view.svelte | 172 ++---------------- .../spreadsheet.svelte | 39 ++-- 9 files changed, 325 insertions(+), 190 deletions(-) create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/highlighting.ts create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/index.ts create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/readonly.ts 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 9f611625ac..13f8da3bc4 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 @@ -6,7 +6,9 @@ import { type Entity, type EntityList, type Record, - toSupportiveEntity + type RecordList, + toSupportiveEntity, + toSupportiveRecord } from './terminology'; import type { Models } from '@appwrite.io/console'; @@ -43,6 +45,14 @@ export type DatabaseSdkResult = { entityId: string; databaseType?: DatabaseType; }) => Promise<{}>; + createRecord: (params: { + databaseId: string; + entityId: string; + recordId: string; + data?: object; + permissions?: string[]; + databaseType?: DatabaseType; + }) => Promise; updateRecord: (params: { databaseId: string; entityId: string; @@ -58,6 +68,18 @@ export type DatabaseSdkResult = { permissions: string[]; databaseType?: DatabaseType; }) => Promise; + deleteRecord: (params: { + databaseId: string; + entityId: string; + recordId?: string; + databaseType?: DatabaseType; + }) => Promise; + deleteRecords: (params: { + databaseId: string; + entityId: string; + queries?: string[]; + databaseType?: DatabaseType; + }) => Promise; }; export function useDatabaseSdk( @@ -217,6 +239,32 @@ export function useDatabaseSdk( } }, + async createRecord(params) { + switch (type ?? params.databaseType) { + case 'legacy': /* databases api */ + case 'tablesdb': + return await baseSdk.tablesDB.createRow({ + databaseId: params.databaseId, + tableId: params.entityId, + rowId: params.recordId, + data: params.data, + permissions: params.permissions + }); + case 'documentsdb': + return await baseSdk.documentsDB.createDocument({ + databaseId: params.databaseId, + collectionId: params.entityId, + documentId: params.recordId, + data: params.data, + permissions: params.permissions + }); + case 'vectordb': + throw new Error('Database type not supported yet'); + default: + throw new Error(`Unknown database type`); + } + }, + async updateRecord(params) { switch (type ?? params.databaseType) { case 'legacy': /* databases api */ @@ -229,7 +277,7 @@ export function useDatabaseSdk( permissions: params.permissions }); case 'documentsdb': - return await baseSdk.documentsDB.updateDocument({ + return await baseSdk.documentsDB.upsertDocument({ databaseId: params.databaseId, collectionId: params.entityId, documentId: params.recordId, @@ -254,7 +302,7 @@ export function useDatabaseSdk( permissions: params.permissions }); case 'documentsdb': - return await baseSdk.documentsDB.updateDocument({ + return await baseSdk.documentsDB.upsertDocument({ databaseId: params.databaseId, collectionId: params.entityId, documentId: params.recordId, @@ -265,6 +313,58 @@ export function useDatabaseSdk( default: throw new Error(`Unknown database type`); } + }, + + async deleteRecord(params) { + switch (type ?? params.databaseType) { + case 'legacy': /* databases api */ + case 'tablesdb': { + const row = await baseSdk.tablesDB.deleteRow({ + databaseId: params.databaseId, + tableId: params.entityId, + rowId: params.recordId + }); + return toSupportiveRecord(row); + } + case 'documentsdb': { + const document = await baseSdk.documentsDB.deleteDocument({ + databaseId: params.databaseId, + collectionId: params.entityId, + documentId: params.recordId + }); + return toSupportiveRecord(document); + } + case 'vectordb': + throw new Error('Database type not supported yet'); + default: + throw new Error(`Unknown database type`); + } + }, + + async deleteRecords(params) { + switch (type ?? params.databaseType) { + case 'legacy': /* databases api */ + case 'tablesdb': { + const { total, rows } = await baseSdk.tablesDB.deleteRows({ + databaseId: params.databaseId, + tableId: params.entityId, + queries: params.queries + }); + return { total, records: rows.map(toSupportiveRecord) }; + } + case 'documentsdb': { + const { total, documents } = await baseSdk.documentsDB.deleteDocuments({ + databaseId: params.databaseId, + collectionId: params.entityId, + queries: params.queries + }); + return { total, records: documents.map(toSupportiveRecord) }; + } + case 'vectordb': + throw new Error('Database type not supported yet'); + default: + throw new Error(`Unknown database type`); + } } }; } 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 b4f04906f9..005a4ca69d 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 @@ -34,6 +34,11 @@ export type EntityList = { entities: Entity[]; }; +export type RecordList = { + total: number; + records: Record[]; +}; + export const baseTerminology = { /** * this is no longer used on console so diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/highlighting.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/highlighting.ts new file mode 100644 index 0000000000..05dc49ab69 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/highlighting.ts @@ -0,0 +1,47 @@ +import { + Decoration, + type DecorationSet, + EditorView, + type ViewUpdate, + ViewPlugin +} from '@codemirror/view'; +import { Range } from '@codemirror/state'; +import { NESTED_KEY_REGEX } from '../helpers/constants'; + +// ViewPlugin to highlight nested keys (4+ spaces) only in visible ranges +export const nestedKeyPlugin = ViewPlugin.fromClass( + class { + decorations: DecorationSet; + constructor(view: EditorView) { + this.decorations = this.compute(view); + } + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = this.compute(update.view); + } + } + compute(view: EditorView): DecorationSet { + const decos: Range[] = []; + for (const { from, to } of view.visibleRanges) { + let line = view.state.doc.lineAt(from); + while (line.from <= to) { + const text = line.text; + const m = text.match(NESTED_KEY_REGEX); + if (m) { + const leading = m[1]; + const key = m[2]; + const start = line.from + leading.length; + const end = start + key.length; + decos.push( + Decoration.mark({ class: 'cm-nestedPropertyName' }).range(start, end) + ); + } + if (line.to >= to) break; + line = view.state.doc.line(line.number + 1); + } + } + return Decoration.set(decos); + } + }, + { decorations: (v) => v.decorations } +); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/index.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/index.ts new file mode 100644 index 0000000000..d9030871ae --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/index.ts @@ -0,0 +1,8 @@ +export { + findReadOnlyRanges, + createReadOnlyRangesField, + createReadOnlyLineField, + createReadOnlyRangesFilter +} from './readonly'; + +export { nestedKeyPlugin } from './highlighting'; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/readonly.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/readonly.ts new file mode 100644 index 0000000000..3ddb717bd2 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/readonly.ts @@ -0,0 +1,123 @@ +import { Decoration, type DecorationSet, EditorView } from '@codemirror/view'; +import { StateField, Transaction, Range } from '@codemirror/state'; +import type { Text } from '@codemirror/state'; +import { SYSTEM_KEYS } from '../helpers/constants'; + +// Find ranges of system keys (lines starting with $id, $createdAt, $updatedAt) +// When isNew=true, skip all readonly range detection since we don't have timestamps yet +export function findReadOnlyRanges(doc: Text, isNew: boolean): Array<{ from: number; to: number }> { + // When creating a new document, allow editing everything + if (isNew) return []; + + const ranges: Array<{ from: number; to: number }> = []; + let found = 0; + + for (let i = 1; i <= doc.lines; i++) { + const line = doc.line(i); + const lineText = line.text.trim(); + for (const key of SYSTEM_KEYS) { + if (lineText.startsWith(key)) { + ranges.push({ from: line.from, to: line.to }); + found++; + break; + } + } + if (found === SYSTEM_KEYS.size) break; + } + + return ranges; +} + +// Ranges field for read-only system lines (single source of truth) +export const createReadOnlyRangesField = (isNew: boolean) => + StateField.define>({ + create(state) { + return findReadOnlyRanges(state.doc, isNew); + }, + update(value, tr) { + if (!tr.docChanged) return value; + return findReadOnlyRanges(tr.state.doc, isNew); + } + }); + +// State field to add decorations to read-only lines +export const createReadOnlyLineField = ( + readOnlyRangesField: StateField> +) => + StateField.define({ + create(state) { + const decorations: Range[] = []; + const readOnlyRanges = state.field(readOnlyRangesField); + + for (const range of readOnlyRanges) { + decorations.push( + Decoration.line({ + class: 'cm-readOnlyLine' + }).range(range.from) + ); + } + + return Decoration.set(decorations); + }, + update(decorations, tr) { + if (!tr.docChanged) return decorations; + + const newDecorations: Range[] = []; + const readOnlyRanges = tr.state.field(readOnlyRangesField); + + for (const range of readOnlyRanges) { + newDecorations.push( + Decoration.line({ + class: 'cm-readOnlyLine' + }).range(range.from) + ); + } + + return Decoration.set(newDecorations); + }, + provide: (f) => EditorView.decorations.from(f) + }); + +// Transaction filter to prevent edits on system key lines +export const createReadOnlyRangesFilter = ( + readOnlyRangesField: StateField>, + readonly: boolean +) => { + return (tr: Transaction) => { + if (readonly || !tr.docChanged) return tr; + const ue = tr.annotation(Transaction.userEvent); + if (typeof ue === 'string' && ue.startsWith('appwrite:')) { + return tr; + } + + const startDoc = tr.startState.doc; + const readOnlyRanges = tr.startState.field(readOnlyRangesField); + let blocked = false; + let fullReplace = false; + + tr.changes.iterChanges((fromA: number, toA: number) => { + // Allow full-document replacement (Select All → Paste) + if (fromA === 0 && toA === startDoc.length) { + fullReplace = true; + return; + } + + // Check if change overlaps with any read-only range + for (const range of readOnlyRanges) { + if ( + // treat line ranges as half-open [from, to) + (fromA >= range.from && fromA < range.to) || + (toA > range.from && toA < range.to) || + (fromA < range.from && toA > range.to) + ) { + blocked = true; + break; + } + } + }); + + if (fullReplace) return tr; + // Block the transaction if it tries to edit a read-only range + return blocked ? [] : tr; + }; +}; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/constants.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/constants.ts index 800c0b7631..91730b6376 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/constants.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/constants.ts @@ -6,8 +6,8 @@ export const SYSTEM_KEYS = new Set(['$id:', '$createdAt:', '$updatedAt:']); export const DEBOUNCE_DELAY = 200; export const LINTER_DELAY = 250; -// regex patterns (compiled once for performance) -export const UNQUOTED_KEY_REGEX = /([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g; +// regex patterns +/* export const UNQUOTED_KEY_REGEX = /([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g; */ export const INDENT_REGEX = /^[\t ]*/; export const SCALAR_VALUE_REGEX = /:\s*(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*$/; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/keymaps.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/keymaps.ts index 5fa8123ed2..dcbfa51769 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/keymaps.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/keymaps.ts @@ -1,4 +1,4 @@ -import { searchKeymap } from '@codemirror/search'; +/*import { searchKeymap } from '@codemirror/search';*/ import { closeBracketsKeymap } from '@codemirror/autocomplete'; import type { EditorView, KeyBinding } from '@codemirror/view'; import { defaultKeymap, historyKeymap, indentLess, indentMore } from '@codemirror/commands'; @@ -28,6 +28,13 @@ export function createEditorKeymaps( }); } + // Disable search/replace for now! + keymaps.push({ + key: 'Mod-f', + preventDefault: true, + run: () => true + }); + return keymaps; } @@ -35,6 +42,6 @@ export function createEditorKeymaps( export const secondaryKeymaps = [ ...closeBracketsKeymap, ...defaultKeymap, - ...searchKeymap, + /*...searchKeymap,*/ ...historyKeymap ]; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte index a65b82606e..b253e3c88a 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte @@ -29,10 +29,7 @@ lineNumbers, highlightActiveLine, highlightActiveLineGutter, - Decoration, - type DecorationSet, - type ViewUpdate, - ViewPlugin + type ViewUpdate } from '@codemirror/view'; import { history } from '@codemirror/commands'; import { @@ -49,8 +46,6 @@ import { EditorState, EditorSelection, - Range, - StateField, Transaction, Compartment, type Extension @@ -68,9 +63,14 @@ import { parse } from './validators/json5'; import { customTheme, customSyntaxHighlighting } from './helpers/theme'; import { createEditorKeymaps, secondaryKeymaps } from './helpers/keymaps'; + import { + createReadOnlyRangesField, + createReadOnlyLineField, + createReadOnlyRangesFilter, + nestedKeyPlugin + } from './extensions'; import { ALLOWED_DOLLAR_PROPS, - SYSTEM_KEYS, DEBOUNCE_DELAY, LINTER_DELAY, INDENT_REGEX, @@ -78,7 +78,6 @@ TRAILING_COMMA_REGEX, WHITESPACE_REGEX, WHITESPACE_ONLY_REGEX, - NESTED_KEY_REGEX, SKELETON_LINES, getIndent } from './helpers/constants'; @@ -265,31 +264,6 @@ return serialized; } - // Find ranges of system keys (lines starting with $id, $createdAt, $updatedAt) - // When isNew=true, skip all readonly range detection since we don't have timestamps yet - function findReadOnlyRanges(doc: Text): Array<{ from: number; to: number }> { - // When creating a new document, allow editing everything - if (isNew) return []; - - const ranges: Array<{ from: number; to: number }> = []; - let found = 0; - - for (let i = 1; i <= doc.lines; i++) { - const line = doc.line(i); - const lineText = line.text.trim(); - for (const key of SYSTEM_KEYS) { - if (lineText.startsWith(key)) { - ranges.push({ from: line.from, to: line.to }); - found++; - break; - } - } - if (found === SYSTEM_KEYS.size) break; - } - - return ranges; - } - // Preserve system key values when content changes function preserveSystemValues(parsed: JsonValue): JsonValue { if ( @@ -740,131 +714,9 @@ return true; } - // Transaction filter to prevent edits on system key lines - function readOnlyRangesFilter(tr: Transaction) { - if (readonly || !tr.docChanged) return tr; - const ue = tr.annotation(Transaction.userEvent); - if (typeof ue === 'string' && ue.startsWith('appwrite:')) { - return tr; - } - - const startDoc = tr.startState.doc; - const readOnlyRanges = tr.startState.field(readOnlyRangesField); - let blocked = false; - let fullReplace = false; - - tr.changes.iterChanges((fromA: number, toA: number) => { - // Allow full-document replacement (Select All → Paste) - if (fromA === 0 && toA === startDoc.length) { - fullReplace = true; - return; - } - - // Check if change overlaps with any read-only range - for (const range of readOnlyRanges) { - if ( - // treat line ranges as half-open [from, to) - (fromA >= range.from && fromA < range.to) || - (toA > range.from && toA < range.to) || - (fromA < range.from && toA > range.to) - ) { - blocked = true; - break; - } - } - }); - - if (fullReplace) return tr; - // Block the transaction if it tries to edit a read-only range - return blocked ? [] : tr; - } - - // Ranges field for read-only system lines (single source of truth) - const readOnlyRangesField = StateField.define>({ - create(state) { - return findReadOnlyRanges(state.doc); - }, - update(value, tr) { - if (!tr.docChanged) return value; - return findReadOnlyRanges(tr.state.doc); - } - }); - - // State field to add decorations to read-only lines - const readOnlyLineField = StateField.define({ - create(state) { - const decorations: Range[] = []; - const readOnlyRanges = state.field(readOnlyRangesField); - - for (const range of readOnlyRanges) { - decorations.push( - Decoration.line({ - class: 'cm-readOnlyLine' - }).range(range.from) - ); - } - - return Decoration.set(decorations); - }, - update(decorations, tr) { - if (!tr.docChanged) return decorations; - - const newDecorations: Range[] = []; - const readOnlyRanges = tr.state.field(readOnlyRangesField); - - for (const range of readOnlyRanges) { - newDecorations.push( - Decoration.line({ - class: 'cm-readOnlyLine' - }).range(range.from) - ); - } - - return Decoration.set(newDecorations); - }, - provide: (f) => EditorView.decorations.from(f) - }); - - // ViewPlugin to highlight nested keys (4+ spaces) only in visible ranges - const nestedKeyPlugin = ViewPlugin.fromClass( - class { - decorations: DecorationSet; - constructor(view: EditorView) { - this.decorations = this.compute(view); - } - update(update: ViewUpdate) { - if (update.docChanged || update.viewportChanged) { - this.decorations = this.compute(update.view); - } - } - compute(view: EditorView): DecorationSet { - const decos: Range[] = []; - for (const { from, to } of view.visibleRanges) { - let line = view.state.doc.lineAt(from); - while (line.from <= to) { - const text = line.text; - const m = text.match(NESTED_KEY_REGEX); - if (m) { - const leading = m[1]; - const key = m[2]; - const start = line.from + leading.length; - const end = start + key.length; - decos.push( - Decoration.mark({ class: 'cm-nestedPropertyName' }).range( - start, - end - ) - ); - } - if (line.to >= to) break; - line = view.state.doc.line(line.number + 1); - } - } - return Decoration.set(decos); - } - }, - { decorations: (v) => v.decorations } - ); + // Create extension instances + const readOnlyRangesField = createReadOnlyRangesField(isNew); + const readOnlyLineField = createReadOnlyLineField(readOnlyRangesField); function parseWithCache(content: string): Promise { if (lastParsePromise && content === lastParseContent) { @@ -962,7 +814,9 @@ closeBrackets(), linter(javascriptLinter, { delay: LINTER_DELAY }), readOnlyRangesField, - EditorState.transactionFilter.of(readOnlyRangesFilter), + EditorState.transactionFilter.of( + createReadOnlyRangesFilter(readOnlyRangesField, readonly) + ), readOnlyLineField, nestedKeyPlugin, highlightSelectionMatches(), diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index e0b3b6903d..e53b262b2e 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -24,13 +24,12 @@ import DualTimeView from '$lib/components/dualTimeView.svelte'; import { IconCalendar, - IconCode, IconDotsHorizontal, IconDuplicate, IconFingerPrint } from '@appwrite.io/pink-icons-svelte'; import { isSmallViewport, isTabletViewport } from '$lib/stores/viewport'; - import { SpreadsheetContainer } from '$database/(entity)'; + import { SpreadsheetContainer, useDatabaseSdk } from '$database/(entity)'; import { copy } from '$lib/helpers/copy'; import { writable } from 'svelte/store'; import { pageToOffset } from '$lib/helpers/load'; @@ -84,6 +83,7 @@ const databaseId = page.params.database; const collectionId = page.params.collection; + const databaseSdk = useDatabaseSdk(page.params.region, page.params.project, data.database.type); const emptyCellsLimit = $spreadsheetLoading ? 30 @@ -169,7 +169,6 @@ minimumWidth: 225, draggable: false, type: 'dynamic', - icon: IconCode /* fuzzy search based Icon later */, isEditable: false, hide: !!selectedColumnsToHide?.includes(key) })); @@ -273,24 +272,17 @@ try { if (selectedDocumentForDelete) { - await sdk - .forProject(page.params.region, page.params.project) - .documentsDB.deleteDocument({ - databaseId, - collectionId, - documentId: selectedDocumentForDelete - }); + await databaseSdk.deleteRecord({ + databaseId, + entityId: collectionId, + recordId: selectedDocumentForDelete + }); } else { if (selectedDocuments.length) { - const documentsSDK = sdk.forProject( - page.params.region, - page.params.project - ).documentsDB; - for (const batch of chunks(selectedDocuments, 100)) { - await documentsSDK.deleteDocuments({ + await databaseSdk.deleteRecords({ databaseId, - collectionId, + entityId: collectionId, queries: [Query.equal('$id', batch)] }); } @@ -401,7 +393,6 @@ // possibly for auto-save! async function createOrUpdateDocument(jsonValue: JsonValue) { const document = jsonValue as Models.Document; - const documentsDB = sdk.forProject(page.params.region, page.params.project).documentsDB; /** * remove dates because @@ -412,10 +403,10 @@ try { if ($noSqlDocument.isNew) { // create - await documentsDB.createDocument({ + await databaseSdk.createRecord({ databaseId, - collectionId, - documentId: $id, + entityId: collectionId, + recordId: $id, data: documentWithoutDates ?? {} }); @@ -426,10 +417,10 @@ }); } else { // update - await documentsDB.updateDocument({ + await databaseSdk.updateRecord({ databaseId, - collectionId, - documentId: $id, + entityId: collectionId, + recordId: $id, data: documentWithoutDates, permissions: document.$permissions ?? [] }); From 42332cb096bfca6972ac6a276d98f76618f01323 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 18 Jan 2026 15:45:25 +0530 Subject: [PATCH 42/60] add: auto-save. add: display names to view selector. add: new sonners for error and save. add: fuzzy search helper. --- package.json | 4 +- pnpm-lock.yaml | 20 +-- src/lib/helpers/search.ts | 52 +++++++ .../(components)/editor/helpers/constants.ts | 1 + .../(components)/editor/view.svelte | 129 ++++++++++-------- .../displayName.svelte} | 2 +- .../(components)/sonners/error.svelte | 59 ++++++++ .../sonners/icons/CheckCircleDuotone.svelte | 19 +++ .../(components)/sonners/index.ts | 2 + .../(components)/sonners/save.svelte | 96 +++++++++++++ .../collection-[collection]/+page.svelte | 12 +- .../settings/displayName.svelte | 14 +- .../spreadsheet.svelte | 21 +-- .../collection-[collection]/store.ts | 15 +- 14 files changed, 351 insertions(+), 95 deletions(-) create mode 100644 src/lib/helpers/search.ts rename src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/{customColumnsEditor.svelte => inputs/displayName.svelte} (97%) create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/error.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/icons/CheckCircleDuotone.svelte create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/index.ts create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/save.svelte diff --git a/package.json b/package.json index 28adefd62b..9cb8f4a2c2 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,9 @@ "@ai-sdk/svelte": "^1.1.24", "@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@bbad65f", + "@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@bbad65f", + "@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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efc1a9658c..9fddf82dce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,14 +18,14 @@ importers: specifier: 0.25.0 version: 0.25.0 '@appwrite.io/pink-icons-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bbad65f - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bbad65f(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@b92a389 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@b92a389(svelte@5.25.3) '@appwrite.io/pink-legacy': specifier: ^1.0.3 version: 1.0.3 '@appwrite.io/pink-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@bbad65f - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@bbad65f(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@b92a389 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@b92a389(svelte@5.25.3) '@codemirror/autocomplete': specifier: ^6.19.0 version: 6.19.0 @@ -314,8 +314,8 @@ packages: peerDependencies: svelte: ^4.0.0 - '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bbad65f': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bbad65f} + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@b92a389': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@b92a389} version: 2.0.0-RC.1 peerDependencies: svelte: ^4.0.0 @@ -329,8 +329,8 @@ packages: '@appwrite.io/pink-legacy@1.0.3': resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==} - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@bbad65f': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@bbad65f} + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@b92a389': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@b92a389} version: 2.0.0-RC.2 peerDependencies: svelte: ^4.0.0 @@ -3910,7 +3910,7 @@ snapshots: dependencies: svelte: 5.25.3 - '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bbad65f(svelte@5.25.3)': + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@b92a389(svelte@5.25.3)': dependencies: svelte: 5.25.3 @@ -3923,7 +3923,7 @@ snapshots: '@appwrite.io/pink-icons': 1.0.0 the-new-css-reset: 1.11.3 - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@bbad65f(svelte@5.25.3)': + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@b92a389(svelte@5.25.3)': dependencies: '@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3) '@floating-ui/dom': 1.6.13 diff --git a/src/lib/helpers/search.ts b/src/lib/helpers/search.ts new file mode 100644 index 0000000000..66f662bf31 --- /dev/null +++ b/src/lib/helpers/search.ts @@ -0,0 +1,52 @@ +import type { Models } from '@appwrite.io/console'; + +type FuzzySearchOptions = { + limit?: number; + minOccurrences?: number | null; +}; + +/** + * Finds common attribute keys across documents by analyzing their frequency. + */ +export function fuzzySearchKeys( + documents: Models.Document[], + options: FuzzySearchOptions = {} +): string[] | null { + if (!documents || documents.length < 5) { + return null; + } + + const { minOccurrences = 2, limit } = options; + + const attributeCount = new Map(); + const threshold = minOccurrences === null ? 5 : Math.max(2, Math.min(minOccurrences, 5)); + + // Process only first 5 documents + const docLimit = Math.min(5, documents.length); + + for (let docIndex = 0; docIndex < docLimit; docIndex++) { + const document = documents[docIndex]; + if (!document || typeof document !== 'object') continue; + + // track per-document keys + const seenInDoc = new Map(); + + for (const key in document) { + if (key[0] === '$' || seenInDoc.has(key)) continue; + + seenInDoc.set(key, true); + attributeCount.set(key, (attributeCount.get(key) || 0) + 1); + } + } + + const result: string[] = []; + for (const [key, count] of attributeCount) { + if (count >= threshold) { + result.push(key); + } + } + + result.sort(); + + return limit && limit > 0 ? result.slice(0, limit) : result; +} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/constants.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/constants.ts index 91730b6376..05b210f4c6 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/constants.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/constants.ts @@ -5,6 +5,7 @@ export const SYSTEM_KEYS = new Set(['$id:', '$createdAt:', '$updatedAt:']); // timing constants export const DEBOUNCE_DELAY = 200; export const LINTER_DELAY = 250; +export const AUTOSAVE_DELAY = 2000; // regex patterns /* export const UNQUOTED_KEY_REGEX = /([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g; */ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte index b253e3c88a..54a214a162 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte @@ -53,12 +53,11 @@ import type { Text } from '@codemirror/state'; import { onMount, onDestroy } from 'svelte'; import Id, { truncateId } from '$lib/components/id.svelte'; - import { Icon, Layout, Skeleton, Spinner, Tooltip, Typography } from '@appwrite.io/pink-svelte'; - import { IconCheck, IconDuplicate, IconX } from '@appwrite.io/pink-icons-svelte'; + import { Icon, Layout, Skeleton, Tooltip } from '@appwrite.io/pink-svelte'; + import { IconDuplicate, IconX } from '@appwrite.io/pink-icons-svelte'; import { Button } from '$lib/elements/forms'; import { copy } from '$lib/helpers/copy'; import { isSmallViewport } from '$lib/stores/viewport'; - import { slide } from 'svelte/transition'; import { parse } from './validators/json5'; import { customTheme, customSyntaxHighlighting } from './helpers/theme'; @@ -73,6 +72,7 @@ ALLOWED_DOLLAR_PROPS, DEBOUNCE_DELAY, LINTER_DELAY, + AUTOSAVE_DELAY, INDENT_REGEX, SCALAR_VALUE_REGEX, TRAILING_COMMA_REGEX, @@ -83,6 +83,8 @@ } from './helpers/constants'; import { toLocaleDateTime } from '$lib/helpers/date'; import { ID } from '@appwrite.io/console'; + import { Error as ErrorSonner, Save as SavingSonner } from '../sonners'; + import { sleep } from '$lib/helpers/promises'; interface Props { isNew?: boolean; @@ -119,13 +121,17 @@ let editorView: EditorView | null = null; let errorMessage = $state(null); let changeTimer: ReturnType | null = null; // debounce timer for parse + onChange + let autoSaveTimer: ReturnType | null = null; // debounce timer for auto-save let tooltipTimer: ReturnType | null = null; // timer for tooltip message reset let pendingCanonicalize = false; // set when a full-document replace (paste-all) occurs let lastExpectedContent = ''; // track latest serialized data to avoid spurious rewrites let lastDocId: string | null = null; // track current document identity for history reset let baseExtensions: Extension[] = []; // cached extension set to rebuild state on doc switch - const readOnlyCompartment = new Compartment(); + + let saveSonnerState: 'saving' | 'saved' | null = $state(null); + const wrapCompartment = new Compartment(); + const readOnlyCompartment = new Compartment(); let tooltipMessage = $state('Copy document'); @@ -792,6 +798,10 @@ } await onSave?.(dataToSave); + + // update after save completes + originalData = $state.snapshot(data); + isSaving = false; } @@ -861,6 +871,12 @@ changeTimer = null; } + // Clear auto-save timer when user starts typing again + if (autoSaveTimer) { + clearTimeout(autoSaveTimer); + autoSaveTimer = null; + } + changeTimer = setTimeout(async () => { const state = update.view.state; const newContent = state.doc.toString(); @@ -880,6 +896,27 @@ data = parsed; onChange?.(parsed, hasDataChanged); lastExpectedContent = serializeData(parsed); + + // Check if this was a manual edit (not undo) and trigger auto-save + const isUndoOrRedo = update.transactions.some( + (tr) => + tr.annotation(Transaction.userEvent) === 'undo' || + tr.annotation(Transaction.userEvent) === 'redo' + ); + + if (!isUndoOrRedo && !$isSmallViewport && hasDataChanged && onSave) { + // Clear existing auto-save timer + if (autoSaveTimer) { + clearTimeout(autoSaveTimer); + autoSaveTimer = null; + } + + // Set new auto-save timer + autoSaveTimer = setTimeout(() => { + handleSave(); + autoSaveTimer = null; + }, AUTOSAVE_DELAY); + } }, DEBOUNCE_DELAY); }), readOnlyCompartment.of(EditorState.readOnly.of(readonly)) @@ -901,6 +938,10 @@ clearTimeout(changeTimer); changeTimer = null; } + if (autoSaveTimer) { + clearTimeout(autoSaveTimer); + autoSaveTimer = null; + } if (tooltipTimer) { clearTimeout(tooltipTimer); tooltipTimer = null; @@ -926,7 +967,8 @@ $effect(() => { if (!editorView) return; - // Detect document switch + const expectedContent = serializeData(data); + if (documentId !== lastDocId) { lastDocId = documentId; lastParseContent = ''; @@ -939,9 +981,8 @@ if (!isNew) { // Capture original data snapshot when switching documents originalData = $state.snapshot(data); - const expected = serializeData(data); - lastExpectedContent = expected; + lastExpectedContent = expectedContent; if (changeTimer) { clearTimeout(changeTimer); @@ -951,7 +992,10 @@ pendingCanonicalize = false; isUpdatingFromEditor = true; - const newState = EditorState.create({ doc: expected, extensions: baseExtensions }); + const newState = EditorState.create({ + doc: expectedContent, + extensions: baseExtensions + }); editorView.setState(newState); queueMicrotask(() => (isUpdatingFromEditor = false)); return; @@ -959,12 +1003,15 @@ } // Only react when the external data actually changed - const expectedContent = serializeData(data); if (expectedContent === lastExpectedContent) return; lastExpectedContent = expectedContent; const currentContent = editorView.state.doc.toString(); if (currentContent !== expectedContent) { + if (!isUpdatingFromEditor && hasDataChanged) { + return; + } + isUpdatingFromEditor = true; editorView.dispatch({ changes: { from: 0, to: currentContent.length, insert: expectedContent }, @@ -995,6 +1042,19 @@ $effect(() => { originalSerialized = serializeData(originalData); }); + + $effect(() => { + if (isSaving) { + saveSonnerState = 'saving'; + } else if (saveSonnerState === 'saving') { + saveSonnerState = 'saved'; + sleep(AUTOSAVE_DELAY).then(() => { + if (saveSonnerState === 'saved') { + saveSonnerState = null; + } + }); + } + });
@@ -1012,12 +1072,6 @@ {/if} - {#if errorMessage && !$isSmallViewport && !loading} -
- {errorMessage} -
- {/if} - {#if documentId} {#if isNew && onCancel} @@ -1036,24 +1090,6 @@ {/if} - - - - Save - - + {/if} + + +
+{/if} + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte index 9514fc1ad2..27df7291a4 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte @@ -29,7 +29,7 @@ import { canWriteRows } from '$lib/stores/roles'; import SpreadSheet from '$database/collection-[collection]/spreadsheet.svelte'; import { toLocaleDateTime } from '$lib/helpers/date'; - import CustomColumnsEditor from '$database/collection-[collection]/(components)/customColumnsEditor.svelte'; + import ColumnDisplayNameInput from '$database/collection-[collection]/(components)/inputs/displayName.svelte'; import { Modal } from '$lib/components'; const { data }: PageProps = $props(); @@ -38,7 +38,7 @@ let showCustomColumnsModal = $state(false); let columnsError: string = $state(null); - let customColumnEditor: CustomColumnsEditor | null = $state(null); + let columnDisplayNameInput: ColumnDisplayNameInput | null = $state(null); function buildInitDoc() { const now = new Date().toISOString(); @@ -231,15 +231,15 @@ bind:error={columnsError} bind:show={showCustomColumnsModal} onSubmit={async () => { - await customColumnEditor?.updateDisplayNames(); + await columnDisplayNameInput?.updateDisplayNames(); }}> Add up to 5 document fields to display as columns in the table view for easy identification. - { @@ -253,7 +253,7 @@ - + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/settings/displayName.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/settings/displayName.svelte index 46c684f394..d6a66baaa2 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/settings/displayName.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/settings/displayName.svelte @@ -7,27 +7,27 @@ import { addNotification } from '$lib/stores/notifications'; import { page } from '$app/state'; import { getTerminologies } from '$database/(entity)'; - import CustomColumnsEditor from '../(components)/customColumnsEditor.svelte'; + import ColumnDisplayNameInput from '../(components)/inputs/displayName.svelte'; const collectionId = page.params.collection; const { terminology } = getTerminologies(); - let customColumnsEditor: CustomColumnsEditor | null = $state(null); + let columnDisplayNameInput: ColumnDisplayNameInput | null = $state(null);
{ - await customColumnsEditor?.updateDisplayNames(); + await columnDisplayNameInput?.updateDisplayNames(); }}> - Display name + Custom columns Add up to 5 document fields to display as columns in the collection view. - { await invalidate(Dependencies.TEAM); addNotification({ @@ -46,7 +46,7 @@ - + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index e53b262b2e..5058dc4c08 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -400,6 +400,9 @@ */ const { $createdAt, $updatedAt, $id, ...documentWithoutDates } = document; + // Set isSaving to trigger the sonner in the editor + noSqlDocument.update({ isSaving: true }); + try { if ($noSqlDocument.isNew) { // create @@ -411,10 +414,6 @@ }); trackEvent(Submit.DocumentCreate); - addNotification({ - message: 'Document has been created', - type: 'success' - }); } else { // update await databaseSdk.updateRecord({ @@ -426,14 +425,10 @@ }); trackEvent(Submit.DocumentUpdate); - addNotification({ - message: 'Document has been updated', - type: 'success' - }); } await invalidate(Dependencies.DOCUMENTS); - noSqlDocument.reset(); + noSqlDocument.reset({ show: true }); // re-render spreadsheet! spreadsheetRenderKey.set(hash(Date.now().toString())); @@ -447,6 +442,8 @@ type: 'error' }); trackError(error, Submit.DocumentUpdate); + } finally { + noSqlDocument.update({ isSaving: false }); } } @@ -534,7 +531,10 @@ submit: { text: 'Update', disabled: !$noSqlDocument.hasDataChanged, - onClick: async () => await createOrUpdateDocument($noSqlDocument.document) + onClick: async () => { + await createOrUpdateDocument($noSqlDocument.document); + return true; + } } }} sideSheetStateCallbacks={{ @@ -741,6 +741,7 @@ isNew={$noSqlDocument.isNew} loading={$noSqlDocument.loading} bind:data={$noSqlDocument.document} + bind:isSaving={$noSqlDocument.isSaving} showHeaderActions={!$isSmallViewport} onCancel={() => noSqlDocument.reset()} onSave={async (document) => await createOrUpdateDocument(document)} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts index 8354c93ed3..5f58732e92 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/store.ts @@ -28,6 +28,7 @@ export type NoSqlDocumentState = { documentId?: string /* for loading from a given id */; hasDataChanged?: boolean; isDirty?: boolean; + isSaving?: boolean; }; const createNoSqlDocumentStore = () => { @@ -42,21 +43,23 @@ const createNoSqlDocumentStore = () => { loading: false, documentId: null, hasDataChanged: false, - isDirty: false + isDirty: false, + isSaving: false }); return { subscribe, set, - reset: () => + reset: (config?: { show?: boolean }) => set({ - show: false, + show: config?.show ?? false, document: null, isNew: false, loading: false, documentId: null, hasDataChanged: false, isDirty: false + // isSaving: false }), create: (document: Models.Document | (object & { $id?: string })) => set({ @@ -66,7 +69,8 @@ const createNoSqlDocumentStore = () => { loading: false, documentId: null, hasDataChanged: false, - isDirty: true + isDirty: true, + isSaving: false }), edit: (document: Models.Document, documentId?: string) => set({ @@ -76,7 +80,8 @@ const createNoSqlDocumentStore = () => { loading: false, documentId: documentId ?? null, hasDataChanged: false, - isDirty: false + isDirty: false, + isSaving: false }), update: (partial: Partial) => baseUpdate((state) => ({ ...state, ...partial })) From b7970e7d5d0c69738a6d1f1922444f70ee2c8c44 Mon Sep 17 00:00:00 2001 From: Darshan Date: Mon, 19 Jan 2026 11:57:21 +0530 Subject: [PATCH 43/60] add: fuzzy search based suggestions. add: json5 parser, linter and remove custom one. update: improve code editor's behaviour. add: apply fuzzy suggestions. update: faker for documentsDB. --- package.json | 4 +- pnpm-lock.yaml | 26 +- src/lib/helpers/faker.ts | 3 +- .../(entity)/views/create.svelte | 14 +- .../(suggestions)/columns.svelte | 18 +- .../(suggestions)/empty.svelte | 28 +- .../(suggestions)/input.svelte | 19 +- .../(suggestions)/store.ts | 8 +- .../editor/extensions/highlighting.ts | 126 ++++++--- .../(components)/editor/extensions/index.ts | 2 +- .../(components)/editor/validators/json5.ts | 73 ----- .../(components)/editor/view.svelte | 258 +++++++++++------- .../(components)/sonners/index.ts | 1 + .../(components)/sonners/suggestions.svelte | 53 ++++ .../collection-[collection]/+layout.svelte | 118 +++++++- .../spreadsheet.svelte | 10 + .../table-[table]/+layout.svelte | 6 +- .../table-[table]/+page.svelte | 10 +- 18 files changed, 500 insertions(+), 277 deletions(-) delete mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/validators/json5.ts create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/suggestions.svelte diff --git a/package.json b/package.json index 9cb8f4a2c2..e63db1f15d 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.38.6", "@faker-js/faker": "^9.9.0", - "@plausible-analytics/tracker": "^0.4.4", "@lezer/highlight": "^1.2.1", + "@plausible-analytics/tracker": "^0.4.4", "@popperjs/core": "^2.11.8", "@sentry/sveltekit": "^8.38.0", "@stripe/stripe-js": "^3.5.0", @@ -46,12 +46,12 @@ "@threlte/extras": "^9.7.1", "ai": "^2.2.37", "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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9fddf82dce..94b8a0f5f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,9 @@ importers: analytics: specifier: ^0.8.16 version: 0.8.16(@types/dlv@1.1.5) + codemirror-json5: + specifier: ^1.0.3 + version: 1.0.3 cron-parser: specifier: ^4.9.0 version: 4.9.0 @@ -98,9 +101,6 @@ importers: ignore: specifier: ^6.0.2 version: 6.0.2 - json5: - specifier: ^2.2.3 - version: 2.2.3 nanoid: specifier: ^5.1.5 version: 5.1.5 @@ -1891,6 +1891,9 @@ packages: code-red@1.0.4: resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} + codemirror-json5@1.0.3: + resolution: {integrity: sha512-HmmoYO2huQxoaoG5ARKjqQc9mz7/qmNPvMbISVfIE2Gk1+4vZQg9X3G6g49MYM5IK00Ol3aijd7OKrySuOkA7Q==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -2704,6 +2707,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lezer-json5@2.0.2: + resolution: {integrity: sha512-NRmtBlKW/f8mA7xatKq8IUOq045t8GVHI4kZXrUtYYUdiVeGiO6zKGAV7/nUAnf5q+rYTY+SWX/gvQdFXMjNxQ==} + lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -5720,6 +5726,16 @@ snapshots: estree-walker: 3.0.3 periscopic: 3.1.0 + codemirror-json5@1.0.3: + dependencies: + '@codemirror/language': 6.11.3 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.6 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + json5: 2.2.3 + lezer-json5: 2.0.2 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -6591,6 +6607,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lezer-json5@2.0.2: + dependencies: + '@lezer/lr': 1.4.2 + lilconfig@2.1.0: {} locate-character@3.0.0: {} diff --git a/src/lib/helpers/faker.ts b/src/lib/helpers/faker.ts index f6fe4b0d1c..07d9ade073 100644 --- a/src/lib/helpers/faker.ts +++ b/src/lib/helpers/faker.ts @@ -105,7 +105,6 @@ function generateDefaultRecord( export function generateFakeRecords( count: number, - type: DatabaseType = 'tablesdb', field?: Field[] ): { ids: string[]; @@ -130,7 +129,7 @@ export function generateFakeRecords( string | number | boolean | Array >; - if (type === 'documentsdb' || filteredColumns.length === 0) { + if (filteredColumns.length === 0) { record = generateDefaultRecord(id); } else { record = { $id: id }; 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 c8830a36e9..06b1589197 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 @@ -8,7 +8,7 @@ import { addNotification } from '$lib/stores/notifications'; import { Input as SuggestionsInput, - tableColumnSuggestions + entityColumnSuggestions } from '$database/(suggestions)/index'; import { getTerminologies } from '../helpers'; @@ -41,12 +41,12 @@ function enableThinkingModeForSuggestions(id: string, name: string) { if (!useSuggestions) return; - if ($tableColumnSuggestions.enabled) { + if ($entityColumnSuggestions.enabled) { // if enabled, trigger thinking mode! - tableColumnSuggestions.update((store) => ({ + entityColumnSuggestions.update((store) => ({ ...store, thinking: true, - table: { + entity: { id, name } @@ -116,10 +116,10 @@ $effect(() => { // reset is OK here, we don't have to check for entity type! - if (show && isOnEntitiesPage && $tableColumnSuggestions.table) { - tableColumnSuggestions.update((store) => ({ + if (show && isOnEntitiesPage && $entityColumnSuggestions.entity) { + entityColumnSuggestions.update((store) => ({ ...store, - table: null + entity: null })); } }); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/columns.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/columns.svelte index 69cc26bcbb..8baafc9c62 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/columns.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/columns.svelte @@ -6,7 +6,7 @@ import Input from './input.svelte'; import { Modal } from '$lib/components'; import { Button } from '$lib/elements/forms'; - import { tableColumnSuggestions } from './store'; + import { entityColumnSuggestions } from './store'; let { show = $bindable(false) @@ -19,17 +19,17 @@ function resetSuggestionsStore() { show = false; - $tableColumnSuggestions.table = null; - $tableColumnSuggestions.context = null; + $entityColumnSuggestions.entity = null; + $entityColumnSuggestions.context = null; - $tableColumnSuggestions.force = false; - $tableColumnSuggestions.enabled = false; - $tableColumnSuggestions.thinking = false; + $entityColumnSuggestions.force = false; + $entityColumnSuggestions.enabled = false; + $entityColumnSuggestions.thinking = false; } async function triggerColumnSuggestions() { // set table info. first! - $tableColumnSuggestions.table = { + $entityColumnSuggestions.entity = { id: page.params.table, name: page.data.table?.name ?? 'Table' }; @@ -48,8 +48,8 @@ ); } - $tableColumnSuggestions.force = true; - $tableColumnSuggestions.enabled = true; + $entityColumnSuggestions.force = true; + $entityColumnSuggestions.enabled = true; show = false; } diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte index fb2c98e6d4..2e97333c0a 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/empty.svelte @@ -27,7 +27,7 @@ type ColumnInput, mapSuggestedColumns, type SuggestedColumnSchema, - tableColumnSuggestions, + entityColumnSuggestions, basicColumnOptions, mockSuggestions, showIndexesSuggestions @@ -607,27 +607,27 @@ }); function resetSuggestionsStore(fullReset: boolean = true) { - if ($tableColumnSuggestions.table?.id !== page.params.table) { + if ($entityColumnSuggestions.entity?.id !== page.params.table) { return; } if (fullReset) { // these are referenced in // `table-[table]/+page.svelte` - $tableColumnSuggestions.table = null; - $tableColumnSuggestions.force = false; - $tableColumnSuggestions.enabled = false; + $entityColumnSuggestions.entity = null; + $entityColumnSuggestions.force = false; + $entityColumnSuggestions.enabled = false; } - $tableColumnSuggestions.context = null; - $tableColumnSuggestions.thinking = false; + $entityColumnSuggestions.context = null; + $entityColumnSuggestions.thinking = false; // reset selection! resetSelectedColumn(); } async function suggestColumns() { - $tableColumnSuggestions.thinking = true; + $entityColumnSuggestions.thinking = true; await tick(); scrollToFirstCustomColumn(); @@ -651,7 +651,7 @@ .console.suggestColumns({ databaseId: page.params.database, tableId: page.params.table, - context: $tableColumnSuggestions.context ?? undefined, + context: $entityColumnSuggestions.context ?? undefined, min: 6 })) as unknown as { total: number; @@ -659,7 +659,7 @@ }; } - const tableName = $tableColumnSuggestions.table?.name ?? undefined; + const tableName = $entityColumnSuggestions.entity?.name ?? undefined; trackEvent(Submit.ColumnSuggestions, { tableName, total: suggestedColumns.total @@ -1298,7 +1298,7 @@ role="none" bind:this={spreadsheetContainer} class:custom-columns={customColumns.length > 0} - class:thinking={$tableColumnSuggestions.thinking} + class:thinking={$entityColumnSuggestions.thinking} class="databases-spreadsheet spreadsheet-container-outer" style:--overlay-icon-color="#fd366e99" style:--non-overlay-icon-color="--fgcolor-neutral-weak" @@ -1309,7 +1309,7 @@ bind:this={rangeOverlayEl} class="columns-range-overlay" class:no-transition={hasTransitioned && customColumns.length > 0} - class:thinking={$tableColumnSuggestions.thinking || creatingColumns}> + class:thinking={$entityColumnSuggestions.thinking || creatingColumns}>
{@render edgeGradients('left')} {@render edgeGradients('right')} @@ -1599,7 +1599,7 @@ data-collapsed-tabs={!$expandTabs}>
- {#if $tableColumnSuggestions.thinking} + {#if $entityColumnSuggestions.thinking}
@@ -1775,7 +1775,7 @@ !isInlineEditing && !$isTabletViewport && !$isSmallViewport && - !$tableColumnSuggestions.thinking && + !$entityColumnSuggestions.thinking && !creatingColumns && hoveredColumnId !== column.id ) { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/input.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/input.svelte index ea5f9ce206..92e9d03fb6 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/input.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/input.svelte @@ -3,7 +3,7 @@ import { isCloud } from '$lib/system'; import IconAI from './icon/ai.svelte'; import { slide } from 'svelte/transition'; - import { tableColumnSuggestions } from './store'; + import { entityColumnSuggestions } from './store'; import { getTerminologies } from '$database/(entity)'; import { Button, InputTextarea } from '$lib/elements/forms'; import { Card, Layout, Selector, Typography } from '@appwrite.io/pink-svelte'; @@ -16,7 +16,7 @@ onMount(() => { if (featureActive) { - $tableColumnSuggestions.enabled = true; + $entityColumnSuggestions.enabled = true; } }); @@ -25,6 +25,7 @@ const type = terminology.type; const field = terminology.field.lower; + const record = terminology.record.lower; const entity = terminology.entity.lower.singular; const title = $derived.by(() => { @@ -45,14 +46,8 @@ const isDocs = type === 'documentsdb'; if (featureActive) { - if (isModal) { - return isDocs - ? `Use AI to generate sample documents` - : `Use AI to suggest useful ${field.plural}`; - } - return isDocs - ? `Enable AI to generate sample documents based on your ${entity} name` + ? `Enable AI to generate sample ${record.plural} based on your ${entity} name` : `Enable AI to suggest useful ${field.plural} based on your ${entity} name`; } @@ -81,7 +76,7 @@ + bind:checked={$entityColumnSuggestions.enabled} />
{/if} @@ -95,13 +90,13 @@ {/if} - {#if $tableColumnSuggestions.enabled && featureActive} + {#if $entityColumnSuggestions.enabled && featureActive}
{/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/store.ts index 0e8daa0362..15185bd24c 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(suggestions)/store.ts @@ -2,14 +2,14 @@ import { writable } from 'svelte/store'; import { IndexType } from '@appwrite.io/console'; import { columnOptions } from '../table-[table]/columns/store'; -export type TableColumnSuggestions = { +export type EntityColumnSuggestions = { force: boolean; enabled: boolean; thinking: boolean; context?: string | undefined; /* for safety when in tables page */ - table?: { + entity?: { id: string; name: string; }; @@ -44,11 +44,11 @@ export type SuggestedIndexSchema = { lengths?: number[] | undefined; }; -export const tableColumnSuggestions = writable({ +export const entityColumnSuggestions = writable({ enabled: false, context: null, thinking: false, - table: null, + entity: null, force: false }); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/highlighting.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/highlighting.ts index 05dc49ab69..b0ae5e32e6 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/highlighting.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/highlighting.ts @@ -5,43 +5,107 @@ import { type ViewUpdate, ViewPlugin } from '@codemirror/view'; -import { Range } from '@codemirror/state'; +import { Range, type Extension } from '@codemirror/state'; import { NESTED_KEY_REGEX } from '../helpers/constants'; // ViewPlugin to highlight nested keys (4+ spaces) only in visible ranges -export const nestedKeyPlugin = ViewPlugin.fromClass( - class { - decorations: DecorationSet; - constructor(view: EditorView) { - this.decorations = this.compute(view); - } - update(update: ViewUpdate) { - if (update.docChanged || update.viewportChanged) { - this.decorations = this.compute(update.view); +export function createNestedKeyPlugin(): Extension { + return ViewPlugin.fromClass( + class { + decorations: DecorationSet; + constructor(view: EditorView) { + this.decorations = this.compute(view); } - } - compute(view: EditorView): DecorationSet { - const decos: Range[] = []; - for (const { from, to } of view.visibleRanges) { - let line = view.state.doc.lineAt(from); - while (line.from <= to) { - const text = line.text; - const m = text.match(NESTED_KEY_REGEX); - if (m) { - const leading = m[1]; - const key = m[2]; - const start = line.from + leading.length; - const end = start + key.length; + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = this.compute(update.view); + } + } + compute(view: EditorView): DecorationSet { + const decos: Range[] = []; + for (const { from, to } of view.visibleRanges) { + let line = view.state.doc.lineAt(from); + while (line.from <= to) { + const text = line.text; + const m = text.match(NESTED_KEY_REGEX); + if (m) { + const leading = m[1]; + const key = m[2]; + const start = line.from + leading.length; + const end = start + key.length; + decos.push( + Decoration.mark({ class: 'cm-nestedPropertyName' }).range( + start, + end + ) + ); + } + if (line.to >= to) break; + line = view.state.doc.line(line.number + 1); + } + } + return Decoration.set(decos); + } + }, + { decorations: (v) => v.decorations } + ); +} + +// ViewPlugin to apply muted styling to system fields ($id, $createdAt, $updatedAt) +export function createSystemFieldStylePlugin(getShouldStyle: () => boolean): Extension { + return ViewPlugin.fromClass( + class { + decorations: DecorationSet; + constructor(view: EditorView) { + this.decorations = this.compute(view); + } + update(update: ViewUpdate) { + // Only recompute when document changes + if (update.docChanged) { + this.decorations = this.compute(update.view); + } + } + compute(view: EditorView): DecorationSet { + const shouldStyle = getShouldStyle(); + + if (!shouldStyle) { + return Decoration.none; + } + + const doc = view.state.doc; + const text = doc.toString(); + const systemFields = ['$id', '$createdAt', '$updatedAt']; + const decos: Range[] = []; + + // Find all occurrences of system field keys + for (const field of systemFields) { + // Match the key in format: "$id": or $id: (with or without quotes) + const quotedPattern = new RegExp(`"${field.replace('$', '\\$')}"\\s*:`, 'g'); + const unquotedPattern = new RegExp(`${field.replace('$', '\\$')}\\s*:`, 'g'); + + let match: RegExpExecArray; + // Check quoted format + while ((match = quotedPattern.exec(text)) !== null) { + const from = match.index; + const to = from + field.length + 2; // +2 for quotes decos.push( - Decoration.mark({ class: 'cm-nestedPropertyName' }).range(start, end) + Decoration.mark({ class: 'cm-system-field-muted' }).range(from, to) + ); + } + + // Check unquoted format + while ((match = unquotedPattern.exec(text)) !== null) { + const from = match.index; + const to = from + field.length; + decos.push( + Decoration.mark({ class: 'cm-system-field-muted' }).range(from, to) ); } - if (line.to >= to) break; - line = view.state.doc.line(line.number + 1); } + + return Decoration.set(decos); } - return Decoration.set(decos); - } - }, - { decorations: (v) => v.decorations } -); + }, + { decorations: (v) => v.decorations } + ); +} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/index.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/index.ts index d9030871ae..c992339b00 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/index.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/index.ts @@ -5,4 +5,4 @@ export { createReadOnlyRangesFilter } from './readonly'; -export { nestedKeyPlugin } from './highlighting'; +export { createNestedKeyPlugin, createSystemFieldStylePlugin } from './highlighting'; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/validators/json5.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/validators/json5.ts deleted file mode 100644 index 572e38ebb9..0000000000 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/validators/json5.ts +++ /dev/null @@ -1,73 +0,0 @@ -import JSON5 from 'json5'; - -export interface Diagnostic { - from: number; - to: number; - message: string; -} - -export interface ValidatorResult { - ok: boolean; - diagnostics: Diagnostic[]; - parsed?: TParsed; - meta?: Record; -} - -async function validate(text: string): Promise { - try { - const parsed = JSON5.parse(text); - return { ok: true, diagnostics: [], parsed }; - } catch (err) { - const line: number | undefined = err?.lineNumber; - - if (!line) { - return { - ok: false, - diagnostics: [ - { - from: 0, - to: text.length, - message: err?.message || 'Syntax error' - } - ] - }; - } - - const lines = text.split('\n'); - - /** - * we highlight the previous line instead because sometimes, - * the reported line is the NEXT line as that's where the validator encounters an error! - */ - const targetLineIndex = Math.max(0, line - 2); - - // calculate line start position - let lineStartPos = 0; - for (let i = 0; i < targetLineIndex; i++) { - lineStartPos += lines[i].length + 1; - } - - // highlight the whole line (trimmed) - const targetLine = lines[targetLineIndex] || ''; - const trimmedStart = targetLine.trimStart(); - const leadingWhitespace = targetLine.length - trimmedStart.length; - const lineEndPos = lineStartPos + leadingWhitespace + trimmedStart.trimEnd().length; - - const diagnostic: Diagnostic = { - from: lineStartPos + leadingWhitespace, - to: lineEndPos, - message: (err?.message || 'Syntax error').replace(/^JSON5:\s*/i, '') - }; - - return { ok: false, diagnostics: [diagnostic] }; - } -} - -export async function parse(text: string): Promise { - const res = await validate(text); - if (!res.ok) - throw Object.assign(new Error(res.diagnostics[0]?.message || 'Invalid JSON5'), { - diagnostics: res.diagnostics - }); - return res.parsed as T; -} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte index 54a214a162..9dc475ad96 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte @@ -3,16 +3,6 @@ export type JsonObject = { [key: string]: JsonValue }; export type JsonArray = JsonValue[]; - type ParseResult = - | { - ok: true; - value: JsonValue; - } - | { - ok: false; - error: unknown; - }; - type Hit = { key: string; valueFrom: number; @@ -36,12 +26,13 @@ bracketMatching, foldGutter, foldEffect, + foldAll, + unfoldAll, indentOnInput, indentUnit } from '@codemirror/language'; import { highlightSelectionMatches } from '@codemirror/search'; import { closeBrackets } from '@codemirror/autocomplete'; - import { javascript } from '@codemirror/lang-javascript'; import { type Diagnostic, linter } from '@codemirror/lint'; import { EditorState, @@ -59,14 +50,14 @@ import { copy } from '$lib/helpers/copy'; import { isSmallViewport } from '$lib/stores/viewport'; - import { parse } from './validators/json5'; import { customTheme, customSyntaxHighlighting } from './helpers/theme'; import { createEditorKeymaps, secondaryKeymaps } from './helpers/keymaps'; import { createReadOnlyRangesField, createReadOnlyLineField, createReadOnlyRangesFilter, - nestedKeyPlugin + createNestedKeyPlugin, + createSystemFieldStylePlugin } from './extensions'; import { ALLOWED_DOLLAR_PROPS, @@ -83,8 +74,9 @@ } from './helpers/constants'; import { toLocaleDateTime } from '$lib/helpers/date'; import { ID } from '@appwrite.io/console'; - import { Error as ErrorSonner, Save as SavingSonner } from '../sonners'; + import { Suggestions, Error as ErrorSonner, Save as SavingSonner } from '../sonners'; import { sleep } from '$lib/helpers/promises'; + import { json5, json5ParseCache, json5ParseLinter } from 'codemirror-json5'; interface Props { isNew?: boolean; @@ -99,6 +91,8 @@ errorInPlace?: boolean; ctrlSave?: boolean; showHeaderActions?: boolean; + showSuggestions?: boolean; + suggestedAttributes?: string[]; } let { @@ -113,7 +107,9 @@ wrapLines = true, errorInPlace = true, ctrlSave = false, - showHeaderActions = true + showHeaderActions = true, + showSuggestions = false, + suggestedAttributes = [] }: Props = $props(); let editorContainer: HTMLDivElement = $state(null); @@ -130,6 +126,9 @@ let saveSonnerState: 'saving' | 'saved' | null = $state(null); + let hasUserContent = $state(false); + let hasStartedEditing = $state(false); + const wrapCompartment = new Compartment(); const readOnlyCompartment = new Compartment(); @@ -147,10 +146,6 @@ // Generate a stable ID once for new documents let generatedId = $state(ID.unique()); - // Content cache - let lastParseContent = ''; - let lastParsePromise: Promise | null = null; - // Serialized data cache let lastSerializedText = ''; let originalSerialized = $state(''); @@ -724,63 +719,77 @@ const readOnlyRangesField = createReadOnlyRangesField(isNew); const readOnlyLineField = createReadOnlyLineField(readOnlyRangesField); - function parseWithCache(content: string): Promise { - if (lastParsePromise && content === lastParseContent) { - return lastParsePromise; - } - - lastParseContent = content; - lastParsePromise = (async () => { - try { - const value = await parse(content); - return { ok: true, value }; - } catch (error) { - return { ok: false, error }; - } - })(); - - return lastParsePromise; - } - - function isParseError(result: ParseResult): result is { ok: false; error: unknown } { - return !result.ok; + // Check if document has user-added content beyond system fields + function hasCustomContent(parsed: JsonValue): boolean { + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false; + const keys = Object.keys(parsed as JsonObject); + const systemKeys = new Set([ + '$id', + '$createdAt', + '$updatedAt', + '$collectionId', + '$databaseId', + '$permissions' + ]); + return keys.some((key) => !systemKeys.has(key)); } - // Safe parse variant that indicates success without mutating editor on failure - async function tryParseEditorContent( - content: string - ): Promise<{ ok: boolean; value: JsonValue }> { - const result = await parseWithCache(content); - if (!isParseError(result)) { - return result; - } - - return { ok: false, value: data }; - } + const baseJson5Linter = json5ParseLinter(); - // JavaScript linter to validate syntax (JSON5-based; precise squiggle when errorInPlace) - async function javascriptLinter(view: { state: { doc: Text } }): Promise { + // JSON5 linter using the parse cache; preserves errorInPlace behavior. + async function json5Linter(view: EditorView): Promise { if (isUpdatingFromEditor) return []; - const content = view.state.doc.toString(); - const result = await parseWithCache(content); - if (!isParseError(result)) { + const result = await baseJson5Linter(view); + if (!result.length) { errorMessage = null; return []; } - const errorMsg = - result.error instanceof Error ? result.error.message : String(result.error); + const errorMsg = (result[0]?.message || 'Syntax error').replace(/^JSON5:\s*/i, ''); errorMessage = errorMsg; - const diags = ( - result.error && typeof result.error === 'object' && 'diagnostics' in result.error - ? (result.error as { diagnostics?: { from: number; to: number }[] }).diagnostics - : undefined - ) as { from: number; to: number }[] | undefined; - if (diags && diags.length && errorInPlace) { - const { from, to } = diags[0]; - return [{ from, to, severity: 'error', message: errorMsg }]; + if (errorInPlace) { + return result; } // Fallback to full-doc underline - return [{ from: 0, to: content.length, severity: 'error', message: errorMsg }]; + return [{ from: 0, to: view.state.doc.length, severity: 'error', message: errorMsg }]; + } + + // Apply suggested attributes to the document + function applySuggestedAttributes() { + if (!editorView || !isNew || suggestedAttributes.length === 0) { + return; + } + + // Create an object with suggested attributes as empty strings + const suggestedObject: Record = {}; + for (const attr of suggestedAttributes) { + suggestedObject[attr] = ''; + } + + // Merge with existing data (keeping system fields) + const updatedData = { + ...(typeof data === 'object' && data !== null && !Array.isArray(data) ? data : {}), + ...suggestedObject + }; + + // Update the data + data = updatedData; + + // Manually update the editor content + const newContent = serializeData(updatedData); + + if (editorView) { + // Save current cursor position + const currentSelection = editorView.state.selection.main; + const currentContent = editorView.state.doc.toString(); + + editorView.dispatch({ + changes: { from: 0, to: currentContent.length, insert: newContent }, + selection: { anchor: currentSelection.anchor, head: currentSelection.head } + }); + } + + // Hide the suggestions bar after applying + hasStartedEditing = false; } // Handle save logic - called from both button and keyboard shortcut @@ -822,13 +831,14 @@ history(), bracketMatching(), closeBrackets(), - linter(javascriptLinter, { delay: LINTER_DELAY }), + linter(json5Linter, { delay: LINTER_DELAY }), readOnlyRangesField, EditorState.transactionFilter.of( createReadOnlyRangesFilter(readOnlyRangesField, readonly) ), readOnlyLineField, - nestedKeyPlugin, + createNestedKeyPlugin(), + createSystemFieldStylePlugin(() => isNew && !hasUserContent), highlightSelectionMatches(), // Clear selection after fold/unfold to prevent split highlighting EditorView.updateListener.of((update) => { @@ -845,13 +855,52 @@ keymap.of( createEditorKeymaps(insertNewlineKeepIndent, ctrlSave ? handleSave : undefined) ), + // Add Cmd+K for applying suggestions + keymap.of([ + { + key: 'Mod-=', + preventDefault: true, + run: unfoldAll + }, + { + key: 'Mod-+', + preventDefault: true, + run: unfoldAll + }, + { + key: 'Mod--', + preventDefault: true, + run: foldAll + }, + { + key: 'Mod-k', + preventDefault: true, + run: () => { + if (showSuggestions && hasStartedEditing) { + applySuggestedAttributes(); + return true; + } + return false; + } + } + ]), keymap.of(secondaryKeymaps), - javascript(), + json5(), customSyntaxHighlighting, customTheme, wrapCompartment.of(wrapLines ? EditorView.lineWrapping : []), EditorView.updateListener.of((update) => { if (!update.docChanged || readonly) return; + + if (isNew && !isUpdatingFromEditor) { + hasStartedEditing = true; + + const parseCache = update.state.field(json5ParseCache, false); + if (!parseCache?.err && parseCache?.obj !== undefined) { + hasUserContent = hasCustomContent(parseCache.obj as JsonValue); + } + } + // First, expand `: {}` / `: []` patterns when they appear if (!isUpdatingFromEditor && maybeExpandEmptyLiteral(update)) { return; @@ -879,13 +928,13 @@ changeTimer = setTimeout(async () => { const state = update.view.state; - const newContent = state.doc.toString(); - const res = await tryParseEditorContent(newContent); - if (!res.ok) { + const parseCache = state.field(json5ParseCache, false); + + if (!parseCache || parseCache.err || parseCache.obj === undefined) { return; // linter will surface the error } - const parsed = preserveSystemValues(res.value); + const parsed = preserveSystemValues(parseCache.obj as JsonValue); if (pendingCanonicalize) { // Patch only top-level $ system fields in-place to avoid reflow @@ -913,7 +962,7 @@ // Set new auto-save timer autoSaveTimer = setTimeout(() => { - handleSave(); + // handleSave(); autoSaveTimer = null; }, AUTOSAVE_DELAY); } @@ -946,8 +995,6 @@ clearTimeout(tooltipTimer); tooltipTimer = null; } - lastParseContent = ''; - lastParsePromise = null; lastSerializedData = null; lastSerializedText = ''; editorView?.destroy(); @@ -959,6 +1006,8 @@ if (isNew && !wasNew) { originalData = $state.snapshot(data); generatedId = ID.unique(); + hasStartedEditing = false; // Reset editing flag for new document + hasUserContent = false; } wasNew = isNew; }); @@ -971,35 +1020,31 @@ if (documentId !== lastDocId) { lastDocId = documentId; - lastParseContent = ''; - lastParsePromise = null; lastSerializedData = null; lastSerializedText = ''; + hasUserContent = false; - // For existing documents only: - // capture snapshot and reset editor state/history - if (!isNew) { - // Capture original data snapshot when switching documents - originalData = $state.snapshot(data); + // Capture original data snapshot when switching documents + originalData = $state.snapshot(data); - lastExpectedContent = expectedContent; + lastExpectedContent = expectedContent; - if (changeTimer) { - clearTimeout(changeTimer); - changeTimer = null; - } + if (changeTimer) { + clearTimeout(changeTimer); + changeTimer = null; + } - pendingCanonicalize = false; - isUpdatingFromEditor = true; + pendingCanonicalize = false; + isUpdatingFromEditor = true; - const newState = EditorState.create({ - doc: expectedContent, - extensions: baseExtensions - }); - editorView.setState(newState); - queueMicrotask(() => (isUpdatingFromEditor = false)); - return; - } + // Reset editor state and history for both new and existing documents + const newState = EditorState.create({ + doc: expectedContent, + extensions: baseExtensions + }); + editorView.setState(newState); + queueMicrotask(() => (isUpdatingFromEditor = false)); + return; } // Only react when the external data actually changed @@ -1143,6 +1188,14 @@ {/if}
+ + {#if showSuggestions && hasStartedEditing} +
+ +
+ {/if}
{#if errorMessage && !loading} @@ -1157,6 +1210,7 @@ height: 100%; overflow: hidden; flex-direction: column; + position: relative; &.loading { overflow: visible; @@ -1313,6 +1367,14 @@ font-weight: 500; } + // System fields muted styling (when suggestions are showing) + // Must come after .cm-propertyName to override + :global(.cm-system-field-muted), + :global(.cm-system-field-muted.cm-propertyName), + :global(.cm-system-field-muted .cm-propertyName) { + color: var(--fgcolor-neutral-tertiary, #97979b) !important; + } + // All value-level tokens use mint color (strings, numbers, etc.) :global(.cm-string) { color: var(--brand-mint-600); 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 67017765e0..cc1daf60ad 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,2 +1,3 @@ export { default as Save } from './save.svelte'; export { default as Error } from './error.svelte'; +export { default as Suggestions } from './suggestions.svelte'; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/suggestions.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/suggestions.svelte new file mode 100644 index 0000000000..e5f6723c0f --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/suggestions.svelte @@ -0,0 +1,53 @@ + + +{#if show} +
+ + + + Press + + + + + + + + for suggestions + + + +
+{/if} + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte index 3054fc7c08..3d06a2052b 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+layout.svelte @@ -34,7 +34,20 @@ documentActivitySheet, documentPermissionSheet } from '$database/collection-[collection]/store'; - import { SideSheet, EditRecordPermissions, RecordActivity } from '$database/(entity)'; + import { + SideSheet, + EditRecordPermissions, + RecordActivity, + type Field + } from '$database/(entity)'; + import { + entityColumnSuggestions, + type ColumnInput, + mapSuggestedColumns, + mockSuggestions + } from '$database/(suggestions)'; + import { VARS } from '$lib/system'; + import { Submit, trackEvent, trackError } from '$lib/actions/analytics'; export let data: LayoutData; @@ -58,17 +71,17 @@ // set faker method. $randomDataModalState.onSubmit = async () => await createFakeData(); + if ( + $entityColumnSuggestions.enabled && + $entityColumnSuggestions.thinking && + $entityColumnSuggestions.entity?.id === collection.$id + ) { + createSampleDocuments(); + } + return realtime.forProject(page.params.region, ['project', 'console'], (response) => { if (response.events.includes('documentsdb.*.collections.*.indexes.*')) { - // don't invalidate when - - // 1. from faker - // 2. ai indexes creation - // 3. ai columns creation - if ( - !isWaterfallFromFaker /*&& - !$showIndexesSuggestions && - !$tableColumnSuggestions.table*/ - ) { + if (!isWaterfallFromFaker && !$entityColumnSuggestions.entity) { invalidate(Dependencies.COLLECTION); } } @@ -173,6 +186,89 @@ indexes: 700 }); + async function createSampleDocuments() { + $spreadsheetLoading = true; + isWaterfallFromFaker = true; + + let suggestedColumns: { total: number; columns: ColumnInput[] } = { + total: 0, + columns: [] + }; + + try { + if (VARS.MOCK_AI_SUGGESTIONS) { + await sleep(1250); + suggestedColumns = mockSuggestions; + } else { + suggestedColumns = (await sdk + .forProject(page.params.region, page.params.project) + .console.suggestColumns({ + databaseId: page.params.database, + tableId: page.params.collection, + context: $entityColumnSuggestions.context ?? undefined, + min: 6 + })) as unknown as { + total: number; + columns: ColumnInput[]; + }; + } + + const collectionName = $entityColumnSuggestions.entity?.name ?? undefined; + trackEvent(Submit.ColumnSuggestions, { + collectionName, + total: suggestedColumns.total + }); + + const mappedColumns = mapSuggestedColumns(suggestedColumns.columns); + const fields = mappedColumns.map((col) => ({ + key: col.key, + type: col.type, + required: col.required, + array: col.array, + size: col.size, + min: col.min, + max: col.max, + format: col.format, + elements: col.elements, + status: 'available' + })) as Field[]; + + // TODO: @itznotabug - maybe we should show a seekbar + const { rows } = generateFakeRecords(100, fields); + + await sdk + .forProject(page.params.region, page.params.project) + .documentsDB.createDocuments({ + databaseId: page.params.database, + collectionId: page.params.collection, + documents: rows + }); + + addNotification({ + type: 'success', + message: 'Sample data added successfully with AI-suggested attributes' + }); + + await invalidate(Dependencies.DOCUMENTS); + } catch (e) { + addNotification({ + type: 'error', + message: e.message + }); + trackError(e, Submit.ColumnSuggestions); + } finally { + // Reset suggestion state + entityColumnSuggestions.update((store) => ({ + ...store, + thinking: false, + entity: null + })); + + $spreadsheetLoading = false; + isWaterfallFromFaker = false; + } + } + async function createFakeData() { isWaterfallFromFaker = true; @@ -184,7 +280,7 @@ let documentIds = []; try { - const { rows, ids } = generateFakeRecords($randomDataModalState.value, 'documentsdb'); + const { rows, ids } = generateFakeRecords($randomDataModalState.value); documentIds = ids; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index 5058dc4c08..f56ea88aa5 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -63,6 +63,7 @@ type HeaderCellAction, type RowCellAction } from '$database/(entity)'; + import { fuzzySearchKeys } from '$lib/helpers/search'; export let data: PageData; @@ -521,6 +522,13 @@ $: rowSelection = !$spreadsheetLoading && !$paginatedDocumentsLoading ? true : ('disabled' as const); + + $: suggestedAttributes = + $noSqlDocument.isNew && $documents?.documents + ? (fuzzySearchKeys($documents.documents, { minOccurrences: 2 }) ?? []) + : []; + + $: showSuggestions = $noSqlDocument.isNew && suggestedAttributes.length > 0; noSqlDocument.reset()} onSave={async (document) => await createOrUpdateDocument(document)} onChange={(_, hasDataChanged) => noSqlDocument.update({ hasDataChanged })} /> 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 9df9a085c9..52e5533ecd 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 @@ -338,11 +338,7 @@ let rowIds = []; try { - const { rows, ids } = generateFakeRecords( - $randomDataModalState.value, - 'tablesdb', - columns - ); + const { rows, ids } = generateFakeRecords($randomDataModalState.value, columns); rowIds = ids; const tablesSDK = sdk.forProject(page.params.region, page.params.project).tablesDB; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte index ee279fedfa..fc0f0172fe 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte @@ -38,7 +38,7 @@ import { EmptySheet, EmptySheetCards, type Field } from '$database/(entity)'; import { Empty as SuggestionsEmptySheet, - tableColumnSuggestions, + entityColumnSuggestions, showColumnsSuggestionsModal } from '$database/(suggestions)'; import { expandTabs, randomDataModalState } from '$database/store'; @@ -97,9 +97,9 @@ $: canShowSuggestionsSheet = // enabled, has table details // and it matches current table - $tableColumnSuggestions.enabled && - $tableColumnSuggestions.table && - $tableColumnSuggestions.table.id === page.params.table; + $entityColumnSuggestions.enabled && + $entityColumnSuggestions.entity && + $entityColumnSuggestions.entity.id === page.params.table; $: disableButton = canShowSuggestionsSheet; @@ -249,7 +249,7 @@
- {#if hasColumns && hasValidColumns && $tableColumnSuggestions.force !== true} + {#if hasColumns && hasValidColumns && $entityColumnSuggestions.force !== true} {#if data.rows.total} From fa9d6bef2114dbac932a1a78488a6932621c6c1c Mon Sep 17 00:00:00 2001 From: Darshan Date: Mon, 19 Jan 2026 14:54:58 +0530 Subject: [PATCH 44/60] add: duplicate linter. fix: a bug on autocomplete + backspace adding excess commas. add: duplicate content shortcut. --- package.json | 2 + pnpm-lock.yaml | 30 ++-- .../editor/extensions/duplicates.ts | 100 +++++++++++ .../(components)/editor/extensions/index.ts | 1 + .../(components)/editor/helpers/constants.ts | 3 +- .../(components)/editor/helpers/keymaps.ts | 46 +++++ .../(components)/editor/view.svelte | 167 +++++++++++++++++- .../(components)/sonners/error.svelte | 18 +- .../(components)/sonners/suggestions.svelte | 2 +- 9 files changed, 340 insertions(+), 29 deletions(-) create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/duplicates.ts diff --git a/package.json b/package.json index e63db1f15d..11c3cf3d1a 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "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", @@ -64,6 +65,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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94b8a0f5f7..9bbebe40d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,6 +101,9 @@ importers: ignore: specifier: ^6.0.2 version: 6.0.2 + json5: + specifier: ^2.2.3 + version: 2.2.3 nanoid: specifier: ^5.1.5 version: 5.1.5 @@ -132,6 +135,9 @@ importers: '@eslint/js': specifier: ^9.31.0 version: 9.31.0 + '@lezer/common': + specifier: ^1.5.0 + version: 1.5.0 '@melt-ui/pp': specifier: ^0.3.2 version: 0.3.2(@melt-ui/svelte@0.86.5(svelte@5.25.3))(svelte@5.25.3) @@ -734,8 +740,8 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@lezer/common@1.2.3': - resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} + '@lezer/common@1.5.0': + resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==} '@lezer/highlight@1.2.1': resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==} @@ -4073,14 +4079,14 @@ snapshots: '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.6 - '@lezer/common': 1.2.3 + '@lezer/common': 1.5.0 '@codemirror/commands@6.9.0': dependencies: '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.6 - '@lezer/common': 1.2.3 + '@lezer/common': 1.5.0 '@codemirror/lang-javascript@6.2.4': dependencies: @@ -4089,7 +4095,7 @@ snapshots: '@codemirror/lint': 6.9.0 '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.6 - '@lezer/common': 1.2.3 + '@lezer/common': 1.5.0 '@lezer/javascript': 1.5.4 '@codemirror/lang-json@6.0.2': @@ -4101,7 +4107,7 @@ snapshots: dependencies: '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.6 - '@lezer/common': 1.2.3 + '@lezer/common': 1.5.0 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 style-mod: 4.1.2 @@ -4323,27 +4329,27 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@lezer/common@1.2.3': {} + '@lezer/common@1.5.0': {} '@lezer/highlight@1.2.1': dependencies: - '@lezer/common': 1.2.3 + '@lezer/common': 1.5.0 '@lezer/javascript@1.5.4': dependencies: - '@lezer/common': 1.2.3 + '@lezer/common': 1.5.0 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 '@lezer/json@1.0.3': dependencies: - '@lezer/common': 1.2.3 + '@lezer/common': 1.5.0 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 '@lezer/lr@1.4.2': dependencies: - '@lezer/common': 1.2.3 + '@lezer/common': 1.5.0 '@marijn/find-cluster-break@1.0.2': {} @@ -5731,7 +5737,7 @@ snapshots: '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.6 - '@lezer/common': 1.2.3 + '@lezer/common': 1.5.0 '@lezer/highlight': 1.2.1 json5: 2.2.3 lezer-json5: 2.0.2 diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/duplicates.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/duplicates.ts new file mode 100644 index 0000000000..996b00b0d6 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/duplicates.ts @@ -0,0 +1,100 @@ +import { json5ParseCache } from 'codemirror-json5'; +import { ensureSyntaxTree } from '@codemirror/language'; +import { linter, type Diagnostic } from '@codemirror/lint'; +import type { SyntaxNode, TreeCursor } from '@lezer/common'; +import type { EditorState, Extension } from '@codemirror/state'; + +type Options = { + delay?: number; + timeBudgetMs?: number; + maxDocLength?: number; +}; + +const DEFAULT_TIME_BUDGET_MS = 200; +const CHECK_BUDGET_EVERY = 200; + +function normalizeKey(raw: string): string { + const trimmed = raw.trim(); + if (trimmed.length < 2) return trimmed; + const first = trimmed[0]; + const last = trimmed[trimmed.length - 1]; + if ((first === '"' && last === '"') || (first === "'" && last === "'")) { + return trimmed.slice(1, -1); + } + return trimmed; +} + +function nowMs(): number { + return typeof performance !== 'undefined' ? performance.now() : Date.now(); +} + +function readPropertyName( + node: SyntaxNode, + state: EditorState +): { key: string; from: number; to: number } | null { + const propName = node.getChild('PropertyName'); + if (!propName) return null; + const raw = state.doc.sliceString(propName.from, propName.to); + return { key: normalizeKey(raw), from: propName.from, to: propName.to }; +} + +function collectDuplicateKeys( + state: EditorState, + cursor: TreeCursor, + deadlineMs: number +): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + const walker = cursor; + let visited = 0; + + do { + if (visited % CHECK_BUDGET_EVERY === 0 && nowMs() > deadlineMs) { + break; + } + visited += 1; + if (walker.name === 'Object') { + const objectNode = walker.node; + const objectCursor = objectNode.cursor(); + const seen = new Map(); + if (objectCursor.firstChild()) { + do { + if (objectCursor.name === 'Property') { + const keyInfo = readPropertyName(objectCursor.node, state); + if (!keyInfo || !keyInfo.key) continue; + const previous = seen.get(keyInfo.key); + if (previous) { + diagnostics.push({ + from: keyInfo.from, + to: keyInfo.to, + severity: 'warning', + message: `Duplicate key "${keyInfo.key}"` + }); + } else { + seen.set(keyInfo.key, { from: keyInfo.from, to: keyInfo.to }); + } + } + } while (objectCursor.nextSibling()); + } + } + } while (walker.next()); + + return diagnostics; +} + +export function createDuplicateKeyLinter(options: Options = {}): Extension { + const timeBudgetMs = options.timeBudgetMs ?? DEFAULT_TIME_BUDGET_MS; + return linter( + (view) => { + if (options.maxDocLength && view.state.doc.length > options.maxDocLength) { + return []; + } + const parseCache = view.state.field(json5ParseCache, false); + if (parseCache?.err) return []; + const tree = ensureSyntaxTree(view.state, view.state.doc.length, timeBudgetMs); + if (!tree) return []; + const deadlineMs = nowMs() + timeBudgetMs; + return collectDuplicateKeys(view.state, tree.cursor(), deadlineMs); + }, + { delay: options.delay } + ); +} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/index.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/index.ts index c992339b00..f92fe28e5d 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/index.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/extensions/index.ts @@ -5,4 +5,5 @@ export { createReadOnlyRangesFilter } from './readonly'; +export { createDuplicateKeyLinter } from './duplicates'; export { createNestedKeyPlugin, createSystemFieldStylePlugin } from './highlighting'; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/constants.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/constants.ts index 05b210f4c6..2d38cf3b66 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/constants.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/constants.ts @@ -3,9 +3,10 @@ export const ALLOWED_DOLLAR_PROPS = ['$id', '$createdAt', '$updatedAt'] as const export const SYSTEM_KEYS = new Set(['$id:', '$createdAt:', '$updatedAt:']); // timing constants -export const DEBOUNCE_DELAY = 200; export const LINTER_DELAY = 250; +export const DEBOUNCE_DELAY = 200; export const AUTOSAVE_DELAY = 2000; +export const SUGGESTIONS_HIDE_DELAY = 3000; // regex patterns /* export const UNQUOTED_KEY_REGEX = /([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g; */ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/keymaps.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/keymaps.ts index dcbfa51769..8cc7695ae1 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/keymaps.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/helpers/keymaps.ts @@ -1,8 +1,52 @@ /*import { searchKeymap } from '@codemirror/search';*/ import { closeBracketsKeymap } from '@codemirror/autocomplete'; import type { EditorView, KeyBinding } from '@codemirror/view'; +import { EditorSelection, Transaction } from '@codemirror/state'; import { defaultKeymap, historyKeymap, indentLess, indentMore } from '@codemirror/commands'; +function duplicateSelectionOrLine(view: EditorView): boolean { + const state = view.state; + + const transaction = state.changeByRange((range) => { + if (range.empty) { + const line = state.doc.lineAt(range.head); + const column = range.head - line.from; + const insertText = `\n${line.text}`; + const insertPos = line.to; + const newPos = insertPos + 1 + column; + + return { + changes: { from: insertPos, insert: insertText }, + range: EditorSelection.cursor(newPos) + }; + } + + const insertText = state.doc.sliceString(range.from, range.to); + const insertPos = range.to; + const newFrom = range.from + insertText.length; + const newTo = range.to + insertText.length; + + return { + changes: { from: insertPos, insert: insertText }, + range: EditorSelection.range(newFrom, newTo) + }; + }); + + view.dispatch({ + ...transaction, + annotations: Transaction.userEvent.of('input') + }); + return true; +} + +function createDuplicateLineKeymap(): KeyBinding { + return { + key: 'Mod-d', + preventDefault: true, + run: duplicateSelectionOrLine + }; +} + // main editor keymaps, // these require functions from the component export function createEditorKeymaps( @@ -35,6 +79,8 @@ export function createEditorKeymaps( run: () => true }); + keymaps.push(createDuplicateLineKeymap()); + return keymaps; } diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte index 9dc475ad96..cd78a6a826 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte @@ -33,7 +33,7 @@ } from '@codemirror/language'; import { highlightSelectionMatches } from '@codemirror/search'; import { closeBrackets } from '@codemirror/autocomplete'; - import { type Diagnostic, linter } from '@codemirror/lint'; + import { forEachDiagnostic, type Diagnostic, linter } from '@codemirror/lint'; import { EditorState, EditorSelection, @@ -57,6 +57,7 @@ createReadOnlyLineField, createReadOnlyRangesFilter, createNestedKeyPlugin, + createDuplicateKeyLinter, createSystemFieldStylePlugin } from './extensions'; import { @@ -64,6 +65,7 @@ DEBOUNCE_DELAY, LINTER_DELAY, AUTOSAVE_DELAY, + SUGGESTIONS_HIDE_DELAY, INDENT_REGEX, SCALAR_VALUE_REGEX, TRAILING_COMMA_REGEX, @@ -116,9 +118,11 @@ let editorView: EditorView | null = null; let errorMessage = $state(null); + let warningMessage = $state(null); let changeTimer: ReturnType | null = null; // debounce timer for parse + onChange let autoSaveTimer: ReturnType | null = null; // debounce timer for auto-save let tooltipTimer: ReturnType | null = null; // timer for tooltip message reset + let suggestionsHideTimer: ReturnType | null = null; // timer to auto-hide suggestions let pendingCanonicalize = false; // set when a full-document replace (paste-all) occurs let lastExpectedContent = ''; // track latest serialized data to avoid spurious rewrites let lastDocId: string | null = null; // track current document identity for history reset @@ -128,6 +132,7 @@ let hasUserContent = $state(false); let hasStartedEditing = $state(false); + let hasSuggestionsBeenShown = $state(false); // Track if suggestions were already shown for this document const wrapCompartment = new Compartment(); const readOnlyCompartment = new Compartment(); @@ -376,6 +381,62 @@ return did; } + // Check if line is a valid key for auto-completion + function isValidKeyForAutoComplete(lineText: string, cursorOffset: number): boolean { + // skip system fields (starting with $) + if (lineText.trimStart().startsWith('$')) return false; + + // pattern: key name followed by colon at end + const beforeCursor = lineText.slice(0, cursorOffset); + const match = /([A-Za-z_$][A-Za-z0-9_$]*)\s*:\s*$/.exec(beforeCursor); + return match !== null; + } + + // Check if line already has completion artifacts + function lineHasCompletion(lineText: string, cursorOffset: number): boolean { + const afterCursor = lineText.slice(cursorOffset); + return afterCursor.includes('"') || afterCursor.includes(','); + } + + // Auto-complete key with empty string: `key:` -> `key: "",` with cursor between quotes + function maybeAutoCompleteKeyValue(update: ViewUpdate): boolean { + const doc = update.state.doc; + let did = false; + + update.changes.iterChanges( + (fromA: number, toA: number, fromB: number, toB: number, inserted: Text) => { + if (did) return; + + // Only trigger on insertion (not deletion) + if (toA >= fromA && toB <= fromB) return; + + // Check if `:` was just typed + const insertedText = inserted.toString(); + if (!insertedText.endsWith(':')) return; + + const line = doc.lineAt(toB); + const lineText = line.text; + const cursorOffset = toB - line.from; + + // Validate line is suitable for auto-completion + if (!isValidKeyForAutoComplete(lineText, cursorOffset)) return; + if (lineHasCompletion(lineText, cursorOffset)) return; + + // Insert space, empty quotes, and comma + const replacement = ' "",'; + const cursorPos = toB + 2; // Position cursor between the quotes + + update.view.dispatch({ + changes: { from: toB, to: toB, insert: replacement }, + selection: EditorSelection.cursor(cursorPos), + userEvent: 'input' + }); + did = true; + } + ); + return did; + } + // Detect indentation from first line after opening brace function detectIndentation(content: string, openIdx: number, closeIdx: number): string { const afterOpenNL = content.indexOf('\n', openIdx); @@ -734,6 +795,25 @@ return keys.some((key) => !systemKeys.has(key)); } + function getLintWarningSummary(state: EditorState): { + message: string | null; + hasWarning: boolean; + } { + let message: string | null = null; + let count = 0; + forEachDiagnostic(state, (diagnostic) => { + if (diagnostic.severity === 'warning') { + count += 1; + if (!message) { + message = diagnostic.message; + } + } + }); + if (count === 0) return { message: null, hasWarning: false }; + if (count === 1) return { message, hasWarning: true }; + return { message: 'Duplicate keys detected', hasWarning: true }; + } + const baseJson5Linter = json5ParseLinter(); // JSON5 linter using the parse cache; preserves errorInPlace behavior. @@ -790,11 +870,21 @@ // Hide the suggestions bar after applying hasStartedEditing = false; + hasSuggestionsBeenShown = true; + + // Clear the auto-hide timer + if (suggestionsHideTimer) { + clearTimeout(suggestionsHideTimer); + suggestionsHideTimer = null; + } } // Handle save logic - called from both button and keyboard shortcut async function handleSave(): Promise { if (!hasDataChanged) return; + const parseCache = editorView?.state.field(json5ParseCache, false); + if (parseCache?.err || errorMessage) return; + if (editorView && getLintWarningSummary(editorView.state).hasWarning) return; isSaving = true; @@ -838,6 +928,7 @@ ), readOnlyLineField, createNestedKeyPlugin(), + createDuplicateKeyLinter({ delay: LINTER_DELAY }), createSystemFieldStylePlugin(() => isNew && !hasUserContent), highlightSelectionMatches(), // Clear selection after fold/unfold to prevent split highlighting @@ -855,7 +946,7 @@ keymap.of( createEditorKeymaps(insertNewlineKeepIndent, ctrlSave ? handleSave : undefined) ), - // Add Cmd+K for applying suggestions + // Add Cmd+A for applying suggestions and Esc to hide keymap.of([ { key: 'Mod-=', @@ -873,7 +964,7 @@ run: foldAll }, { - key: 'Mod-k', + key: 'Mod-a', preventDefault: true, run: () => { if (showSuggestions && hasStartedEditing) { @@ -882,6 +973,21 @@ } return false; } + }, + { + key: 'Escape', + run: () => { + if (showSuggestions && hasStartedEditing) { + hasStartedEditing = false; + hasSuggestionsBeenShown = true; + if (suggestionsHideTimer) { + clearTimeout(suggestionsHideTimer); + suggestionsHideTimer = null; + } + return true; + } + return false; + } } ]), keymap.of(secondaryKeymaps), @@ -890,18 +996,45 @@ customTheme, wrapCompartment.of(wrapLines ? EditorView.lineWrapping : []), EditorView.updateListener.of((update) => { + if (update.docChanged || update.transactions.some((tr) => tr.effects.length > 0)) { + const summary = getLintWarningSummary(update.state); + warningMessage = summary.message; + } if (!update.docChanged || readonly) return; - if (isNew && !isUpdatingFromEditor) { + // Check if this is manual typing (not paste, undo, or programmatic) + const isPaste = update.transactions.some((tr) => + tr.annotation(Transaction.userEvent)?.startsWith('input.paste') + ); + const isManualInput = !isPaste && !isUpdatingFromEditor; + + if (isNew && isManualInput && !hasSuggestionsBeenShown) { hasStartedEditing = true; + if (showSuggestions) { + if (suggestionsHideTimer) { + clearTimeout(suggestionsHideTimer); + } + + suggestionsHideTimer = setTimeout(() => { + hasStartedEditing = false; + hasSuggestionsBeenShown = true; + suggestionsHideTimer = null; + }, SUGGESTIONS_HIDE_DELAY); + } + const parseCache = update.state.field(json5ParseCache, false); if (!parseCache?.err && parseCache?.obj !== undefined) { hasUserContent = hasCustomContent(parseCache.obj as JsonValue); } } - // First, expand `: {}` / `: []` patterns when they appear + // First, auto-complete `key:` to `key: "",` + if (!isUpdatingFromEditor && maybeAutoCompleteKeyValue(update)) { + return; + } + + // Then, expand `: {}` / `: []` patterns when they appear if (!isUpdatingFromEditor && maybeExpandEmptyLiteral(update)) { return; } @@ -962,7 +1095,17 @@ // Set new auto-save timer autoSaveTimer = setTimeout(() => { - // handleSave(); + const parseCache = editorView?.state.field(json5ParseCache, false); + if (parseCache?.err || errorMessage) { + autoSaveTimer = null; + return; + } + if (editorView && getLintWarningSummary(editorView.state).hasWarning) { + autoSaveTimer = null; + return; + } + + handleSave(); autoSaveTimer = null; }, AUTOSAVE_DELAY); } @@ -995,6 +1138,10 @@ clearTimeout(tooltipTimer); tooltipTimer = null; } + if (suggestionsHideTimer) { + clearTimeout(suggestionsHideTimer); + suggestionsHideTimer = null; + } lastSerializedData = null; lastSerializedText = ''; editorView?.destroy(); @@ -1008,6 +1155,7 @@ generatedId = ID.unique(); hasStartedEditing = false; // Reset editing flag for new document hasUserContent = false; + hasSuggestionsBeenShown = false; // Reset suggestions shown flag for new document } wasNew = isNew; }); @@ -1023,6 +1171,7 @@ lastSerializedData = null; lastSerializedText = ''; hasUserContent = false; + hasSuggestionsBeenShown = false; // Reset suggestions shown flag when switching documents // Capture original data snapshot when switching documents originalData = $state.snapshot(data); @@ -1198,8 +1347,10 @@ {/if}
-{#if errorMessage && !loading} - +{#if !loading && (errorMessage || warningMessage)} + {/if} 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.svelte index 702028ed9f..fee93e586a 100644 --- 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.svelte @@ -3,13 +3,17 @@ import { IconExclamationCircle } from '@appwrite.io/pink-icons-svelte'; let { - error + message, + severity = 'error' }: { - error: string; + message: string; + severity?: 'error' | 'warning'; } = $props(); + + const iconColor = $derived(severity === 'warning' ? '--fgcolor-warning' : '--fgcolor-error'); -{#if error} +{#if message}
@@ -19,10 +23,10 @@ direction="row" alignItems="center" style="width: max-content;"> - + -
- {error} +
+ {message}
@@ -48,7 +52,7 @@ } } - .error-message { + .sonner-message { flex: 1; font-size: 13px; overflow: hidden; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/suggestions.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/suggestions.svelte index e5f6723c0f..db2ecadc3d 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/suggestions.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/sonners/suggestions.svelte @@ -23,7 +23,7 @@ - + for suggestions From b197ab6c5463202dfcdb8a035684f636e1b26f20 Mon Sep 17 00:00:00 2001 From: Darshan Date: Mon, 19 Jan 2026 16:45:13 +0530 Subject: [PATCH 45/60] fix: bug with overlay height computation. update: unsaved changes warning. --- src/lib/helpers/unsavedChanges.ts | 39 +++++++++ .../(entity)/views/layouts/empty.svelte | 2 + .../(components)/editor/view.svelte | 15 +++- .../spreadsheet.svelte | 81 ++++++++++++++----- 4 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 src/lib/helpers/unsavedChanges.ts diff --git a/src/lib/helpers/unsavedChanges.ts b/src/lib/helpers/unsavedChanges.ts new file mode 100644 index 0000000000..481b423a1d --- /dev/null +++ b/src/lib/helpers/unsavedChanges.ts @@ -0,0 +1,39 @@ +import { beforeNavigate } from '$app/navigation'; +import type { BeforeNavigate } from '@sveltejs/kit'; + +type UnsavedChangesGuardOptions = { + message?: string; + hasUnsavedChanges: () => boolean; + onConfirmNavigate?: () => void; + shouldBlockNavigation?: (navigation: BeforeNavigate) => boolean; +}; + +export const setupUnsavedChangesGuard = ({ + message, + hasUnsavedChanges, + onConfirmNavigate, + shouldBlockNavigation +}: UnsavedChangesGuardOptions) => { + message = message ?? 'You have unsaved changes. Are you sure you want to leave?'; + + const beforeUnload = (event: BeforeUnloadEvent) => { + if (!hasUnsavedChanges()) return; + event.preventDefault(); + event.returnValue = message; + return message; + }; + + beforeNavigate((navigation: BeforeNavigate) => { + if (!hasUnsavedChanges()) return; + if (shouldBlockNavigation && !shouldBlockNavigation(navigation)) return; + + if (!confirm(message)) { + navigation.cancel(); + return; + } + + onConfirmNavigate?.(); + }); + + return { beforeUnload }; +}; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte index 5952613ee7..b00e5696d6 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/views/layouts/empty.svelte @@ -94,6 +94,8 @@ onMount(async () => { if (spreadsheetContainer) { + requestAnimationFrame(updateOverlayHeight); + resizeObserver = new ResizeObserver(debouncedUpdateOverlayHeight); resizeObserver.observe(spreadsheetContainer); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte index cd78a6a826..528ad03111 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/view.svelte @@ -398,11 +398,22 @@ return afterCursor.includes('"') || afterCursor.includes(','); } + function isPasteTransaction(tr: Transaction): boolean { + return tr.annotation(Transaction.userEvent)?.startsWith('input.paste') ?? false; + } + + function isPasteUpdate(update: ViewUpdate): boolean { + return update.transactions.some(isPasteTransaction); + } + // Auto-complete key with empty string: `key:` -> `key: "",` with cursor between quotes function maybeAutoCompleteKeyValue(update: ViewUpdate): boolean { const doc = update.state.doc; let did = false; + const isPaste = isPasteUpdate(update); + if (isPaste) return false; + update.changes.iterChanges( (fromA: number, toA: number, fromB: number, toB: number, inserted: Text) => { if (did) return; @@ -1003,9 +1014,7 @@ if (!update.docChanged || readonly) return; // Check if this is manual typing (not paste, undo, or programmatic) - const isPaste = update.transactions.some((tr) => - tr.annotation(Transaction.userEvent)?.startsWith('input.paste') - ); + const isPaste = isPasteUpdate(update); const isManualInput = !isPaste && !isUpdatingFromEditor; if (isNew && isManualInput && !hasSuggestionsBeenShown) { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index f56ea88aa5..3f701c0abb 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -38,6 +38,7 @@ import { chunks } from '$lib/helpers/array'; import { mapToQueryParams } from '$lib/components/filters/store'; import { expandTabs, buildWildcardEntitiesQuery } from '$database/store'; + import { setupUnsavedChangesGuard } from '$lib/helpers/unsavedChanges'; import { collectionColumns, documentActivitySheet, @@ -529,8 +530,26 @@ : []; $: showSuggestions = $noSqlDocument.isNew && suggestedAttributes.length > 0; + + const hasUnsavedChanges = () => + Boolean( + $noSqlDocument?.show && + ($noSqlDocument?.hasDataChanged || + ($noSqlDocument?.isNew && $noSqlDocument?.isDirty)) + ); + + const { beforeUnload } = setupUnsavedChangesGuard({ + hasUnsavedChanges, + onConfirmNavigate: () => noSqlDocument.reset({ show: false }), + shouldBlockNavigation: (navigation) => { + const nextPath = navigation.to?.url?.pathname; + return Boolean(nextPath && nextPath !== page.url.pathname); + } + }); + + { + if (isUnsavedRow) return; noSqlDocument.edit(document); }} - style:cursor="pointer"> + style:cursor={isUnsavedRow ? 'default' : 'pointer'}> {:else if columnId === 'actions'} - { - onSelectSheetOption(option, document); - }} - onVisibilityChanged={(visible) => { - canShowDatetimePopover = !visible; - }}> - {#snippet children(toggle)} - - - - {/snippet} - + {#if isUnsavedRow} + + + + {:else} + { + onSelectSheetOption(option, document); + }} + onVisibilityChanged={(visible) => { + canShowDatetimePopover = !visible; + }}> + {#snippet children(toggle)} + + + + {/snippet} + + {/if} {:else} {@const value = document[columnId]} {#if value} @@ -753,7 +785,14 @@ showHeaderActions={!$isSmallViewport} {showSuggestions} {suggestedAttributes} - onCancel={() => noSqlDocument.reset()} + onCancel={() => { + const firstDocument = $documents?.documents?.[0]; + if (firstDocument) { + noSqlDocument.edit(firstDocument); + } else { + noSqlDocument.reset({ show: false }); + } + }} onSave={async (document) => await createOrUpdateDocument(document)} onChange={(_, hasDataChanged) => noSqlDocument.update({ hasDataChanged })} /> {/snippet} From 10c2d864cdd5722e526f871f78e9d8f457515447 Mon Sep 17 00:00:00 2001 From: Darshan Date: Mon, 19 Jan 2026 16:49:30 +0530 Subject: [PATCH 46/60] update: don't use incorrect flag based gating. `show` is only for mobile. --- .../collection-[collection]/spreadsheet.svelte | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte index 3f701c0abb..6dbad78835 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/spreadsheet.svelte @@ -533,9 +533,7 @@ const hasUnsavedChanges = () => Boolean( - $noSqlDocument?.show && - ($noSqlDocument?.hasDataChanged || - ($noSqlDocument?.isNew && $noSqlDocument?.isDirty)) + $noSqlDocument?.hasDataChanged || ($noSqlDocument?.isNew && $noSqlDocument?.isDirty) ); const { beforeUnload } = setupUnsavedChangesGuard({ From 42a4353472df816bd3d0b185948de0a5a7787d16 Mon Sep 17 00:00:00 2001 From: Darshan Date: Mon, 19 Jan 2026 17:33:43 +0530 Subject: [PATCH 47/60] update: csv to json import. fix: responsive side spacing for header. --- src/lib/actions/analytics.ts | 2 + .../collection-[collection]/+page.svelte | 44 ++++++++++--------- .../collection-[collection]/store.ts | 2 +- .../database-[database]/header.svelte | 5 ++- .../table-[table]/+page.svelte | 2 +- 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 32613790d1..5cdd782621 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -155,6 +155,7 @@ export enum Click { DatabaseRowDelete = 'click_row_delete', DatabaseDatabaseDelete = 'click_database_delete', DatabaseImportCsv = 'click_database_import_csv', + DatabaseImportJson = 'click_database_import_json', DomainCreateClick = 'click_domain_create', DomainDeleteClick = 'click_domain_delete', @@ -281,6 +282,7 @@ export enum Submit { DatabaseDelete = 'submit_database_delete', DatabaseUpdateName = 'submit_database_update_name', DatabaseImportCsv = 'submit_database_import_csv', + DatabaseImportJSON = 'submit_database_import_json', DatabaseBackupDelete = 'submit_database_backup_delete', DatabaseBackupPolicyCreate = 'submit_database_backup_policy_create', diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte index 27df7291a4..6dd4385990 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/+page.svelte @@ -8,7 +8,6 @@ import type { PageProps } from './$types'; import FilePicker from '$lib/components/filePicker.svelte'; import { page } from '$app/state'; - import { sdk } from '$lib/stores/sdk'; import { addNotification } from '$lib/stores/notifications'; import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { isSmallViewport } from '$lib/stores/viewport'; @@ -22,7 +21,7 @@ import { expandTabs, randomDataModalState } from '$database/store'; import { EmptySheet, EmptySheetCards } from '$database/(entity)'; import { - isCollectionsCsvImportInProgress, + isCollectionsJsonImportInProgress, noSqlDocument, collectionColumns } from '$database/collection-[collection]/store'; @@ -34,7 +33,7 @@ const { data }: PageProps = $props(); - let showImportCSV = $state(false); + let showImportJson = $state(false); let showCustomColumnsModal = $state(false); let columnsError: string = $state(null); @@ -50,32 +49,34 @@ } async function onSelect(file: Models.File, localFile = false) { - $isCollectionsCsvImportInProgress = true; + $isCollectionsJsonImportInProgress = true; + + console.log(file, localFile); try { - await sdk + /*await sdk .forProject(page.params.region, page.params.project) - .migrations.createCSVImport({ + .migrations.createJSONImport({ bucketId: file.bucketId, fileId: file.$id, resourceId: `${page.params.database}:${page.params.collection}`, internalFile: localFile - }); + });*/ addNotification({ type: 'success', - message: 'Documents import from csv has started' + message: 'Documents import from JSON has started' }); - trackEvent(Submit.DatabaseImportCsv); + trackEvent(Submit.DatabaseImportJSON); } catch (e) { - trackError(e, Submit.DatabaseImportCsv); + trackError(e, Submit.DatabaseImportJSON); addNotification({ type: 'error', message: e.message }); } finally { - $isCollectionsCsvImportInProgress = false; + $isCollectionsJsonImportInProgress = false; } } @@ -95,6 +96,7 @@ isCustomTable view={data.view} columns={collectionColumns} + disableButton={data.documents.total === 0} onCustomOptionClick={() => (showCustomColumnsModal = true)} />
@@ -105,7 +107,7 @@ direction="row" alignItems="center" justifyContent="flex-end" - style="padding-right: 40px;"> + style="padding-right: {$isSmallViewport ? '0' : '40px'};"> {#if !$isSmallViewport}
{/snippet} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 52/60] 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 53/60] 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 54/60] 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 55/60] (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 56/60] (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 57/60] (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 58/60] (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 59/60] (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}