Files
AFFiNE-Mirror/packages/backend/server/src/plugins/calendar/resolver.ts
T
DarkSky c53457691d feat(server): entitlement based model (#14996)
#### PR Dependency Tree


* **PR #14996** 👈

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

* **New Features**
  * Admin mutations to grant/revoke commercial entitlements.
  * New Doc comment-update permission.
  * Realtime user/workspace quota-state endpoints and live-update rooms.

* **Bug Fixes**
  * More accurate readable-doc filtering and permission evaluation.

* **Refactor**
* Workspace feature management moved to entitlement-based model;
permission and quota pipelines redesigned.
  * Admin workspace UI now edits flags only (feature toggles removed).

* **Tests**
* Extensive new and updated tests for permissions, entitlements, quota,
projection, and backfills.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/toeverything/AFFiNE/pull/14996?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-19 22:48:05 +08:00

248 lines
6.8 KiB
TypeScript

import {
Args,
GraphQLISODateTime,
Int,
Mutation,
Parent,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { ActionForbidden, AuthenticationRequired, Config } from '../../base';
import { CurrentUser } from '../../core/auth';
import { ServerConfigType } from '../../core/config/types';
import { PermissionAccess } from '../../core/permission';
import { UserType } from '../../core/user';
import { WorkspaceType } from '../../core/workspaces';
import { Models } from '../../models';
import { CalendarOAuthService } from './oauth';
import { CalendarProviderFactory, CalendarProviderName } from './providers';
import { CalendarService } from './service';
import {
CalendarAccountObjectType,
CalendarCalDAVProviderPresetObjectType,
CalendarEventObjectType,
CalendarSubscriptionObjectType,
LinkCalDAVAccountInput,
LinkCalendarAccountInput,
UpdateWorkspaceCalendarsInput,
WorkspaceCalendarObjectType,
} from './types';
@Resolver(() => ServerConfigType)
export class CalendarServerConfigResolver {
constructor(
private readonly providerFactory: CalendarProviderFactory,
private readonly config: Config,
private readonly calendar: CalendarService
) {}
@ResolveField(() => [CalendarProviderName])
async calendarProviders(@CurrentUser() user?: CurrentUser) {
const providers = [];
for (const provider of this.providerFactory.providers) {
if (!(await this.calendar.canLinkProvider(user?.id, provider))) {
continue;
}
providers.push(provider);
}
return providers;
}
@ResolveField(() => [CalendarCalDAVProviderPresetObjectType])
calendarCalDAVProviders() {
const caldavConfig = this.config.calendar.caldav;
if (!caldavConfig?.enabled) {
return [];
}
return caldavConfig.providers.map(provider => ({
id: provider.id,
label: provider.label,
requiresAppPassword: provider.requiresAppPassword ?? null,
docsUrl: provider.docsUrl ?? null,
}));
}
}
@Resolver(() => UserType)
export class UserCalendarResolver {
constructor(private readonly calendar: CalendarService) {}
@ResolveField(() => [CalendarAccountObjectType])
async calendarAccounts(
@CurrentUser() currentUser: CurrentUser,
@Parent() user: UserType
) {
if (!currentUser || currentUser.id !== user.id) {
throw new ActionForbidden();
}
return await this.calendar.listAccounts(user.id);
}
}
@Resolver(() => CalendarAccountObjectType)
export class CalendarAccountResolver {
constructor(private readonly calendar: CalendarService) {}
@ResolveField(() => Int)
async calendarsCount(
@CurrentUser() user: CurrentUser,
@Parent() account: CalendarAccountObjectType
) {
if (typeof account.calendarsCount === 'number') {
return account.calendarsCount;
}
const calendars = await this.calendar.listAccountCalendars(
user.id,
account.id
);
return calendars.length;
}
@ResolveField(() => [CalendarSubscriptionObjectType])
async calendars(
@CurrentUser() user: CurrentUser,
@Parent() account: CalendarAccountObjectType
) {
return await this.calendar.listAccountCalendars(user.id, account.id);
}
}
@Resolver(() => WorkspaceType)
export class WorkspaceCalendarResolver {
constructor(
private readonly calendar: CalendarService,
private readonly access: PermissionAccess
) {}
@ResolveField(() => [WorkspaceCalendarObjectType])
async calendars(
@CurrentUser() user: CurrentUser,
@Parent() workspace: WorkspaceType
) {
await this.access
.user(user.id)
.workspace(workspace.id)
.assert('Workspace.Settings.Read');
return await this.calendar.getWorkspaceCalendars(workspace.id);
}
}
@Resolver(() => WorkspaceCalendarObjectType)
export class WorkspaceCalendarEventsResolver {
constructor(
private readonly calendar: CalendarService,
private readonly access: PermissionAccess
) {}
@ResolveField(() => [CalendarEventObjectType])
async events(
@CurrentUser() user: CurrentUser,
@Parent() calendar: WorkspaceCalendarObjectType,
@Args({ name: 'from', type: () => GraphQLISODateTime }) from: Date,
@Args({ name: 'to', type: () => GraphQLISODateTime }) to: Date
) {
await this.access
.user(user.id)
.workspace(calendar.workspaceId)
.assert('Workspace.Settings.Read');
return await this.calendar.listWorkspaceEvents({
workspaceCalendarId: calendar.id,
from,
to,
});
}
}
@Resolver(() => CalendarAccountObjectType)
export class CalendarMutationResolver {
constructor(
private readonly calendar: CalendarService,
private readonly oauth: CalendarOAuthService,
private readonly models: Models,
private readonly access: PermissionAccess
) {}
@Mutation(() => String)
async linkCalendarAccount(
@CurrentUser() user: CurrentUser | null,
@Args('input') input: LinkCalendarAccountInput
) {
if (!user) {
throw new AuthenticationRequired();
}
await this.calendar.assertCanLinkProvider(user.id, input.provider);
const state = await this.oauth.saveOAuthState({
provider: input.provider,
userId: user.id,
redirectUri: input.redirectUri ?? undefined,
});
const callbackUrl = this.calendar.getCallbackUrl();
return this.calendar.getAuthUrl(input.provider, state, callbackUrl);
}
@Mutation(() => CalendarAccountObjectType)
async linkCalDAVAccount(
@CurrentUser() user: CurrentUser | null,
@Args('input') input: LinkCalDAVAccountInput
) {
if (!user) {
throw new AuthenticationRequired();
}
return await this.calendar.linkCalDAVAccount({
userId: user.id,
input,
});
}
@Mutation(() => CalendarAccountObjectType, { nullable: true })
async updateCalendarAccount(
@CurrentUser() user: CurrentUser,
@Args('accountId') accountId: string,
@Args('refreshIntervalMinutes') refreshIntervalMinutes: number
) {
return await this.calendar.updateAccountRefreshInterval(
user.id,
accountId,
refreshIntervalMinutes
);
}
@Mutation(() => Boolean)
async unlinkCalendarAccount(
@CurrentUser() user: CurrentUser,
@Args('accountId') accountId: string
) {
return await this.calendar.unlinkAccount(user.id, accountId);
}
@Mutation(() => WorkspaceCalendarObjectType)
async updateWorkspaceCalendars(
@CurrentUser() user: CurrentUser,
@Args('input') input: UpdateWorkspaceCalendarsInput
) {
await this.access
.user(user.id)
.workspace(input.workspaceId)
.assert('Workspace.Settings.Update');
const calendar = await this.calendar.updateWorkspaceCalendars({
workspaceId: input.workspaceId,
userId: user.id,
items: input.items,
});
const items = await this.models.workspaceCalendar.listItems(calendar.id);
return {
...calendar,
items,
};
}
}