mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 02:26:21 +08:00
41b3b0e82e
#### PR Dependency Tree * **PR #14510** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved calendar sync reliability with exponential backoff for repeated failures. * Better handling of token refresh failures with automatic account invalidation and cleanup when needed. * Subscriptions are now automatically disabled and related events removed when the calendar provider reports missing resources. * **Tests** * Added comprehensive tests covering sync failures, backoff behavior, token refresh handling, skipping retries during backoff, and recovery. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
204 lines
5.9 KiB
TypeScript
204 lines
5.9 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { Transactional } from '@nestjs-cls/transactional';
|
|
import type { CalendarAccount, Prisma } from '@prisma/client';
|
|
|
|
import { CryptoHelper } from '../base';
|
|
import { BaseModel } from './base';
|
|
|
|
export interface CalendarAccountTokens {
|
|
accessToken?: string | null;
|
|
refreshToken?: string | null;
|
|
expiresAt?: Date | null;
|
|
scope?: string | null;
|
|
}
|
|
|
|
export interface UpsertCalendarAccountInput extends CalendarAccountTokens {
|
|
userId: string;
|
|
provider: string;
|
|
providerAccountId: string;
|
|
displayName?: string | null;
|
|
email?: string | null;
|
|
providerPresetId?: string | null;
|
|
serverUrl?: string | null;
|
|
principalUrl?: string | null;
|
|
calendarHomeUrl?: string | null;
|
|
username?: string | null;
|
|
authType?: string | null;
|
|
status?: string | null;
|
|
lastError?: string | null;
|
|
refreshIntervalMinutes?: number | null;
|
|
}
|
|
|
|
export interface UpdateCalendarAccountTokensInput extends CalendarAccountTokens {
|
|
status?: string | null;
|
|
lastError?: string | null;
|
|
}
|
|
|
|
@Injectable()
|
|
export class CalendarAccountModel extends BaseModel {
|
|
constructor(private readonly crypto: CryptoHelper) {
|
|
super();
|
|
}
|
|
|
|
private encryptToken(token?: string | null) {
|
|
return token ? this.crypto.encrypt(token) : null;
|
|
}
|
|
|
|
private decryptToken(token?: string | null) {
|
|
return token ? this.crypto.decrypt(token) : null;
|
|
}
|
|
|
|
async listByUser(userId: string) {
|
|
return await this.db.calendarAccount.findMany({
|
|
where: { userId },
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
}
|
|
|
|
async get(id: string) {
|
|
return await this.db.calendarAccount.findUnique({
|
|
where: { id },
|
|
});
|
|
}
|
|
|
|
async getByProviderAccount(
|
|
userId: string,
|
|
provider: string,
|
|
providerAccountId: string
|
|
) {
|
|
return await this.db.calendarAccount.findFirst({
|
|
where: { userId, provider, providerAccountId },
|
|
});
|
|
}
|
|
|
|
async upsert(input: UpsertCalendarAccountInput) {
|
|
const accessToken = this.encryptToken(input.accessToken);
|
|
const refreshToken = this.encryptToken(input.refreshToken);
|
|
const data: Prisma.CalendarAccountUncheckedCreateInput = {
|
|
userId: input.userId,
|
|
provider: input.provider,
|
|
providerAccountId: input.providerAccountId,
|
|
displayName: input.displayName ?? null,
|
|
email: input.email ?? null,
|
|
providerPresetId: input.providerPresetId ?? null,
|
|
serverUrl: input.serverUrl ?? null,
|
|
principalUrl: input.principalUrl ?? null,
|
|
calendarHomeUrl: input.calendarHomeUrl ?? null,
|
|
username: input.username ?? null,
|
|
authType: input.authType ?? null,
|
|
accessToken: accessToken ?? null,
|
|
refreshToken: refreshToken ?? null,
|
|
expiresAt: input.expiresAt ?? null,
|
|
scope: input.scope ?? null,
|
|
status: input.status ?? 'active',
|
|
lastError: input.lastError ?? null,
|
|
refreshIntervalMinutes: input.refreshIntervalMinutes ?? 60,
|
|
};
|
|
|
|
const updateData: Prisma.CalendarAccountUncheckedUpdateInput = {
|
|
displayName: data.displayName,
|
|
email: data.email,
|
|
providerPresetId: data.providerPresetId,
|
|
serverUrl: data.serverUrl,
|
|
principalUrl: data.principalUrl,
|
|
calendarHomeUrl: data.calendarHomeUrl,
|
|
username: data.username,
|
|
authType: data.authType,
|
|
expiresAt: data.expiresAt,
|
|
scope: data.scope,
|
|
status: data.status,
|
|
lastError: data.lastError,
|
|
refreshIntervalMinutes: data.refreshIntervalMinutes,
|
|
};
|
|
|
|
if (accessToken) {
|
|
updateData.accessToken = accessToken;
|
|
}
|
|
if (refreshToken) {
|
|
updateData.refreshToken = refreshToken;
|
|
}
|
|
|
|
return await this.db.calendarAccount.upsert({
|
|
where: {
|
|
userId_provider_providerAccountId: {
|
|
userId: input.userId,
|
|
provider: input.provider,
|
|
providerAccountId: input.providerAccountId,
|
|
},
|
|
},
|
|
create: data,
|
|
update: updateData,
|
|
});
|
|
}
|
|
|
|
async updateTokens(id: string, input: UpdateCalendarAccountTokensInput) {
|
|
const data: Prisma.CalendarAccountUncheckedUpdateInput = {};
|
|
if (input.accessToken !== undefined) {
|
|
data.accessToken = this.encryptToken(input.accessToken);
|
|
}
|
|
if (input.refreshToken !== undefined) {
|
|
data.refreshToken = this.encryptToken(input.refreshToken);
|
|
}
|
|
if (input.expiresAt !== undefined) {
|
|
data.expiresAt = input.expiresAt ?? null;
|
|
}
|
|
if (input.scope !== undefined) {
|
|
data.scope = input.scope ?? null;
|
|
}
|
|
if (input.status !== undefined) {
|
|
data.status = input.status ?? undefined;
|
|
}
|
|
if (input.lastError !== undefined) {
|
|
data.lastError = input.lastError ?? null;
|
|
}
|
|
|
|
return await this.db.calendarAccount.update({
|
|
where: { id },
|
|
data,
|
|
});
|
|
}
|
|
|
|
async updateStatus(id: string, status: string, lastError?: string | null) {
|
|
return await this.db.calendarAccount.update({
|
|
where: { id },
|
|
data: {
|
|
status,
|
|
lastError: lastError ?? null,
|
|
},
|
|
});
|
|
}
|
|
|
|
async updateRefreshInterval(id: string, refreshIntervalMinutes: number) {
|
|
return await this.db.calendarAccount.update({
|
|
where: { id },
|
|
data: { refreshIntervalMinutes },
|
|
});
|
|
}
|
|
|
|
@Transactional()
|
|
async invalidateAndPurge(id: string, lastError?: string | null) {
|
|
await this.updateStatus(id, 'invalid', lastError ?? null);
|
|
const subscriptions =
|
|
await this.models.calendarSubscription.listByAccount(id);
|
|
const subscriptionIds = subscriptions.map(subscription => subscription.id);
|
|
if (subscriptionIds.length > 0) {
|
|
await this.models.calendarEvent.deleteBySubscriptionIds(subscriptionIds);
|
|
}
|
|
await this.models.calendarSubscription.clearSyncTokensByAccount(id);
|
|
}
|
|
|
|
async delete(id: string) {
|
|
return await this.db.calendarAccount.delete({
|
|
where: { id },
|
|
});
|
|
}
|
|
|
|
decryptTokens(account: CalendarAccount) {
|
|
return {
|
|
...account,
|
|
accessToken: this.decryptToken(account.accessToken),
|
|
refreshToken: this.decryptToken(account.refreshToken),
|
|
};
|
|
}
|
|
}
|