Merge pull request #2775 from appwrite/fix-dat-1012

This commit is contained in:
Darshan
2026-01-08 14:35:49 +05:30
committed by GitHub
12 changed files with 109 additions and 424 deletions
+4 -4
View File
@@ -5,7 +5,6 @@
import { isCloud, isSelfHosted } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { BillingPlan, Dependencies } from '$lib/constants';
import type { BackupArchive, BackupRestoration } from '$lib/sdk/backups';
import { goto, invalidate } from '$app/navigation';
import { page } from '$app/state';
import { addNotification } from '$lib/stores/notifications';
@@ -13,10 +12,11 @@
import { getProjectId } from '$lib/helpers/project';
import { toLocaleDate } from '$lib/helpers/date';
import { Typography } from '@appwrite.io/pink-svelte';
import { type Models } from '@appwrite.io/console';
const backupRestoreItems: {
archives: Map<string, BackupArchive>;
restorations: Map<string, BackupRestoration>;
archives: Map<string, Models.BackupArchive>;
restorations: Map<string, Models.BackupRestoration>;
} = {
archives: new Map(),
restorations: new Map()
@@ -117,7 +117,7 @@
if (which === 'restorations') lastDatabaseRestorationId = null;
}
function backupName(item: BackupArchive | BackupRestoration, key: string) {
function backupName(item: Models.BackupArchive | Models.BackupRestoration, key: string) {
const column = key === 'archives' ? '$createdAt' : 'startedAt';
return toLocaleDate(item[column]);
-332
View File
@@ -1,332 +0,0 @@
import { AppwriteException, Client, type Payload } from '@appwrite.io/console';
export type BackupPolicyList = {
total: number;
policies: BackupPolicy[];
};
export type BackupArchiveList = {
total: number;
archives: BackupArchive[];
};
export type BackupRestorationList = {
total: number;
restorations: BackupRestoration[];
};
export type BackupPolicy = {
$id: string;
name: string;
$createdAt: string;
$updatedAt: string;
services: string[];
resources: string[];
resourceId?: string;
resourceType?: string;
retention: number;
schedule: string;
enabled: boolean;
};
export type BackupArchive = {
$id: string;
$createdAt: string;
$updatedAt: string;
policyId: string;
size: number;
status: string;
startedAt: string;
migrationId: string;
services: string[];
resources: string[];
resourceId?: string;
resourceType?: string;
};
export type BackupRestoration = {
$id: string;
$createdAt: string;
$updatedAt: string;
archiveId: string;
policyId: string;
status: string;
startedAt: string;
migrationId: string;
services: string[];
resources: string[];
options: string;
};
export class Backups {
client: Client;
constructor(client: Client) {
this.client = client;
}
async createArchive(services: string[], resourceId?: string): Promise<BackupArchive> {
if (typeof services === 'undefined') {
throw new AppwriteException('Missing required parameter: "services"');
}
const apiPath = '/backups/archives';
const payload: Payload = {};
if (typeof services !== 'undefined') {
payload['services'] = services;
}
if (typeof resourceId !== 'undefined') {
payload['resourceId'] = resourceId;
}
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('post', uri, apiHeaders, payload);
}
async deleteArchive(archiveId: string): Promise<object> {
if (typeof archiveId === 'undefined') {
throw new AppwriteException('Missing required parameter: "archiveId"');
}
const apiPath = '/backups/archives/{archiveId}'.replace('{archiveId}', archiveId);
const payload: Payload = {};
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('delete', uri, apiHeaders, payload);
}
async listArchives(queries?: string[]): Promise<BackupArchiveList> {
const apiPath = '/backups/archives';
const payload: Payload = {};
if (typeof queries !== 'undefined') {
payload['queries'] = queries;
}
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('get', uri, apiHeaders, payload);
}
async getArchive(archiveId: string): Promise<BackupArchive> {
if (typeof archiveId === 'undefined') {
throw new AppwriteException('Missing required parameter: "archiveId"');
}
const apiPath = '/backups/archives/{archiveId}'.replace('{archiveId}', archiveId);
const payload: Payload = {};
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('get', uri, apiHeaders, payload);
}
async listPolicies(queries?: string[]): Promise<BackupPolicyList> {
const apiPath = '/backups/policies';
const payload: Payload = {};
if (typeof queries !== 'undefined') {
payload['queries'] = queries;
}
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('get', uri, apiHeaders, payload);
}
async createPolicy(
policyId: string,
services: string[],
retention: number,
schedule: string,
name?: string,
resourceId?: string,
enabled?: boolean
): Promise<BackupPolicy> {
if (typeof policyId === 'undefined') {
throw new AppwriteException('Missing required parameter: "policyId"');
}
if (typeof services === 'undefined') {
throw new AppwriteException('Missing required parameter: "services"');
}
if (typeof retention === 'undefined') {
throw new AppwriteException('Missing required parameter: "retention"');
}
if (typeof schedule === 'undefined') {
throw new AppwriteException('Missing required parameter: "schedule"');
}
const apiPath = '/backups/policies';
const payload: Payload = {};
if (typeof policyId !== 'undefined') {
payload['policyId'] = policyId;
}
if (typeof name !== 'undefined') {
payload['name'] = name;
}
if (typeof services !== 'undefined') {
payload['services'] = services;
}
if (typeof resourceId !== 'undefined') {
payload['resourceId'] = resourceId;
}
if (typeof enabled !== 'undefined') {
payload['enabled'] = enabled;
}
if (typeof retention !== 'undefined') {
payload['retention'] = retention;
}
if (typeof schedule !== 'undefined') {
payload['schedule'] = schedule;
}
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('post', uri, apiHeaders, payload);
}
async getPolicy(policyId: string): Promise<BackupPolicy> {
if (typeof policyId === 'undefined') {
throw new AppwriteException('Missing required parameter: "policyId"');
}
const apiPath = '/backups/policies/{policyId}'.replace('{policyId}', policyId);
const payload: Payload = {};
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('get', uri, apiHeaders, payload);
}
async updatePolicy(
policyId: string,
name?: string,
retention?: number,
schedule?: string,
enabled?: boolean
): Promise<BackupPolicy> {
if (typeof policyId === 'undefined') {
throw new AppwriteException('Missing required parameter: "policyId"');
}
const apiPath = '/backups/policies/{policyId}'.replace('{policyId}', policyId);
const payload: Payload = {};
if (typeof name !== 'undefined') {
payload['name'] = name;
}
if (typeof retention !== 'undefined') {
payload['retention'] = retention;
}
if (typeof schedule !== 'undefined') {
payload['schedule'] = schedule;
}
if (typeof enabled !== 'undefined') {
payload['enabled'] = enabled;
}
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('patch', uri, apiHeaders, payload);
}
async deletePolicy(policyId: string): Promise<object> {
if (typeof policyId === 'undefined') {
throw new AppwriteException('Missing required parameter: "policyId"');
}
const apiPath = '/backups/policies/{policyId}'.replace('{policyId}', policyId);
const payload: Payload = {};
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('delete', uri, apiHeaders, payload);
}
async createRestoration(
archiveId: string,
services: string[],
newResourceId?: string,
newResourceName?: string
): Promise<BackupRestoration> {
if (typeof archiveId === 'undefined') {
throw new AppwriteException('Missing required parameter: "archiveId"');
}
if (typeof services === 'undefined') {
throw new AppwriteException('Missing required parameter: "services"');
}
const apiPath = '/backups/restoration';
const payload: Payload = {};
if (typeof archiveId !== 'undefined') {
payload['archiveId'] = archiveId;
}
if (typeof services !== 'undefined') {
payload['services'] = services;
}
if (typeof newResourceId !== 'undefined') {
payload['newResourceId'] = newResourceId;
}
if (typeof newResourceName !== 'undefined') {
payload['newResourceName'] = newResourceName;
}
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('post', uri, apiHeaders, payload);
}
async listRestorations(queries?: string[]): Promise<BackupRestorationList> {
const apiPath = '/backups/restorations';
const payload: Payload = {};
if (typeof queries !== 'undefined') {
payload['queries'] = queries;
}
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('get', uri, apiHeaders, payload);
}
async getRestoration(restorationId: string): Promise<BackupArchive> {
if (typeof restorationId === 'undefined') {
throw new AppwriteException('Missing required parameter: "restorationId"');
}
const apiPath = '/backups/restorations/{restorationId}'.replace(
'{restorationId}',
restorationId
);
const payload: Payload = {};
const uri = new URL(this.client.config.endpoint + apiPath);
const apiHeaders: { [header: string]: string } = {
'content-type': 'application/json'
};
return await this.client.call('get', uri, apiHeaders, payload);
}
}
+3 -3
View File
@@ -27,9 +27,9 @@ export async function checkForDatabaseBackupPolicies(
if (isCloud && backupsEnabled) {
try {
const policies = await sdk
.forProject(region, projectId)
.backups.listPolicies([Query.limit(1), Query.equal('resourceId', database.$id)]);
const policies = await sdk.forProject(region, projectId).backups.listPolicies({
queries: [Query.limit(1), Query.equal('resourceId', database.$id)]
});
total = policies.total;
} catch (e) {
+1 -1
View File
@@ -3,6 +3,7 @@ import {
Account,
Assistant,
Avatars,
Backups,
Client,
Console,
Functions,
@@ -26,7 +27,6 @@ import {
Organizations
} from '@appwrite.io/console';
import { Billing } from '../sdk/billing';
import { Backups } from '../sdk/backups';
import { Sources } from '$lib/sdk/sources';
import {
REGION_FRA,
@@ -77,7 +77,11 @@
{#if data.view === 'grid'}
<Grid {data} bind:showCreate />
{:else}
<Table {data} />
<Table
tables={data.tables}
policies={data.policies}
databases={data.databases}
lastBackups={data.lastBackups} />
{/if}
<PaginationWithLimit
@@ -4,7 +4,6 @@ import { sdk } from '$lib/stores/sdk';
import { type Models, Query } from '@appwrite.io/console';
import { timeFromNow } from '$lib/helpers/date';
import type { PageLoad, RouteParams } from './$types';
import type { BackupPolicy } from '$lib/sdk/backups';
import { isSelfHosted } from '$lib/system';
import { isCloud } from '$lib/system';
import type { Plan } from '$lib/sdk/billing';
@@ -71,7 +70,8 @@ async function fetchDatabasesAndBackups(
})
);
let lastBackups: Record<string, string>, policies: Record<string, BackupPolicy[]>;
let lastBackups: Record<string, string> = {};
let policies: Record<string, Models.BackupPolicy[]> = {};
if (isCloud && backupsEnabled) {
[policies, lastBackups] = await Promise.all([
@@ -86,19 +86,21 @@ async function fetchDatabasesAndBackups(
async function fetchPolicies(databases: Models.DatabaseList, params: RouteParams) {
if (isSelfHosted) return {};
const databasePolicies: Record<string, BackupPolicy[]> = {};
const databasePolicies: Record<string, Models.BackupPolicy[]> = {};
await Promise.all(
databases.databases.map(async (database) => {
try {
const { policies } = await sdk
.forProject(params.region, params.project)
.backups.listPolicies([
// TODO: are all needed!?
// Query.limit(3),
Query.equal('resourceType', 'database'),
Query.equal('resourceId', database.$id)
]);
.backups.listPolicies({
queries: [
// TODO: are all needed!?
// Query.limit(3),
Query.equal('resourceType', 'database'),
Query.equal('resourceId', database.$id)
]
});
if (policies.length > 0) {
databasePolicies[database.$id] = policies;
@@ -122,12 +124,14 @@ async function fetchLastBackups(databases: Models.DatabaseList, params: RoutePar
try {
const { archives } = await sdk
.forProject(params.region, params.project)
.backups.listArchives([
Query.limit(1),
Query.orderDesc('$createdAt'),
Query.equal('resourceType', 'database'),
Query.equal('resourceId', database.$id)
]);
.backups.listArchives({
queries: [
Query.limit(1),
Query.orderDesc('$createdAt'),
Query.equal('resourceType', 'database'),
Query.equal('resourceId', database.$id)
]
});
if (archives.length > 0) {
lastBackups[database.$id] = timeFromNow(archives[0].$createdAt);
@@ -61,16 +61,14 @@
const totalPoliciesPromise = totalPolicies.map((policy) => {
cronExpression(policy);
return sdk
.forProject(page.params.region, page.params.project)
.backups.createPolicy(
ID.unique(),
['databases'],
policy.retained,
policy.schedule,
policy.label,
resourceId
);
return sdk.forProject(page.params.region, page.params.project).backups.createPolicy({
policyId: ID.unique(),
services: ['databases'],
retention: policy.retained,
schedule: policy.schedule,
name: policy.label,
resourceId
});
});
await Promise.all(totalPoliciesPromise);
@@ -66,9 +66,10 @@
const createManualBackup = async () => {
try {
await sdk
.forProject(page.params.region, page.params.project)
.backups.createArchive(['databases'], data.database.$id);
await sdk.forProject(page.params.region, page.params.project).backups.createArchive({
services: ['databases'],
resourceId: data.database.$id
});
await invalidate(Dependencies.BACKUPS);
addNotification({
type: 'success',
@@ -119,16 +120,14 @@
const totalPoliciesPromise = totalPolicies.map((policy) => {
cronExpression(policy);
return sdk
.forProject(page.params.region, page.params.project)
.backups.createPolicy(
ID.unique(),
['databases'],
policy.retained,
policy.schedule,
policy.label,
data.database.$id
);
return sdk.forProject(page.params.region, page.params.project).backups.createPolicy({
policyId: ID.unique(),
services: ['databases'],
retention: policy.retained,
schedule: policy.schedule,
name: policy.label,
resourceId: data.database.$id
});
});
try {
@@ -2,8 +2,8 @@ import { getLimit, getPage, getView, pageToOffset, View } from '$lib/helpers/loa
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
import type { BackupArchive, BackupArchiveList, BackupPolicyList } from '$lib/sdk/backups';
import { isCloud } from '$lib/system';
import { type Models } from '@appwrite.io/console';
export const load = async ({ params, url, route, depends, parent }) => {
depends(Dependencies.BACKUPS);
@@ -13,8 +13,8 @@ export const load = async ({ params, url, route, depends, parent }) => {
const view = getView(url, route, View.Grid);
const offset = pageToOffset(page, limit);
let backups: BackupArchiveList = { total: 0, archives: [] };
let policies: BackupPolicyList = { total: 0, policies: [] };
let backups: Models.BackupArchiveList = { total: 0, archives: [] };
let policies: Models.BackupPolicyList = { total: 0, policies: [] };
// already loaded by parent.
const { currentPlan } = await parent();
@@ -23,23 +23,23 @@ export const load = async ({ params, url, route, depends, parent }) => {
if (isCloud && backupsEnabled) {
try {
[backups, policies] = await Promise.all([
sdk
.forProject(params.region, params.project)
.backups.listArchives([
sdk.forProject(params.region, params.project).backups.listArchives({
queries: [
Query.limit(limit),
Query.offset(offset),
Query.orderDesc('$createdAt'),
Query.equal('resourceType', 'database'),
Query.equal('resourceId', params.database)
]),
]
}),
sdk
.forProject(params.region, params.project)
.backups.listPolicies([
sdk.forProject(params.region, params.project).backups.listPolicies({
queries: [
Query.orderDesc('$createdAt'),
Query.equal('resourceType', 'database'),
Query.equal('resourceId', params.database)
])
]
})
]);
} catch (e) {
// ignore
@@ -59,17 +59,17 @@ export const load = async ({ params, url, route, depends, parent }) => {
};
};
const groupArchivesByPolicy = (archives: BackupArchive[]) => {
const groupArchivesByPolicy = (archives: Models.BackupArchive[]) => {
return archives.reduce((acc, archive) => {
if (!acc.has(archive.policyId)) {
acc.set(archive.policyId, []);
}
acc.get(archive.policyId)!.push(archive);
return acc;
}, new Map<string, BackupArchive[]>());
}, new Map<string, Models.BackupArchive[]>());
};
const getLatestBackupForPolicies = (policyIdMap: Map<string, BackupArchive[]>) => {
const getLatestBackupForPolicies = (policyIdMap: Map<string, Models.BackupArchive[]>) => {
const latestBackups = new Map<string, string | null>();
for (const [policyId, archives] of policyIdMap) {
const latestBackup = archives.sort(
@@ -11,7 +11,6 @@
import { toLocaleDateTime } from '$lib/helpers/date';
import EmptyDark from '$lib/images/backups/backups-dark.png';
import EmptyLight from '$lib/images/backups/backups-light.png';
import type { BackupPolicy, BackupPolicyList } from '$lib/sdk/backups';
import { backupFrequencies } from '$lib/helpers/backups';
import { Click, trackEvent } from '$lib/actions/analytics';
import {
@@ -27,21 +26,22 @@
import { Confirm } from '$lib/components/index.js';
import Ellipse from './components/Ellipse.svelte';
import { page } from '$app/state';
import { type Models } from '@appwrite.io/console';
let showDelete = false;
let selectedPolicy: BackupPolicy = null;
let selectedPolicy: Models.BackupPolicy = null;
let showEveryPolicy = false;
export let showCreatePolicy = false;
export let policies: BackupPolicyList;
export let policies: Models.BackupPolicyList;
export let lastBackupDates: Record<string, string>;
async function deletePolicy() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.backups.deletePolicy(selectedPolicy.$id);
await sdk.forProject(page.params.region, page.params.project).backups.deletePolicy({
policyId: selectedPolicy.$id
});
addNotification({
type: 'success',
message: 'Backup policy has been deleted'
@@ -15,10 +15,9 @@
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
import { calculateSize } from '$lib/helpers/sizeConvertion';
import { ID } from '@appwrite.io/console';
import { ID, type Models } from '@appwrite.io/console';
import { columns } from './store';
import { database } from '../store';
import type { BackupArchive, BackupPolicy } from '$lib/sdk/backups';
import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { copy } from '$lib/helpers/copy';
import { LabelCard } from '$lib/components/index.js';
@@ -53,7 +52,7 @@
} = $props();
let showDelete = $state(false);
let selectedBackup: BackupArchive | null = $state(null);
let selectedBackup: Models.BackupArchive | null = $state(null);
let showDropdown = [];
@@ -87,15 +86,15 @@
);
});
function getPolicyDetails(policyId: string | null): BackupPolicy | null {
function getPolicyDetails(policyId: string | null): Models.BackupPolicy | null {
return data.policies.policies.find((policy) => policy.$id === policyId);
}
function getCleanBackupName(backup: BackupArchive): string {
function getCleanBackupName(backup: Models.BackupArchive): string {
return toLocaleDateTime(backup.$createdAt).replaceAll(',', '');
}
function getBackupStatus(backup: BackupArchive) {
function getBackupStatus(backup: Models.BackupArchive) {
switch (backup.status) {
case 'pending':
return 'pending';
@@ -115,7 +114,7 @@
try {
await sdk
.forProject(page.params.region, page.params.project)
.backups.deleteArchive(archiveId);
.backups.deleteArchive({ archiveId });
addNotification({
type: 'success',
@@ -135,7 +134,9 @@
async function deleteBackups(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
const result = await batchDelete((archiveId) =>
sdk.forProject(page.params.region, page.params.project).backups.deleteArchive(archiveId)
sdk
.forProject(page.params.region, page.params.project)
.backups.deleteArchive({ archiveId })
);
try {
@@ -160,12 +161,12 @@
try {
await sdk
.forProject(page.params.region, page.params.project)
.backups.createRestoration(
selectedBackup.$id,
['databases'],
newDatabaseInfo.id ?? ID.unique(),
newDatabaseInfo.name
);
.backups.createRestoration({
archiveId: selectedBackup.$id,
services: ['databases'],
newResourceId: newDatabaseInfo.id ?? ID.unique(),
newResourceName: newDatabaseInfo.name
});
await invalidate(Dependencies.BACKUPS);
addNotification({
@@ -6,10 +6,19 @@
import { columns } from './store';
import { IconExclamation } from '@appwrite.io/pink-icons-svelte';
import { Layout, Tooltip, Table, Icon } from '@appwrite.io/pink-svelte';
import type { BackupPolicy } from '$lib/sdk/backups';
import { type Models } from '@appwrite.io/console';
export let data;
const tables = data.tables;
let {
tables,
policies,
databases,
lastBackups
}: {
tables: Record<string, string>;
databases: Models.DatabaseList;
lastBackups: Record<string, string>;
policies: Record<string, Models.BackupPolicy[]>;
} = $props();
function getPolicyDescription(cron: string): string {
const [minute, hour, dayOfMonth, , dayOfWeek] = cron.split(' ');
@@ -19,6 +28,10 @@
if (minute !== '*' && hour === '*') return 'Hourly';
if (hour !== '*') return 'Daily';
}
function getPoliciesDescription(policies: Models.BackupPolicy[] | null): string {
return policies?.map((policy) => getPolicyDescription(policy.schedule)).join(', ') ?? '';
}
</script>
<Table.Root columns={$columns} let:root>
@@ -27,7 +40,7 @@
<Table.Header.Cell column={id} {root}>{title}</Table.Header.Cell>
{/each}
</svelte:fragment>
{#each data.databases.databases as database (database.$id)}
{#each databases.databases as database (database.$id)}
<!-- takes directly to the spreadsheet -->
{@const tableId = tables[database?.$id] ?? null}
{@const tableHref = tableId ? `/table-${tableId}` : ''}
@@ -45,18 +58,16 @@
{:else if column.id === 'name'}
{database.name}
{:else if column.id === 'backup'}
{@const policies = data.policies?.[database.$id] ?? null}
{@const lastBackup = data.lastBackups?.[database.$id] ?? null}
{@const description = policies
?.map((policy: BackupPolicy) => getPolicyDescription(policy.schedule))
.join(', ')}
{@const backupPolicies = policies?.[database.$id] ?? null}
{@const lastBackup = lastBackups?.[database.$id] ?? null}
{@const description = getPoliciesDescription(backupPolicies)}
<Tooltip
placement="bottom"
disabled={!policies || !lastBackup}
disabled={!backupPolicies || !lastBackup}
maxWidth="fit-content">
<span class="u-trim">
{#if !policies}
{#if !backupPolicies}
<Layout.Stack direction="row" gap="xxs" alignItems="center">
<Icon
icon={IconExclamation}