Files
AFFiNE-Mirror/packages/backend/server/src/models/calendar-subscription.ts
T
DarkSky 41b3b0e82e feat: handle calendar sync failed (#14510)
#### 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 -->
2026-02-24 22:45:47 +08:00

212 lines
5.4 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { Transactional } from '@nestjs-cls/transactional';
import type { CalendarSubscription, Prisma } from '@prisma/client';
import { BaseModel } from './base';
export interface UpsertCalendarSubscriptionInput {
accountId: string;
provider: string;
externalCalendarId: string;
displayName?: string | null;
timezone?: string | null;
color?: string | null;
enabled?: boolean;
}
export interface UpdateCalendarSubscriptionSyncInput {
syncToken?: string | null;
lastSyncAt?: Date | null;
}
export interface UpdateCalendarSubscriptionChannelInput {
customChannelId?: string | null;
customResourceId?: string | null;
channelExpiration?: Date | null;
}
@Injectable()
export class CalendarSubscriptionModel extends BaseModel {
async listByAccount(accountId: string) {
return await this.db.calendarSubscription.findMany({
where: { accountId },
orderBy: { createdAt: 'asc' },
});
}
async listByAccountIds(accountIds: string[]) {
return await this.db.calendarSubscription.findMany({
where: { accountId: { in: accountIds } },
});
}
async get(id: string) {
return await this.db.calendarSubscription.findUnique({
where: { id },
});
}
async getByChannelId(customChannelId: string) {
return await this.db.calendarSubscription.findFirst({
where: { customChannelId },
});
}
async upsert(input: UpsertCalendarSubscriptionInput) {
const data: Prisma.CalendarSubscriptionUncheckedCreateInput = {
accountId: input.accountId,
provider: input.provider,
externalCalendarId: input.externalCalendarId,
displayName: input.displayName ?? null,
timezone: input.timezone ?? null,
color: input.color ?? null,
enabled: input.enabled ?? true,
};
return await this.db.calendarSubscription.upsert({
where: {
accountId_externalCalendarId: {
accountId: input.accountId,
externalCalendarId: input.externalCalendarId,
},
},
create: data,
update: {
displayName: data.displayName,
timezone: data.timezone,
color: data.color,
enabled: data.enabled,
},
});
}
async updateSync(id: string, input: UpdateCalendarSubscriptionSyncInput) {
return await this.db.calendarSubscription.update({
where: { id },
data: {
syncToken: input.syncToken ?? null,
lastSyncAt: input.lastSyncAt ?? null,
},
});
}
async updateChannel(
id: string,
input: UpdateCalendarSubscriptionChannelInput
) {
return await this.db.calendarSubscription.update({
where: { id },
data: {
customChannelId: input.customChannelId ?? null,
customResourceId: input.customResourceId ?? null,
channelExpiration: input.channelExpiration ?? null,
},
});
}
async updateEnabled(id: string, enabled: boolean) {
return await this.db.calendarSubscription.update({
where: { id },
data: { enabled },
});
}
async deleteByAccount(accountId: string) {
return await this.db.calendarSubscription.deleteMany({
where: { accountId },
});
}
async deleteByIds(ids: string[]) {
return await this.db.calendarSubscription.deleteMany({
where: { id: { in: ids } },
});
}
async listActiveByAccount(accountId: string) {
return await this.db.calendarSubscription.findMany({
where: { accountId, enabled: true },
});
}
async listWithAccount(id: string) {
return await this.db.calendarSubscription.findUnique({
where: { id },
include: { account: true },
});
}
async listWithAccounts(ids: string[]) {
return await this.db.calendarSubscription.findMany({
where: { id: { in: ids } },
include: { account: true },
});
}
async listAccountSubscriptions(
accountId: string,
subscriptionIds?: string[]
) {
return await this.db.calendarSubscription.findMany({
where: {
accountId,
...(subscriptionIds ? { id: { in: subscriptionIds } } : undefined),
},
});
}
async listAllWithAccountForSync() {
return await this.db.calendarSubscription.findMany({
where: { enabled: true },
include: { account: true },
});
}
async listByAccountForSync(accountId: string) {
return await this.db.calendarSubscription.findMany({
where: { accountId, enabled: true },
include: { account: true },
});
}
async updateLastSyncAt(id: string, lastSyncAt: Date) {
return await this.db.calendarSubscription.update({
where: { id },
data: { lastSyncAt },
});
}
async clearSyncTokensByAccount(accountId: string) {
return await this.db.calendarSubscription.updateMany({
where: { accountId },
data: { syncToken: null },
});
}
async updateManyStatus(
ids: string[],
data: Partial<Pick<CalendarSubscription, 'enabled'>>
) {
return await this.db.calendarSubscription.updateMany({
where: { id: { in: ids } },
data,
});
}
@Transactional()
async disableAndPurge(subscriptionId: string) {
await this.db.calendarSubscription.update({
where: { id: subscriptionId },
data: {
enabled: false,
syncToken: null,
customChannelId: null,
customResourceId: null,
channelExpiration: null,
},
});
await this.models.calendarEvent.deleteBySubscriptionIds([subscriptionId]);
}
}