From 744d6eafb5389f80a7dda11ca7317a866fccb34c Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Mon, 17 Aug 2026 15:35:59 +0530 Subject: [PATCH 1/4] chore(deps): bump @appwrite.io/console to 16.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 1.9.6 console spec adds the mfa-factors project policy, which the pinned 15.3.0 build predates. The major bump also removes the Health service (registered in the SDK store but never called), renames Models.CloudLocale to Models.Locale, moves createTextEmbeddings from VectorsDB to a new Embeddings service, drops UsageProject.executionsBreakdown in favour of the dimensional usage API, and splits the composite `databaseId:collectionId` migration resourceId into resourceId plus parentResourceId. Models.Database.type is now the SDK's DatabaseType enum, which also covers the dedicated backings (mysql, postgresql, mongodb) this console has no UI for. The local union is extracted from the SDK enum so a renamed or dropped value breaks the switches instead of drifting silently, and toDatabaseType() narrows at the boundary — matching the entity dispatchers, which already throw on an unknown type. --- bun.lock | 8 +-- package.json | 2 +- src/lib/components/csvImportBox.svelte | 4 +- src/lib/sdk/usage.ts | 63 ++++++++++++++++++- src/lib/stores/sdk.ts | 7 ++- .../account/payments/addressModal.svelte | 2 +- .../account/payments/billingAddress.svelte | 2 +- .../account/payments/editAddressModal.svelte | 2 +- .../billing/billingAddress.svelte | 2 +- .../billing/replaceAddress.svelte | 2 +- .../settings/Soc2.svelte | 2 +- .../settings/Soc2Modal.svelte | 2 +- .../databases/+page.ts | 4 +- .../(entity)/helpers/terminology.ts | 42 ++++++++++--- .../databases/database-[database]/+page.ts | 4 +- .../(components)/editor/embeddingModal.svelte | 2 +- .../collection-[collection]/+layout.svelte | 10 ++- .../collection-[collection]/+layout.ts | 8 ++- .../collection-[collection]/+page.svelte | 9 ++- .../collection-[collection]/+page.ts | 8 ++- .../export/+page.svelte | 3 +- .../indexes/+page.svelte | 8 ++- .../settings/+page.svelte | 7 ++- .../spreadsheet.svelte | 7 ++- .../table-[table]/+layout.ts | 8 ++- .../table-[table]/+page.svelte | 3 +- .../table-[table]/export/+page.svelte | 3 +- .../databases/store.ts | 3 +- .../databases/table.svelte | 4 +- .../settings/usage/[[invoice]]/+page.svelte | 4 +- .../settings/usage/[[invoice]]/+page.ts | 12 +++- 31 files changed, 183 insertions(+), 64 deletions(-) diff --git a/bun.lock b/bun.lock index 4f7dcfe958..f028a4c204 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "@appwrite/console", "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@6be9e62", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@ed09983", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", @@ -130,15 +130,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@6be9e62", { "dependencies": { "json-bigint": "1.0.0" } }, "sha512-XvEyYAb9HtcySeLgn5ZxVCEBhzxSCXfQurcSVp6Wnj1amBFx5CWsKhfWHelG9DxFG9SkeHmY+bTcZneOAhgBkw=="], + "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@ed09983", { "dependencies": { "json-bigint": "1.0.0" } }], "@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@bfe7ce3", { "peerDependencies": { "svelte": "^4.0.0" } }, "sha512-2HYl/CC2OlfZIR7LzbLXuSPBn0iNkjbnxpaeGCkZ7UNZ/hFeSeeWjDJqTBMdZ8+X95uuZqHx62XPTiE/svuSXQ=="], + "@appwrite.io/pink-icons-svelte": ["@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", { "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@8dcaa17", { "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" } }, "sha512-rw3zXN7/cUciCnhj0FR8M0H5Db+LYYMaKtPxvOAIMxNTBmStzU8kTw6grqIvdtFu9vybIsjKtIwm9QLHpNDBjA=="], + "@appwrite.io/pink-svelte": ["@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@8dcaa17", { "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=="], diff --git a/package.json b/package.json index c4958f63e7..f7ffc23529 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@6be9e62", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@ed09983", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", diff --git a/src/lib/components/csvImportBox.svelte b/src/lib/components/csvImportBox.svelte index 6069ac4c41..35505191b3 100644 --- a/src/lib/components/csvImportBox.svelte +++ b/src/lib/components/csvImportBox.svelte @@ -73,8 +73,8 @@ if (importData.source.toLowerCase() !== 'csv') return; const status = importData.status; - const resourceId = importData.resourceId ?? ''; - const [databaseId, tableId] = resourceId.split(':') ?? []; + const databaseId = importData.parentResourceId ?? ''; + const tableId = importData.resourceId ?? ''; const current = importItems.get(importData.$id); let tableName = current?.table ?? null; diff --git a/src/lib/sdk/usage.ts b/src/lib/sdk/usage.ts index b446bfa50f..d5bd0b05e6 100644 --- a/src/lib/sdk/usage.ts +++ b/src/lib/sdk/usage.ts @@ -1,4 +1,12 @@ -import type { Models } from '@appwrite.io/console'; +import { + Query, + UsageEventDimension, + UsageEventMetric, + UsageOrderBy, + UsageOrderDirection, + type Models +} from '@appwrite.io/console'; +import { sdk } from '$lib/stores/sdk'; export function accumulateUsage(usage: Models.Metric[], base: number): Models.Metric[] { const accumulation = usage.reduce( @@ -18,6 +26,59 @@ export function accumulateUsage(usage: Models.Metric[], base: number): Models.Me return accumulation.metrics; } +export type ExecutionsBreakdown = { + resourceId: string; + name?: string; + value: number; +}; + +const executionsBreakdownLimit = 25; + +/** + * `UsageProject.executionsBreakdown` was dropped in SDK 16; the per-resource split now comes from + * the dimensional usage API, which returns bare resource IDs, so names are resolved separately. + */ +export async function listExecutionsBreakdown( + region: string, + projectId: string, + startAt: string, + endAt: string +): Promise { + const project = sdk.forProject(region, projectId); + + const events = await project.usage.listEvents({ + metrics: [UsageEventMetric.Executions], + dimensions: [UsageEventDimension.ResourceId], + startAt, + endAt, + orderBy: UsageOrderBy.Value, + orderDir: UsageOrderDirection.Desc, + limit: executionsBreakdownLimit + }); + + const totals = new Map(); + for (const metric of events.metrics) { + for (const point of metric.points) { + if (!point.resourceId) continue; + totals.set(point.resourceId, (totals.get(point.resourceId) ?? 0) + point.value); + } + } + + if (totals.size === 0) return []; + + const resourceIds = [...totals.keys()]; + const functions = await project.functions.list({ + queries: [Query.equal('$id', resourceIds), Query.limit(resourceIds.length)] + }); + const names = new Map(functions.functions.map((func) => [func.$id, func.name])); + + return resourceIds.map((resourceId) => ({ + resourceId, + name: names.get(resourceId), + value: totals.get(resourceId) + })); +} + export type Metric = { /** * The value of this metric at the timestamp. diff --git a/src/lib/stores/sdk.ts b/src/lib/stores/sdk.ts index cdea8ebd19..47115c8174 100644 --- a/src/lib/stores/sdk.ts +++ b/src/lib/stores/sdk.ts @@ -8,8 +8,8 @@ import { Backups, Client, Console, + Embeddings, Functions, - Health, Locale, Messaging, Migrations, @@ -31,6 +31,7 @@ import { Webhooks, Realtime, Organizations, + Usage, VectorsDB } from '@appwrite.io/console'; import { buildRegionalV1Endpoint } from '$lib/helpers/apiEndpoint'; @@ -54,7 +55,6 @@ function createConsoleSdk(client: Client) { oauth2: new Oauth2(client), avatars: new Avatars(client), functions: new Functions(client), - health: new Health(client), locale: new Locale(client), projects: new Projects(client), teams: new Teams(client), @@ -115,7 +115,6 @@ const sdkForProject = { avatars: new Avatars(clientProject), backups: new Backups(clientProject), functions: new Functions(clientProject), - health: new Health(clientProject), locale: new Locale(clientProject), messaging: new Messaging(clientProject), project: new Project(clientProject), @@ -131,6 +130,8 @@ const sdkForProject = { tablesDB: new TablesDB(clientProject), documentsDB: new DocumentsDB(clientProject), vectorsDB: new VectorsDB(clientProject), + embeddings: new Embeddings(clientProject), + usage: new Usage(clientProject), webhooks: new Webhooks(clientProject), console: new Console(clientProject) // for suggestions API }; diff --git a/src/routes/(console)/account/payments/addressModal.svelte b/src/routes/(console)/account/payments/addressModal.svelte index af186cb103..57ea22c101 100644 --- a/src/routes/(console)/account/payments/addressModal.svelte +++ b/src/routes/(console)/account/payments/addressModal.svelte @@ -10,7 +10,7 @@ import type { Models } from '@appwrite.io/console'; export let show = false; - export let locale: Models.CloudLocale; + export let locale: Models.Locale; export let organization: string = null; export let countryList: Models.CountryList; diff --git a/src/routes/(console)/account/payments/billingAddress.svelte b/src/routes/(console)/account/payments/billingAddress.svelte index 703ad7e902..5e9b888376 100644 --- a/src/routes/(console)/account/payments/billingAddress.svelte +++ b/src/routes/(console)/account/payments/billingAddress.svelte @@ -29,7 +29,7 @@ export let data: PageData; - const locale: Models.CloudLocale = data.locale; + const locale: Models.Locale = data.locale; const countryList: Models.CountryList = data.countryList; let show = false; diff --git a/src/routes/(console)/account/payments/editAddressModal.svelte b/src/routes/(console)/account/payments/editAddressModal.svelte index 0fc1fdad9a..891b65103f 100644 --- a/src/routes/(console)/account/payments/editAddressModal.svelte +++ b/src/routes/(console)/account/payments/editAddressModal.svelte @@ -10,7 +10,7 @@ import type { Models } from '@appwrite.io/console'; export let show = false; - export let locale: Models.CloudLocale; + export let locale: Models.Locale; export let countryList: Models.CountryList; export let selectedAddress: Models.BillingAddress; diff --git a/src/routes/(console)/organization-[organization]/billing/billingAddress.svelte b/src/routes/(console)/organization-[organization]/billing/billingAddress.svelte index 6dbc5bfabb..ebecd52319 100644 --- a/src/routes/(console)/organization-[organization]/billing/billingAddress.svelte +++ b/src/routes/(console)/organization-[organization]/billing/billingAddress.svelte @@ -22,7 +22,7 @@ } from '@appwrite.io/pink-icons-svelte'; import type { Models } from '@appwrite.io/console'; - export let locale: Models.CloudLocale; + export let locale: Models.Locale; export let countryList: Models.CountryList; export let organization: Models.Organization; export let billingAddress: Models.BillingAddress; diff --git a/src/routes/(console)/organization-[organization]/billing/replaceAddress.svelte b/src/routes/(console)/organization-[organization]/billing/replaceAddress.svelte index 926f21319e..56798f55b3 100644 --- a/src/routes/(console)/organization-[organization]/billing/replaceAddress.svelte +++ b/src/routes/(console)/organization-[organization]/billing/replaceAddress.svelte @@ -13,7 +13,7 @@ import type { Models } from '@appwrite.io/console'; export let show = false; - export let locale: Models.CloudLocale; + export let locale: Models.Locale; export let countryList: Models.CountryList; let loading = true; diff --git a/src/routes/(console)/organization-[organization]/settings/Soc2.svelte b/src/routes/(console)/organization-[organization]/settings/Soc2.svelte index 7d71dbd609..167a802613 100644 --- a/src/routes/(console)/organization-[organization]/settings/Soc2.svelte +++ b/src/routes/(console)/organization-[organization]/settings/Soc2.svelte @@ -5,7 +5,7 @@ import type { Models } from '@appwrite.io/console'; let show = false; - export let locale: Models.CloudLocale; + export let locale: Models.Locale; export let countryList: Models.CountryList; diff --git a/src/routes/(console)/organization-[organization]/settings/Soc2Modal.svelte b/src/routes/(console)/organization-[organization]/settings/Soc2Modal.svelte index 34331de126..c4b3b645d1 100644 --- a/src/routes/(console)/organization-[organization]/settings/Soc2Modal.svelte +++ b/src/routes/(console)/organization-[organization]/settings/Soc2Modal.svelte @@ -11,7 +11,7 @@ import type { Models } from '@appwrite.io/console'; export let show = false; - export let locale: Models.CloudLocale; + export let locale: Models.Locale; export let countryList: Models.CountryList; let email = ''; diff --git a/src/routes/(console)/project-[region]-[project]/databases/+page.ts b/src/routes/(console)/project-[region]-[project]/databases/+page.ts index 642f277e3d..e219c48f93 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/+page.ts @@ -6,7 +6,7 @@ import { timeFromNow } from '$lib/helpers/date'; import type { PageLoad, RouteParams } from './$types'; import { isSelfHosted } from '$lib/system'; import { isCloud } from '$lib/system'; -import { useDatabaseSdk } from '$database/(entity)'; +import { toDatabaseType, useDatabaseSdk } from '$database/(entity)'; export const load: PageLoad = async ({ url, route, depends, params, parent }) => { depends(Dependencies.DATABASES); @@ -62,7 +62,7 @@ async function fetchDatabasesAndBackups( databases.databases.map(async ({ $id, type }) => { const res = await databaseSdk.listEntities({ databaseId: $id, - databaseType: type, + databaseType: toDatabaseType(type), queries: [Query.limit(1), Query.orderDesc('')] }); 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 02fb339a2a..d803b9e0b3 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,15 +1,27 @@ import type { Page } from '@sveltejs/kit'; import { capitalize, plural } from '$lib/helpers/string'; -import { AppwriteException, type TablesDBIndexType, type Models } from '@appwrite.io/console'; +import { + AppwriteException, + type DatabaseType as SdkDatabaseType, + type TablesDBIndexType, + 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; -/* manual type for the time being because vectorsdb is pending */ -export type DatabaseType = 'legacy' | 'tablesdb' | 'documentsdb' | 'vectorsdb'; +/** + * The SDK also reports the raw dedicated backings (`mysql`, `postgresql`, `mongodb`), which the + * console has no UI for. Extracting from the SDK keeps the supported set from drifting silently: + * a renamed or dropped SDK value removes the member here and breaks every `case` that uses it. + */ +export type DatabaseType = Extract< + `${SdkDatabaseType}`, + 'legacy' | 'tablesdb' | 'documentsdb' | 'vectorsdb' +>; export type CollectionDatabaseType = Extract; export const DEFAULT_VECTOR_DIMENSION = 768; @@ -73,6 +85,22 @@ export const baseTerminology = { } } as const; +function isSupportedDatabaseType(type: string): type is DatabaseType { + return Object.hasOwn(baseTerminology, type); +} + +/** + * Narrows an API reported database type down to the ones this console implements. + * Throws on the rest, matching how every dispatcher in `sdk.ts` rejects an unknown type. + */ +export function toDatabaseType(type: Models.Database['type'] | DatabaseType): DatabaseType { + if (!isSupportedDatabaseType(type)) { + throw new AppwriteException(`Unsupported database type: ${type}`, 500); + } + + return type; +} + const createTerm = (singular: string, pluralForm: string): Term => { return { singular, plural: pluralForm }; }; @@ -152,15 +180,13 @@ export function toSupportiveRecord(raw: Record | Models.Document | Models.Row): * Use `getTerminologies()` instead when in `database-[database]` routes where context is available. */ export function useTerminology(pageOrType: Page | DatabaseType): TerminologyResult { - const type = - typeof pageOrType === 'object' - ? (pageOrType.data?.database?.type as DatabaseType) - : pageOrType; - if (!type) { + const rawType = typeof pageOrType === 'object' ? pageOrType.data?.database?.type : pageOrType; + if (!rawType) { // strict check because this should always be available! throw new AppwriteException('Database type is required', 500); } + const type = toDatabaseType(rawType); const dbTerminologies = terminologyData[type] || {}; const strictSchema = type === 'legacy' || type === 'tablesdb'; 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 358f8d97ee..130ba58c64 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 @@ -2,7 +2,7 @@ import { Query } from '@appwrite.io/console'; import { getLimit, getPage, getSearch, getView, pageToOffset, View } from '$lib/helpers/load'; import type { PageLoad } from './$types'; import { CARD_LIMIT, Dependencies } from '$lib/constants'; -import { type DatabaseType, useDatabaseSdk } from '$database/(entity)'; +import { toDatabaseType, useDatabaseSdk } from '$database/(entity)'; export const load: PageLoad = async ({ params, url, route, depends, parent }) => { const { database } = await parent(); @@ -14,7 +14,7 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) => const view = getView(url, route, View.Grid); const offset = pageToOffset(page, limit); - const databaseType = database.type as DatabaseType; + const databaseType = toDatabaseType(database.type); const databaseSdk = useDatabaseSdk(params.region, params.project, databaseType); const entities = await databaseSdk.listEntities({ diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/embeddingModal.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/embeddingModal.svelte index 51d427291b..5269113176 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/embeddingModal.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/(components)/editor/embeddingModal.svelte @@ -27,7 +27,7 @@ try { const response = await sdk .forProject(page.params.region, page.params.project) - .vectorsDB.createTextEmbeddings({ texts: [content.trim()] }); + .embeddings.createTextEmbeddings({ texts: [content.trim()] }); const embedding = response?.embeddings?.[0]?.embedding; if (embedding?.length) { 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 12ecab9eaf..d9ecf98ed3 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 @@ -49,6 +49,7 @@ import { SideSheet, EditRecordPermissions, + toDatabaseType, useDatabaseSdk, DEFAULT_VECTOR_DIMENSION, type Field, @@ -77,6 +78,7 @@ let editRecordPermissions: EditRecordPermissions; $: collection = data.collection; + $: databaseType = toDatabaseType(data.database.type); $: basePath = resolveRoute( '/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]', page.params @@ -189,11 +191,7 @@ }); async function handleCreateIndex(index: Index) { - const databaseSdk = useDatabaseSdk( - page.params.region, - page.params.project, - data.database.type - ); + const databaseSdk = useDatabaseSdk(page.params.region, page.params.project, databaseType); await databaseSdk.createIndex({ databaseId: page.params.database, @@ -344,7 +342,7 @@ }}> { const { database } = await parent(); depends(Dependencies.COLLECTION); - const databaseSdk = useDatabaseSdk(params.region, params.project, database.type); + const databaseSdk = useDatabaseSdk( + params.region, + params.project, + toDatabaseType(database.type) + ); const collection = await databaseSdk.getEntity({ databaseId: params.database, 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 017e1a03a0..ec2e127667 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 @@ -38,7 +38,7 @@ import { invalidate } from '$app/navigation'; import { hash } from '$lib/helpers/string'; import { Dependencies } from '$lib/constants'; - import { EmptySheet, EmptySheetCards } from '$database/(entity)'; + import { EmptySheet, EmptySheetCards, toDatabaseType } from '$database/(entity)'; import { isCollectionsJsonImportInProgress, noSqlDocument, @@ -55,6 +55,8 @@ const filterColumns = writable([]); + const databaseType = $derived(toDatabaseType(data.database.type)); + let isRefreshing = $state(false); let showImportJson = $state(false); let showImportOptions = $state(false); @@ -105,7 +107,8 @@ .migrations.createJSONImport({ bucketId: pendingFile.bucketId, fileId: pendingFile.$id, - resourceId: `${page.params.database}:${page.params.collection}`, + databaseId: page.params.database, + collectionId: page.params.collection, internalFile: pendingLocalFile, onDuplicate: importOnDuplicate }); @@ -328,7 +331,7 @@ {/snippet} {:else} - + {#snippet actions()} { const { collection, database } = await parent(); @@ -20,7 +20,11 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) => queries.set(parsedQueries); const currentSort = extractSortFromQueries(parsedQueries); - const collectionSdk = getCollectionService(params.region, params.project, database.type); + const collectionSdk = getCollectionService( + params.region, + params.project, + toDatabaseType(database.type) + ); const documentsPage = await loadGridRows( collection, diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/export/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/export/+page.svelte index a4a2b7e9aa..9d19f13b9c 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/export/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/export/+page.svelte @@ -56,7 +56,8 @@ await sdk .forProject(page.params.region, page.params.project) .migrations.createJSONExport({ - resourceId: `${page.params.database}:${page.params.collection}`, + databaseId: page.params.database, + collectionId: page.params.collection, filename: filename, columns: [], queries: exportWithFilters ? Array.from(localQueries.values()) : [], 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 ce5f1aaad6..e8f79981f0 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 @@ -6,6 +6,7 @@ Indexes, EmptySheet, EmptySheetCards, + toDatabaseType, useDatabaseSdk } from '$database/(entity)'; import { IconPlus } from '@appwrite.io/pink-icons-svelte'; @@ -15,7 +16,8 @@ let createIndexRef: CreateIndexForm; - const databaseSdk = useDatabaseSdk(page.params.region, page.params.project, data.database.type); + const databaseType = $derived(toDatabaseType(data.database.type)); + const databaseSdk = useDatabaseSdk(page.params.region, page.params.project, databaseType); async function onCreateIndex(index: CreateIndexesCallbackType) { await databaseSdk.createIndex({ @@ -46,14 +48,14 @@ {#snippet createIndexForm()} {/snippet} {#snippet emptyIndexesSheetView(toggle)} - + {#snippet actions()} void } | undefined; diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.ts index cd5d75e6db..021c3c00a5 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.ts @@ -1,7 +1,7 @@ import Header from './header.svelte'; import type { LayoutLoad } from './$types'; import { Dependencies } from '$lib/constants'; -import { Breadcrumbs, useDatabaseSdk } from '$database/(entity)'; +import { Breadcrumbs, toDatabaseType, useDatabaseSdk } from '$database/(entity)'; import { guardResourceBlock } from '$lib/helpers/project'; export const load: LayoutLoad = async ({ params, depends, parent }) => { @@ -9,7 +9,11 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => { depends(Dependencies.TABLE); guardResourceBlock(project, ['tables', 'collections'], params.table); - const databaseSdk = useDatabaseSdk(params.region, params.project, database.type); + const databaseSdk = useDatabaseSdk( + params.region, + params.project, + toDatabaseType(database.type) + ); const table = await databaseSdk.getEntity({ databaseId: params.database, 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 f1fe008679..89444b447a 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 @@ -139,7 +139,8 @@ .migrations.createCSVImport({ bucketId: pendingFile.bucketId, fileId: pendingFile.$id, - resourceId: `${page.params.database}:${page.params.table}`, + databaseId: page.params.database, + collectionId: page.params.table, internalFile: pendingLocalFile, onDuplicate: importOnDuplicate }); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/export/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/export/+page.svelte index 9ed600090f..8291162dbb 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/export/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/export/+page.svelte @@ -101,7 +101,8 @@ await sdk .forProject(page.params.region, page.params.project) .migrations.createCSVExport({ - resourceId: `${page.params.database}:${page.params.table}`, + databaseId: page.params.database, + collectionId: page.params.table, filename: filename, columns: selectedCols, queries: exportWithFilters ? Array.from(localQueries.values()) : [], diff --git a/src/routes/(console)/project-[region]-[project]/databases/store.ts b/src/routes/(console)/project-[region]-[project]/databases/store.ts index 79d0aa31c2..05d7e32127 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/store.ts @@ -2,7 +2,6 @@ import { isCloud } from '$lib/system'; import { writable } from 'svelte/store'; import type { Column } from '$lib/helpers/types'; import type { Models } from '@appwrite.io/console'; -import type { DatabaseType } from '$database/(entity)'; export const columns = writable( isCloud @@ -24,7 +23,7 @@ export const columns = writable( ); export function getDatabaseTypeTitle(database: Models.Database) { - switch (database.type as DatabaseType) { + switch (database.type) { default: case 'legacy': case 'tablesdb': diff --git a/src/routes/(console)/project-[region]-[project]/databases/table.svelte b/src/routes/(console)/project-[region]-[project]/databases/table.svelte index 9d408cba2f..445641a11b 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/table.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/table.svelte @@ -3,7 +3,7 @@ import { columns, getDatabaseTypeTitle } from './store'; import { Id } from '$lib/components'; import type { Models } from '@appwrite.io/console'; - import { useTerminology } from '$database/(entity)'; + import { toDatabaseType, useTerminology } from '$database/(entity)'; import DualTimeView from '$lib/components/dualTimeView.svelte'; import { resolveRoute, withPath } from '$lib/stores/navigation'; import { IconExclamation } from '@appwrite.io/pink-icons-svelte'; @@ -35,7 +35,7 @@ } function getEntityUrl(database: Models.Database, entityId: string) { - const terminology = useTerminology(database.type); + const terminology = useTerminology(toDatabaseType(database.type)); const entityType = terminology.entity.lower.singular; return withPath( diff --git a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte index 50e94d3ff0..86f229c05b 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte @@ -321,13 +321,13 @@ data: [...executions.map((e) => [e.date, e.value])] } ]} /> - {#if data.usage.executionsBreakdown?.length > 0} + {#if data.executionsBreakdown?.length > 0} Function Usage - {#each data.usage.executionsBreakdown as func} + {#each data.executionsBreakdown as func} diff --git a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts index 0efb7c6020..3fb49bd26b 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts @@ -1,6 +1,6 @@ import { sdk } from '$lib/stores/sdk'; import type { PageLoad } from './$types'; -import { accumulateUsage } from '$lib/sdk/usage'; +import { accumulateUsage, listExecutionsBreakdown, type ExecutionsBreakdown } from '$lib/sdk/usage'; import { type Models, Query } from '@appwrite.io/console'; export const load: PageLoad = async ({ params, parent }) => { @@ -66,10 +66,18 @@ export const load: PageLoad = async ({ params, parent }) => { usage.users = accumulateUsage(usage.users, usage.usersTotal); + let executionsBreakdown: ExecutionsBreakdown[] = []; + try { + executionsBreakdown = await listExecutionsBreakdown(region, project, startDate, endDate); + } catch { + // supplementary — a server without the usage events API still renders the rest of the page + } + return { usage, invoices, currentInvoice, - currentAggregation + currentAggregation, + executionsBreakdown }; }; From 17f0fa5124bcfe7bac7ccd82c99ee303ceaea8bb Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Mon, 17 Aug 2026 15:36:15 +0530 Subject: [PATCH 2/4] feat(auth): add MFA factors policy card Lets a project choose which factors can complete an MFA challenge, including the new custom factor, where Appwrite generates and verifies the code and the app delivers it over a channel of its own. The card stays hidden when listPolicies() omits mfa-factors, so a self-hosted server older than 1.9.6 does not render a control whose PATCH would 404. --- src/lib/actions/analytics.ts | 1 + .../auth/security/+page.svelte | 4 + .../auth/security/+page.ts | 3 + .../auth/security/updateMfaFactors.svelte | 87 +++++++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 src/routes/(console)/project-[region]-[project]/auth/security/updateMfaFactors.svelte diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index e7e014de9e..80ef51848b 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -283,6 +283,7 @@ export enum Submit { AuthCorporateEmailsUpdate = 'submit_auth_corporate_emails_update', AuthSessionAlertsUpdate = 'submit_auth_session_alerts_update', AuthMembershipPrivacyUpdate = 'submit_auth_membership_privacy_update', + AuthMfaFactorsUpdate = 'submit_auth_mfa_factors_update', AuthMockNumbersUpdate = 'submit_auth_mock_numbers_update', AuthInvalidateSession = 'submit_auth_invalidate_session', SessionsLengthUpdate = 'submit_sessions_length_update', diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/+page.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/+page.svelte index 64737bb7f8..47ea7b2505 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/+page.svelte @@ -8,6 +8,7 @@ import UpdateSessionsLimit from './updateSessionsLimit.svelte'; import PasswordPolicies from './passwordPolicies.svelte'; import PasswordStrengthPolicy from './passwordStrengthPolicy.svelte'; + import UpdateMfaFactors from './updateMfaFactors.svelte'; import SessionSecurity from './sessionSecurity.svelte'; import UpdateSignupEmailSecurity from './updateSignupEmailSecurity.svelte'; import { isCloud } from '$lib/system'; @@ -25,6 +26,9 @@ dictionaryPolicy={data.passwordDictionaryPolicy} historyPolicy={data.passwordHistoryPolicy} personalDataPolicy={data.passwordPersonalDataPolicy} /> + {#if data.mfaFactorsPolicy} + + {/if} {#if isCloud} { ) as Partial>; return { + mfaFactorsPolicy: policiesById[ProjectPolicyId.Mfafactors] as + | Models.PolicyMfaFactors + | undefined, membershipPrivacyPolicy: policiesById[ ProjectPolicyId.Membershipprivacy ] as Models.PolicyMembershipPrivacy, diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateMfaFactors.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateMfaFactors.svelte new file mode 100644 index 0000000000..ff4c37bb51 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/auth/security/updateMfaFactors.svelte @@ -0,0 +1,87 @@ + + +
+ + MFA factors + Choose which factors your users can use to complete a multi-factor authentication challenge. Recovery + codes always remain available as a fallback. + + + + + + + + + + +
From 1cc7e263bd33940585b8e6bfb7a2eb513f564037 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Mon, 17 Aug 2026 15:56:53 +0530 Subject: [PATCH 3/4] fix: gate MFA factors policy behind the project write scope Matches the project settings cards, which disable their controls on !$canWriteProjects rather than letting a read-only member submit a request the API rejects. Also fills in labels, installationScopes, and installationRedirectUrl on the App literal built from a CIMD document. Those three became required on Models.App in SDK 16 and a CIMD document carries none of them, so they take the same empty defaults as the other fields the document does not provide. --- src/lib/helpers/oauth2-cimd.ts | 3 +++ .../auth/security/updateMfaFactors.svelte | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/lib/helpers/oauth2-cimd.ts b/src/lib/helpers/oauth2-cimd.ts index 7d631f678e..e9ec0c288a 100644 --- a/src/lib/helpers/oauth2-cimd.ts +++ b/src/lib/helpers/oauth2-cimd.ts @@ -69,6 +69,7 @@ export function cimdDocumentToApp(clientId: string, document: unknown): Models.A : [], tagline: '', tags: [], + labels: [], images: [], supportUrl: '', dataDeletionUrl: '', @@ -83,6 +84,8 @@ export function cimdDocumentToApp(clientId: string, document: unknown): Models.A deviceFlow: Array.isArray(doc.grant_types) && doc.grant_types.includes(DEVICE_GRANT_TYPE), teamId: '', userId: '', + installationScopes: [], + installationRedirectUrl: '', secrets: [] }; } diff --git a/src/routes/(console)/project-[region]-[project]/auth/security/updateMfaFactors.svelte b/src/routes/(console)/project-[region]-[project]/auth/security/updateMfaFactors.svelte index ff4c37bb51..b435aad413 100644 --- a/src/routes/(console)/project-[region]-[project]/auth/security/updateMfaFactors.svelte +++ b/src/routes/(console)/project-[region]-[project]/auth/security/updateMfaFactors.svelte @@ -5,6 +5,7 @@ import { Dependencies } from '$lib/constants'; import { Button, Form } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; + import { canWriteProjects } from '$lib/stores/roles'; import { sdk } from '$lib/stores/sdk'; import { Selector } from '@appwrite.io/pink-svelte'; import type { Models } from '@appwrite.io/console'; @@ -63,25 +64,29 @@ id="mfaFactorTotp" label="TOTP" description="Time-based codes from an authenticator app" + disabled={!$canWriteProjects} bind:checked={totp} /> - + From 28b46d9f7bf1fa38855fca889cd30a23ed24077d Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Mon, 17 Aug 2026 16:24:21 +0530 Subject: [PATCH 4/4] fix(usage): scope the executions breakdown to function executions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `executions` is the umbrella metric covering both functions and sites, but the table it feeds is headed "Function" and links each row to /functions/function-{resourceId}. A project with a trafficked site would get rows showing a raw site ID, unresolvable by functions.list, linking to a function that does not exist. `functions.executions` is the metric that matches the table. Fold the call into the load function's existing Promise.all so it no longer serialises behind the invoice and usage requests, and report a failure through trackError instead of swallowing it — a 403 or a malformed response was previously indistinguishable from a project with no executions. Drop the duplicate database-type read in the collection layout, which recomputed locally what the component already derives. --- src/lib/actions/analytics.ts | 1 + src/lib/sdk/usage.ts | 7 ++++++- .../collection-[collection]/+layout.svelte | 3 +-- .../settings/usage/[[invoice]]/+page.ts | 19 ++++++++++--------- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 80ef51848b..ebd6e8abf0 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -259,6 +259,7 @@ export enum Submit { ProjectService = 'submit_project_service', ProjectUpdateSMTP = 'submit_project_update_smtp', ProjectUpdateOAuth2Server = 'submit_project_update_oauth2_server', + ProjectUsageExecutionsBreakdown = 'submit_project_usage_executions_breakdown', ProjectResume = 'submit_project_resume', MemberCreate = 'submit_member_create', MemberDelete = 'submit_member_delete', diff --git a/src/lib/sdk/usage.ts b/src/lib/sdk/usage.ts index d5bd0b05e6..d84a0cc26f 100644 --- a/src/lib/sdk/usage.ts +++ b/src/lib/sdk/usage.ts @@ -37,6 +37,11 @@ const executionsBreakdownLimit = 25; /** * `UsageProject.executionsBreakdown` was dropped in SDK 16; the per-resource split now comes from * the dimensional usage API, which returns bare resource IDs, so names are resolved separately. + * + * Scoped to `functions.executions` rather than the umbrella `executions` metric, which also counts + * site executions — those resolve to no name here and their rows link to a function that does not exist. + * Omitting `interval` makes each point a whole-window aggregate per resource, so the limit is a + * top-N-by-total rather than a truncation of the underlying data. */ export async function listExecutionsBreakdown( region: string, @@ -47,7 +52,7 @@ export async function listExecutionsBreakdown( const project = sdk.forProject(region, projectId); const events = await project.usage.listEvents({ - metrics: [UsageEventMetric.Executions], + metrics: [UsageEventMetric.FunctionsExecutions], dimensions: [UsageEventDimension.ResourceId], startAt, endAt, 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 d9ecf98ed3..6db36f36d6 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 @@ -257,8 +257,7 @@ const { rows, ids } = generateFakeRecords($randomDataModalState.value, fields); documentIds = ids; - const dbType = data.database?.type; - const isVectorsDb = dbType === 'vectorsdb'; + const isVectorsDb = databaseType === 'vectorsdb'; const dimension = collection?.dimension ?? DEFAULT_VECTOR_DIMENSION; // For vectorsdb, wrap fields in metadata and add empty embeddings diff --git a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts index 3fb49bd26b..3a365a155e 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts @@ -1,4 +1,5 @@ import { sdk } from '$lib/stores/sdk'; +import { Submit, trackError } from '$lib/actions/analytics'; import type { PageLoad } from './$types'; import { accumulateUsage, listExecutionsBreakdown, type ExecutionsBreakdown } from '$lib/sdk/usage'; import { type Models, Query } from '@appwrite.io/console'; @@ -35,12 +36,19 @@ export const load: PageLoad = async ({ params, parent }) => { } } - const [invoices, usage] = await Promise.all([ + const [invoices, usage, executionsBreakdown] = await Promise.all([ sdk.forConsole.organizations.listInvoices({ organizationId: organization.$id, queries: [Query.orderDesc('from')] }), - sdk.forProject(region, project).project.getUsage({ startDate, endDate }) + sdk.forProject(region, project).project.getUsage({ startDate, endDate }), + listExecutionsBreakdown(region, project, startDate, endDate).catch( + (error): ExecutionsBreakdown[] => { + // supplementary to the executions chart, so never fail the page for it + trackError(error, Submit.ProjectUsageExecutionsBreakdown); + return []; + } + ) ]); if (currentAggregation) { @@ -66,13 +74,6 @@ export const load: PageLoad = async ({ params, parent }) => { usage.users = accumulateUsage(usage.users, usage.usersTotal); - let executionsBreakdown: ExecutionsBreakdown[] = []; - try { - executionsBreakdown = await listExecutionsBreakdown(region, project, startDate, endDate); - } catch { - // supplementary — a server without the usage events API still renders the rest of the page - } - return { usage, invoices,