mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-06 00:41:39 +08:00
feat(core): integrate realtime features (#15003)
This commit is contained in:
@@ -5,7 +5,6 @@ import {
|
|||||||
listNotificationsQuery,
|
listNotificationsQuery,
|
||||||
MentionNotificationBodyType,
|
MentionNotificationBodyType,
|
||||||
mentionUserMutation,
|
mentionUserMutation,
|
||||||
notificationCountQuery,
|
|
||||||
NotificationObjectType,
|
NotificationObjectType,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
readAllNotificationsMutation,
|
readAllNotificationsMutation,
|
||||||
@@ -13,6 +12,7 @@ import {
|
|||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
|
|
||||||
import { Mockers } from '../../mocks';
|
import { Mockers } from '../../mocks';
|
||||||
|
import { createRealtimeClient, realtimeRequest } from '../realtime';
|
||||||
import { app, e2e } from '../test';
|
import { app, e2e } from '../test';
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
@@ -270,10 +270,10 @@ e2e('should mark notification as read', async t => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const count = await app.gql({
|
const socket = await createRealtimeClient(app, member);
|
||||||
query: notificationCountQuery,
|
t.teardown(() => socket.disconnect());
|
||||||
});
|
const count = await realtimeRequest(socket, 'notification.count.get', {});
|
||||||
t.is(count.currentUser!.notificationCount, 0);
|
t.is(count.count, 0);
|
||||||
|
|
||||||
// read again should work
|
// read again should work
|
||||||
for (const notification of notifications) {
|
for (const notification of notifications) {
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import type {
|
||||||
|
RealtimeAck,
|
||||||
|
RealtimeRequestInputOf,
|
||||||
|
RealtimeRequestName,
|
||||||
|
RealtimeRequestOutputOf,
|
||||||
|
} from '@affine/realtime';
|
||||||
|
import { io, type Socket as SocketIOClient } from 'socket.io-client';
|
||||||
|
import type { Response } from 'supertest';
|
||||||
|
|
||||||
|
import type { MockedUser } from '../mocks';
|
||||||
|
import type { TestingApp } from './create-app';
|
||||||
|
|
||||||
|
const REALTIME_CLIENT_VERSION = '0.26.0';
|
||||||
|
const WS_TIMEOUT_MS = 5_000;
|
||||||
|
|
||||||
|
function cookieHeader(res: Response) {
|
||||||
|
return (res.get('Set-Cookie') ?? [])
|
||||||
|
.map(cookie => cookie.split(';')[0])
|
||||||
|
.join('; ');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withTimeout<T>(
|
||||||
|
promise: Promise<T>,
|
||||||
|
timeoutMs: number,
|
||||||
|
label: string
|
||||||
|
) {
|
||||||
|
let timer: NodeJS.Timeout | undefined;
|
||||||
|
const timeout = new Promise<never>((_, reject) => {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
reject(new Error(`Timeout (${timeoutMs}ms): ${label}`));
|
||||||
|
}, timeoutMs);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await Promise.race([promise, timeout]);
|
||||||
|
} finally {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForConnect(socket: SocketIOClient) {
|
||||||
|
if (socket.connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await withTimeout(
|
||||||
|
new Promise<void>((resolve, reject) => {
|
||||||
|
socket.once('connect', resolve);
|
||||||
|
socket.once('connect_error', reject);
|
||||||
|
}),
|
||||||
|
WS_TIMEOUT_MS,
|
||||||
|
'realtime socket connect'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRealtimeClient(app: TestingApp, user: MockedUser) {
|
||||||
|
const login = await app.login(user);
|
||||||
|
const socket = io(app.url, {
|
||||||
|
transports: ['websocket'],
|
||||||
|
reconnection: false,
|
||||||
|
forceNew: true,
|
||||||
|
extraHeaders: {
|
||||||
|
cookie: cookieHeader(login),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await waitForConnect(socket);
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function realtimeRequest<Op extends RealtimeRequestName>(
|
||||||
|
socket: SocketIOClient,
|
||||||
|
op: Op,
|
||||||
|
input: RealtimeRequestInputOf<Op>
|
||||||
|
): Promise<RealtimeRequestOutputOf<Op>> {
|
||||||
|
const ack = await withTimeout(
|
||||||
|
new Promise<RealtimeAck<RealtimeRequestOutputOf<Op>>>(resolve => {
|
||||||
|
socket.emit(
|
||||||
|
'realtime:request',
|
||||||
|
{ op, input, clientVersion: REALTIME_CLIENT_VERSION },
|
||||||
|
(res: RealtimeAck<RealtimeRequestOutputOf<Op>>) => resolve(res)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
WS_TIMEOUT_MS,
|
||||||
|
`realtime request ${op}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if ('error' in ack) {
|
||||||
|
throw new Error(`${ack.error.name}: ${ack.error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ack.data;
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
approveWorkspaceTeamMemberMutation,
|
approveWorkspaceTeamMemberMutation,
|
||||||
createInviteLinkMutation,
|
createInviteLinkMutation,
|
||||||
getInviteInfoQuery,
|
getInviteInfoQuery,
|
||||||
getMembersByWorkspaceIdQuery,
|
|
||||||
inviteByEmailsMutation,
|
inviteByEmailsMutation,
|
||||||
leaveWorkspaceMutation,
|
leaveWorkspaceMutation,
|
||||||
revokeMemberPermissionMutation,
|
revokeMemberPermissionMutation,
|
||||||
@@ -13,16 +12,21 @@ import {
|
|||||||
WorkspaceMemberStatus,
|
WorkspaceMemberStatus,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
|
import {
|
||||||
|
WorkspaceMemberSource,
|
||||||
|
WorkspaceMemberStatus as PrismaWorkspaceMemberStatus,
|
||||||
|
} from '@prisma/client';
|
||||||
|
|
||||||
import { EntitlementService } from '../../../core/entitlement';
|
import { EntitlementService } from '../../../core/entitlement';
|
||||||
import { WorkspacePolicyService } from '../../../core/permission';
|
import { WorkspacePolicyService } from '../../../core/permission';
|
||||||
import { Models } from '../../../models';
|
import { Models, WorkspaceRole as ModelWorkspaceRole } from '../../../models';
|
||||||
import {
|
import {
|
||||||
SubscriptionPlan,
|
SubscriptionPlan,
|
||||||
SubscriptionRecurring,
|
SubscriptionRecurring,
|
||||||
SubscriptionStatus,
|
SubscriptionStatus,
|
||||||
} from '../../../plugins/payment/types';
|
} from '../../../plugins/payment/types';
|
||||||
import { Mockers } from '../../mocks';
|
import { Mockers } from '../../mocks';
|
||||||
|
import { createRealtimeClient, realtimeRequest } from '../realtime';
|
||||||
import { app, e2e } from '../test';
|
import { app, e2e } from '../test';
|
||||||
|
|
||||||
const TWO_BILLION_BYTES = 2_000_000_000;
|
const TWO_BILLION_BYTES = 2_000_000_000;
|
||||||
@@ -380,39 +384,31 @@ e2e('should support pagination for member', async t => {
|
|||||||
userId: u2.id,
|
userId: u2.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.login(owner);
|
const socket = await createRealtimeClient(app, owner);
|
||||||
let result = await app.gql({
|
t.teardown(() => socket.disconnect());
|
||||||
query: getMembersByWorkspaceIdQuery,
|
let result = await realtimeRequest(socket, 'workspace.members.get', {
|
||||||
variables: {
|
workspaceId: workspace.id,
|
||||||
workspaceId: workspace.id,
|
skip: 0,
|
||||||
skip: 0,
|
take: 2,
|
||||||
take: 2,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
t.is(result.workspace.memberCount, 3);
|
t.is(result.memberCount, 3);
|
||||||
t.is(result.workspace.members.length, 2);
|
t.is(result.members.length, 2);
|
||||||
|
|
||||||
result = await app.gql({
|
result = await realtimeRequest(socket, 'workspace.members.get', {
|
||||||
query: getMembersByWorkspaceIdQuery,
|
workspaceId: workspace.id,
|
||||||
variables: {
|
skip: 2,
|
||||||
workspaceId: workspace.id,
|
take: 2,
|
||||||
skip: 2,
|
|
||||||
take: 2,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
t.is(result.workspace.memberCount, 3);
|
t.is(result.memberCount, 3);
|
||||||
t.is(result.workspace.members.length, 1);
|
t.is(result.members.length, 1);
|
||||||
|
|
||||||
result = await app.gql({
|
result = await realtimeRequest(socket, 'workspace.members.get', {
|
||||||
query: getMembersByWorkspaceIdQuery,
|
workspaceId: workspace.id,
|
||||||
variables: {
|
skip: 3,
|
||||||
workspaceId: workspace.id,
|
take: 2,
|
||||||
skip: 3,
|
|
||||||
take: 2,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
t.is(result.workspace.memberCount, 3);
|
t.is(result.memberCount, 3);
|
||||||
t.is(result.workspace.members.length, 0);
|
t.is(result.members.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
e2e('should limit member count correctly', async t => {
|
e2e('should limit member count correctly', async t => {
|
||||||
@@ -428,17 +424,15 @@ e2e('should limit member count correctly', async t => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
await app.login(owner);
|
const socket = await createRealtimeClient(app, owner);
|
||||||
const result = await app.gql({
|
t.teardown(() => socket.disconnect());
|
||||||
query: getMembersByWorkspaceIdQuery,
|
const result = await realtimeRequest(socket, 'workspace.members.get', {
|
||||||
variables: {
|
workspaceId: workspace.id,
|
||||||
workspaceId: workspace.id,
|
skip: 0,
|
||||||
skip: 0,
|
take: 10,
|
||||||
take: 10,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
t.is(result.workspace.memberCount, 11);
|
t.is(result.memberCount, 11);
|
||||||
t.is(result.workspace.members.length, 10);
|
t.is(result.members.length, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
e2e('should get invite link info with status', async t => {
|
e2e('should get invite link info with status', async t => {
|
||||||
@@ -634,38 +628,54 @@ e2e(
|
|||||||
userId: user2.id,
|
userId: user2.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.login(owner);
|
const socket = await createRealtimeClient(app, owner);
|
||||||
let result = await app.gql({
|
t.teardown(() => socket.disconnect());
|
||||||
query: getMembersByWorkspaceIdQuery,
|
let result = await realtimeRequest(socket, 'workspace.members.get', {
|
||||||
variables: {
|
workspaceId: workspace.id,
|
||||||
workspaceId: workspace.id,
|
query: 'lucy',
|
||||||
query: 'lucy',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
t.is(result.workspace.memberCount, 3);
|
t.is(result.memberCount, 3);
|
||||||
t.is(result.workspace.members.length, 1);
|
t.is(result.members.length, 1);
|
||||||
t.is(result.workspace.members[0].name, user1.name);
|
t.is(result.members[0].name, user1.name);
|
||||||
|
|
||||||
result = await app.gql({
|
result = await realtimeRequest(socket, 'workspace.members.get', {
|
||||||
query: getMembersByWorkspaceIdQuery,
|
workspaceId: workspace.id,
|
||||||
variables: {
|
query: 'LUCY',
|
||||||
workspaceId: workspace.id,
|
|
||||||
query: 'LUCY',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
t.is(result.workspace.memberCount, 3);
|
t.is(result.memberCount, 3);
|
||||||
t.is(result.workspace.members.length, 1);
|
t.is(result.members.length, 1);
|
||||||
t.is(result.workspace.members[0].name, user1.name);
|
t.is(result.members[0].name, user1.name);
|
||||||
|
|
||||||
result = await app.gql({
|
result = await realtimeRequest(socket, 'workspace.members.get', {
|
||||||
query: getMembersByWorkspaceIdQuery,
|
workspaceId: workspace.id,
|
||||||
variables: {
|
query: 'jeanne_doe',
|
||||||
workspaceId: workspace.id,
|
|
||||||
query: 'jeanne_doe',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
t.is(result.workspace.memberCount, 3);
|
t.is(result.memberCount, 3);
|
||||||
t.is(result.workspace.members.length, 1);
|
t.is(result.members.length, 1);
|
||||||
t.is(result.workspace.members[0].email, user2.email);
|
t.is(result.members[0].email, user2.email);
|
||||||
|
|
||||||
|
const pendingEmail = `pending_search.${randomUUID()}@affine.pro`;
|
||||||
|
const pendingUser = await app.create(Mockers.User, {
|
||||||
|
email: pendingEmail,
|
||||||
|
});
|
||||||
|
await app
|
||||||
|
.get(Models)
|
||||||
|
.workspaceUser.set(
|
||||||
|
workspace.id,
|
||||||
|
pendingUser.id,
|
||||||
|
ModelWorkspaceRole.Collaborator,
|
||||||
|
{
|
||||||
|
status: PrismaWorkspaceMemberStatus.Pending,
|
||||||
|
source: WorkspaceMemberSource.Email,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
result = await realtimeRequest(socket, 'workspace.members.get', {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
query: 'pending_search',
|
||||||
|
});
|
||||||
|
t.is(result.memberCount, 4);
|
||||||
|
t.is(result.members.length, 1);
|
||||||
|
t.is(result.members[0].email, pendingEmail);
|
||||||
|
t.is(result.members[0].status, WorkspaceMemberStatus.Pending);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
+1
-1
@@ -10,5 +10,5 @@ import { CacheInterceptor } from './interceptor';
|
|||||||
})
|
})
|
||||||
export class CacheModule {}
|
export class CacheModule {}
|
||||||
export { Cache, SessionCache };
|
export { Cache, SessionCache };
|
||||||
|
|
||||||
export { CacheInterceptor, MakeCache, PreventCache } from './interceptor';
|
export { CacheInterceptor, MakeCache, PreventCache } from './interceptor';
|
||||||
|
export { isValidCacheTtl } from './provider';
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ export interface CacheSetOptions {
|
|||||||
ttl?: number;
|
ttl?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isValidCacheTtl(ttl: unknown): ttl is number {
|
||||||
|
return typeof ttl === 'number' && Number.isSafeInteger(ttl) && ttl > 0;
|
||||||
|
}
|
||||||
|
|
||||||
export class CacheProvider {
|
export class CacheProvider {
|
||||||
constructor(private readonly redis: Redis) {}
|
constructor(private readonly redis: Redis) {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export {
|
export {
|
||||||
Cache,
|
Cache,
|
||||||
CacheInterceptor,
|
CacheInterceptor,
|
||||||
|
isValidCacheTtl,
|
||||||
MakeCache,
|
MakeCache,
|
||||||
PreventCache,
|
PreventCache,
|
||||||
SessionCache,
|
SessionCache,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
Resolver,
|
Resolver,
|
||||||
} from '@nestjs/graphql';
|
} from '@nestjs/graphql';
|
||||||
|
|
||||||
import { ActionForbidden } from '../../base';
|
import { ActionForbidden, EventBus } from '../../base';
|
||||||
import { Models } from '../../models';
|
import { Models } from '../../models';
|
||||||
import { CurrentUser } from '../auth/session';
|
import { CurrentUser } from '../auth/session';
|
||||||
import { UserType } from '../user';
|
import { UserType } from '../user';
|
||||||
@@ -26,7 +26,10 @@ class GenerateAccessTokenInput {
|
|||||||
|
|
||||||
@Resolver(() => AccessToken)
|
@Resolver(() => AccessToken)
|
||||||
export class AccessTokenResolver {
|
export class AccessTokenResolver {
|
||||||
constructor(private readonly models: Models) {}
|
constructor(
|
||||||
|
private readonly models: Models,
|
||||||
|
private readonly event: EventBus
|
||||||
|
) {}
|
||||||
|
|
||||||
@Query(() => [RevealedAccessToken], {
|
@Query(() => [RevealedAccessToken], {
|
||||||
deprecationReason: 'use currentUser.revealedAccessTokens',
|
deprecationReason: 'use currentUser.revealedAccessTokens',
|
||||||
@@ -42,11 +45,13 @@ export class AccessTokenResolver {
|
|||||||
@CurrentUser() user: CurrentUser,
|
@CurrentUser() user: CurrentUser,
|
||||||
@Args('input') input: GenerateAccessTokenInput
|
@Args('input') input: GenerateAccessTokenInput
|
||||||
): Promise<RevealedAccessToken> {
|
): Promise<RevealedAccessToken> {
|
||||||
return await this.models.accessToken.create({
|
const token = await this.models.accessToken.create({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
expiresAt: input.expiresAt,
|
expiresAt: input.expiresAt,
|
||||||
});
|
});
|
||||||
|
this.event.emit('user.access_token.created', { userId: user.id });
|
||||||
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
@@ -55,6 +60,7 @@ export class AccessTokenResolver {
|
|||||||
@Args('id') id: string
|
@Args('id') id: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
await this.models.accessToken.revoke(id, user.id);
|
await this.models.accessToken.revoke(id, user.id);
|
||||||
|
this.event.emit('user.access_token.revoked', { userId: user.id });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from '@nestjs/graphql';
|
} from '@nestjs/graphql';
|
||||||
import { difference } from 'lodash-es';
|
import { difference } from 'lodash-es';
|
||||||
|
|
||||||
import { BadRequest } from '../../base';
|
import { BadRequest, EventBus } from '../../base';
|
||||||
import { Feature, Models, type UserFeatureName } from '../../models';
|
import { Feature, Models, type UserFeatureName } from '../../models';
|
||||||
import { Admin } from '../common';
|
import { Admin } from '../common';
|
||||||
import { EntitlementService } from '../entitlement';
|
import { EntitlementService } from '../entitlement';
|
||||||
@@ -42,7 +42,8 @@ export class UserFeatureResolver extends AvailableUserFeatureConfig {
|
|||||||
export class AdminFeatureManagementResolver extends AvailableUserFeatureConfig {
|
export class AdminFeatureManagementResolver extends AvailableUserFeatureConfig {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly models: Models,
|
private readonly models: Models,
|
||||||
private readonly entitlement: EntitlementService
|
private readonly entitlement: EntitlementService,
|
||||||
|
private readonly event: EventBus
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -76,6 +77,11 @@ export class AdminFeatureManagementResolver extends AvailableUserFeatureConfig {
|
|||||||
removed.map(feature => this.models.userFeature.remove(id, feature))
|
removed.map(feature => this.models.userFeature.remove(id, feature))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const user = await this.models.user.get(id);
|
||||||
|
if (user) {
|
||||||
|
this.event.emit('user.updated', user);
|
||||||
|
}
|
||||||
|
|
||||||
return features;
|
return features;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export {
|
|||||||
PERMISSION_SHADOW_MISMATCH_CATEGORIES,
|
PERMISSION_SHADOW_MISMATCH_CATEGORIES,
|
||||||
PermissionDiagnosticService,
|
PermissionDiagnosticService,
|
||||||
} from './diagnostic';
|
} from './diagnostic';
|
||||||
|
export {
|
||||||
|
type DotToUnderline,
|
||||||
|
mapPermissionsToGraphqlPermissions,
|
||||||
|
} from './permission-map';
|
||||||
export { WorkspacePolicyService } from './policy';
|
export { WorkspacePolicyService } from './policy';
|
||||||
export { PermissionProjectionChecker } from './projection-checker';
|
export { PermissionProjectionChecker } from './projection-checker';
|
||||||
export { PermissionService } from './service';
|
export { PermissionService } from './service';
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export type DotToUnderline<T extends string> =
|
||||||
|
T extends `${infer Prefix}.${infer Suffix}`
|
||||||
|
? `${Prefix}_${DotToUnderline<Suffix>}`
|
||||||
|
: T;
|
||||||
|
|
||||||
|
export function mapPermissionsToGraphqlPermissions<A extends string>(
|
||||||
|
permission: Record<A, boolean>
|
||||||
|
): Record<DotToUnderline<A>, boolean> {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(permission).map(([key, value]) => [
|
||||||
|
key.replaceAll('.', '_'),
|
||||||
|
value,
|
||||||
|
])
|
||||||
|
) as Record<DotToUnderline<A>, boolean>;
|
||||||
|
}
|
||||||
@@ -5,19 +5,43 @@ import {
|
|||||||
import test from 'ava';
|
import test from 'ava';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { PublicDocMode } from '../../../models';
|
||||||
import type { CopilotTranscriptionReader } from '../../../plugins/copilot/transcript';
|
import type { CopilotTranscriptionReader } from '../../../plugins/copilot/transcript';
|
||||||
import { CopilotTranscriptRealtimeProvider } from '../../../plugins/copilot/transcript';
|
import { CopilotTranscriptRealtimeProvider } from '../../../plugins/copilot/transcript';
|
||||||
import type { CurrentUser } from '../../auth';
|
import type { CurrentUser } from '../../auth';
|
||||||
import { CommentRealtimeProvider } from '../../comment/realtime';
|
import { CommentRealtimeProvider } from '../../comment/realtime';
|
||||||
import { NotificationRealtimeProvider } from '../../notification/realtime';
|
import { NotificationRealtimeProvider } from '../../notification/realtime';
|
||||||
import type { PermissionAccess } from '../../permission';
|
import {
|
||||||
|
DocRole,
|
||||||
|
type PermissionAccess,
|
||||||
|
WorkspaceRole,
|
||||||
|
} from '../../permission';
|
||||||
import { QuotaStateRealtimeProvider } from '../../quota/realtime';
|
import { QuotaStateRealtimeProvider } from '../../quota/realtime';
|
||||||
|
import { UserRealtimeProvider } from '../../user/realtime';
|
||||||
|
import {
|
||||||
|
DocGrantsRealtimeProvider,
|
||||||
|
DocShareRealtimeProvider,
|
||||||
|
} from '../../workspaces/doc-realtime';
|
||||||
|
import {
|
||||||
|
WorkspaceAccessRealtimeProvider,
|
||||||
|
WorkspaceConfigRealtimeProvider,
|
||||||
|
WorkspaceMembersRealtimeProvider,
|
||||||
|
} from '../../workspaces/realtime';
|
||||||
import { RealtimeGateway } from '../gateway';
|
import { RealtimeGateway } from '../gateway';
|
||||||
import {
|
import {
|
||||||
realtimeCommentRoom,
|
realtimeCommentRoom,
|
||||||
|
realtimeDocGrantsRoom,
|
||||||
|
realtimeDocShareStateRoom,
|
||||||
realtimeNotificationRoom,
|
realtimeNotificationRoom,
|
||||||
realtimeTranscriptTaskRoom,
|
realtimeTranscriptTaskRoom,
|
||||||
|
realtimeUserAccessTokensRoom,
|
||||||
|
realtimeUserProfileRoom,
|
||||||
|
realtimeUserSettingsRoom,
|
||||||
|
realtimeWorkspaceAccessRoom,
|
||||||
|
realtimeWorkspaceConfigRoom,
|
||||||
realtimeWorkspaceEmbeddingProgressRoom,
|
realtimeWorkspaceEmbeddingProgressRoom,
|
||||||
|
realtimeWorkspaceInviteLinkRoom,
|
||||||
|
realtimeWorkspaceMembersRoom,
|
||||||
realtimeWorkspaceQuotaStateRoom,
|
realtimeWorkspaceQuotaStateRoom,
|
||||||
registerRealtimeLiveQuery,
|
registerRealtimeLiveQuery,
|
||||||
} from '../index';
|
} from '../index';
|
||||||
@@ -166,6 +190,18 @@ test('room helpers produce stable realtime room names', t => {
|
|||||||
realtimeWorkspaceEmbeddingProgressRoom('space'),
|
realtimeWorkspaceEmbeddingProgressRoom('space'),
|
||||||
'workspace:space:embedding-progress'
|
'workspace:space:embedding-progress'
|
||||||
);
|
);
|
||||||
|
t.is(realtimeWorkspaceAccessRoom('space'), 'workspace:space:access');
|
||||||
|
t.is(realtimeWorkspaceConfigRoom('space'), 'workspace:space:config');
|
||||||
|
t.is(realtimeWorkspaceMembersRoom('space'), 'workspace:space:members');
|
||||||
|
t.is(realtimeWorkspaceInviteLinkRoom('space'), 'workspace:space:invite-link');
|
||||||
|
t.is(
|
||||||
|
realtimeDocShareStateRoom('space', 'doc'),
|
||||||
|
'workspace:space:doc:doc:share-state'
|
||||||
|
);
|
||||||
|
t.is(realtimeDocGrantsRoom('space', 'doc'), 'workspace:space:doc:doc:grants');
|
||||||
|
t.is(realtimeUserProfileRoom('u1'), 'user:u1:profile');
|
||||||
|
t.is(realtimeUserSettingsRoom('u1'), 'user:u1:settings');
|
||||||
|
t.is(realtimeUserAccessTokensRoom('u1'), 'user:u1:access-tokens');
|
||||||
t.is(
|
t.is(
|
||||||
realtimeTranscriptTaskRoom('space', 'task'),
|
realtimeTranscriptTaskRoom('space', 'task'),
|
||||||
'copilot:transcript:space:task'
|
'copilot:transcript:space:task'
|
||||||
@@ -225,6 +261,484 @@ test('realtime providers expose runtime injection metadata for registry dependen
|
|||||||
QuotaStateRealtimeProvider
|
QuotaStateRealtimeProvider
|
||||||
).includes(RealtimeRegistry)
|
).includes(RealtimeRegistry)
|
||||||
);
|
);
|
||||||
|
t.true(
|
||||||
|
Reflect.getMetadata(
|
||||||
|
'design:paramtypes',
|
||||||
|
WorkspaceAccessRealtimeProvider
|
||||||
|
).includes(RealtimeRegistry)
|
||||||
|
);
|
||||||
|
t.true(
|
||||||
|
Reflect.getMetadata(
|
||||||
|
'design:paramtypes',
|
||||||
|
WorkspaceConfigRealtimeProvider
|
||||||
|
).includes(RealtimeRegistry)
|
||||||
|
);
|
||||||
|
t.true(
|
||||||
|
Reflect.getMetadata(
|
||||||
|
'design:paramtypes',
|
||||||
|
WorkspaceMembersRealtimeProvider
|
||||||
|
).includes(RealtimeRegistry)
|
||||||
|
);
|
||||||
|
t.true(
|
||||||
|
Reflect.getMetadata('design:paramtypes', DocShareRealtimeProvider).includes(
|
||||||
|
RealtimeRegistry
|
||||||
|
)
|
||||||
|
);
|
||||||
|
t.true(
|
||||||
|
Reflect.getMetadata(
|
||||||
|
'design:paramtypes',
|
||||||
|
DocGrantsRealtimeProvider
|
||||||
|
).includes(RealtimeRegistry)
|
||||||
|
);
|
||||||
|
t.true(
|
||||||
|
Reflect.getMetadata('design:paramtypes', UserRealtimeProvider).includes(
|
||||||
|
RealtimeRegistry
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('workspace realtime providers register access, config, members and invite link handlers', async t => {
|
||||||
|
const registry = new RealtimeRegistry();
|
||||||
|
const assertions: unknown[] = [];
|
||||||
|
const ac = {
|
||||||
|
user(userId: string) {
|
||||||
|
return {
|
||||||
|
workspace(workspaceId: string) {
|
||||||
|
return {
|
||||||
|
async assert(action: string) {
|
||||||
|
assertions.push({ userId, workspaceId, action });
|
||||||
|
},
|
||||||
|
async permissions() {
|
||||||
|
return {
|
||||||
|
role: WorkspaceRole.Admin,
|
||||||
|
permissions: { 'Workspace.Read': true },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} as unknown as PermissionAccess;
|
||||||
|
const models = {
|
||||||
|
workspace: {
|
||||||
|
get: async () => ({
|
||||||
|
enableAi: true,
|
||||||
|
enableSharing: false,
|
||||||
|
enableUrlPreview: true,
|
||||||
|
enableDocEmbedding: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
workspaceUser: {
|
||||||
|
search: async () => [],
|
||||||
|
paginate: async () => [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 'invite',
|
||||||
|
type: WorkspaceRole.Collaborator,
|
||||||
|
status: 'Accepted',
|
||||||
|
user: {
|
||||||
|
id: 'u1',
|
||||||
|
name: 'User',
|
||||||
|
email: 'u1@affine.pro',
|
||||||
|
avatarUrl: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
1,
|
||||||
|
],
|
||||||
|
count: async () => 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const workspaceService = {
|
||||||
|
isTeamWorkspace: async () => true,
|
||||||
|
};
|
||||||
|
const cache = {
|
||||||
|
get: async () => ({ inviteId: 'invite-link' }),
|
||||||
|
ttl: async () => 10,
|
||||||
|
};
|
||||||
|
const url = {
|
||||||
|
link: (path: string) => `https://app.affine.pro${path}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
new WorkspaceAccessRealtimeProvider(
|
||||||
|
ac,
|
||||||
|
workspaceService as never,
|
||||||
|
registry
|
||||||
|
).onModuleInit();
|
||||||
|
new WorkspaceConfigRealtimeProvider(
|
||||||
|
ac,
|
||||||
|
models as never,
|
||||||
|
registry
|
||||||
|
).onModuleInit();
|
||||||
|
new WorkspaceMembersRealtimeProvider(
|
||||||
|
cache as never,
|
||||||
|
url as never,
|
||||||
|
ac,
|
||||||
|
models as never,
|
||||||
|
registry
|
||||||
|
).onModuleInit();
|
||||||
|
|
||||||
|
t.deepEqual(
|
||||||
|
await registry.getRequest('workspace.access.get').handle(user, {
|
||||||
|
workspaceId: 'space',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
access: {
|
||||||
|
role: 'Admin',
|
||||||
|
permissions: { Workspace_Read: true },
|
||||||
|
team: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
t.deepEqual(
|
||||||
|
await registry.getRequest('workspace.config.get').handle(user, {
|
||||||
|
workspaceId: 'space',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
config: {
|
||||||
|
enableAi: true,
|
||||||
|
enableSharing: false,
|
||||||
|
enableUrlPreview: true,
|
||||||
|
enableDocEmbedding: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
t.like(
|
||||||
|
await registry.getRequest('workspace.members.get').handle(user, {
|
||||||
|
workspaceId: 'space',
|
||||||
|
take: 1000,
|
||||||
|
}),
|
||||||
|
{ memberCount: 1 }
|
||||||
|
);
|
||||||
|
t.like(
|
||||||
|
await registry.getRequest('workspace.invite-link.get').handle(user, {
|
||||||
|
workspaceId: 'space',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
inviteLink: {
|
||||||
|
link: 'https://app.affine.pro/invite/invite-link',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
t.is(
|
||||||
|
registry
|
||||||
|
.getTopic('workspace.access.changed')
|
||||||
|
.room(user, { workspaceId: 'space' }),
|
||||||
|
realtimeWorkspaceAccessRoom('space')
|
||||||
|
);
|
||||||
|
t.is(
|
||||||
|
registry
|
||||||
|
.getTopic('workspace.config.changed')
|
||||||
|
.room(user, { workspaceId: 'space' }),
|
||||||
|
realtimeWorkspaceConfigRoom('space')
|
||||||
|
);
|
||||||
|
t.is(
|
||||||
|
registry
|
||||||
|
.getTopic('workspace.members.changed')
|
||||||
|
.room(user, { workspaceId: 'space' }),
|
||||||
|
realtimeWorkspaceMembersRoom('space')
|
||||||
|
);
|
||||||
|
t.is(
|
||||||
|
registry
|
||||||
|
.getTopic('workspace.invite-link.changed')
|
||||||
|
.room(user, { workspaceId: 'space' }),
|
||||||
|
realtimeWorkspaceInviteLinkRoom('space')
|
||||||
|
);
|
||||||
|
t.true(
|
||||||
|
assertions.some(
|
||||||
|
item =>
|
||||||
|
JSON.stringify(item) ===
|
||||||
|
JSON.stringify({
|
||||||
|
userId: 'u1',
|
||||||
|
workspaceId: 'space',
|
||||||
|
action: 'Workspace.Users.Read',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('doc realtime providers register share state and grants handlers', async t => {
|
||||||
|
const registry = new RealtimeRegistry();
|
||||||
|
const assertedActions: string[] = [];
|
||||||
|
const ac = {
|
||||||
|
user(userId: string) {
|
||||||
|
return {
|
||||||
|
doc(workspaceId: string, docId: string) {
|
||||||
|
return {
|
||||||
|
async assert(action: string) {
|
||||||
|
t.deepEqual(
|
||||||
|
{ userId, workspaceId, docId },
|
||||||
|
{
|
||||||
|
userId: 'u1',
|
||||||
|
workspaceId: 'space',
|
||||||
|
docId: 'doc',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assertedActions.push(action);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} as unknown as PermissionAccess;
|
||||||
|
const models = {
|
||||||
|
doc: {
|
||||||
|
getDocInfo: async () => ({
|
||||||
|
public: true,
|
||||||
|
mode: PublicDocMode.Page,
|
||||||
|
defaultRole: DocRole.Reader,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
docUser: {
|
||||||
|
findDirectGrantDocIdsByUser: async () => [],
|
||||||
|
paginate: async () => [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
userId: 'u2',
|
||||||
|
type: DocRole.Manager,
|
||||||
|
createdAt: new Date('2026-01-01T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
1,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
getWorkspaceUsers: async () => [
|
||||||
|
{
|
||||||
|
id: 'u2',
|
||||||
|
name: 'User 2',
|
||||||
|
email: 'u2@affine.pro',
|
||||||
|
avatarUrl: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const grants = {
|
||||||
|
paginateGrantedUsers: async () => ({
|
||||||
|
totalCount: 1,
|
||||||
|
pageInfo: { endCursor: null, hasNextPage: false },
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
type: DocRole.Manager,
|
||||||
|
user: {
|
||||||
|
id: 'u2',
|
||||||
|
name: 'User 2',
|
||||||
|
email: 'u2@affine.pro',
|
||||||
|
avatarUrl: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
new DocShareRealtimeProvider(ac, models as never, registry).onModuleInit();
|
||||||
|
new DocGrantsRealtimeProvider(
|
||||||
|
ac,
|
||||||
|
models as never,
|
||||||
|
grants as never,
|
||||||
|
registry
|
||||||
|
).onModuleInit();
|
||||||
|
|
||||||
|
t.deepEqual(
|
||||||
|
await registry.getRequest('doc.share-state.get').handle(user, {
|
||||||
|
workspaceId: 'space',
|
||||||
|
docId: 'doc',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
state: {
|
||||||
|
public: true,
|
||||||
|
mode: 'Page',
|
||||||
|
defaultRole: 'Reader',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
t.like(
|
||||||
|
await registry.getRequest('doc.grants.get').handle(user, {
|
||||||
|
workspaceId: 'space',
|
||||||
|
docId: 'doc',
|
||||||
|
pagination: { first: 10 },
|
||||||
|
}),
|
||||||
|
{ totalCount: 1 }
|
||||||
|
);
|
||||||
|
t.deepEqual(assertedActions, ['Doc.Read', 'Doc.Users.Read']);
|
||||||
|
t.is(
|
||||||
|
registry
|
||||||
|
.getTopic('doc.share-state.changed')
|
||||||
|
.room(user, { workspaceId: 'space', docId: 'doc' }),
|
||||||
|
realtimeDocShareStateRoom('space', 'doc')
|
||||||
|
);
|
||||||
|
t.is(
|
||||||
|
registry
|
||||||
|
.getTopic('doc.grants.changed')
|
||||||
|
.room(user, { workspaceId: 'space', docId: 'doc' }),
|
||||||
|
realtimeDocGrantsRoom('space', 'doc')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('user realtime provider snapshots private profile settings and access tokens without plaintext token', async t => {
|
||||||
|
const registry = new RealtimeRegistry();
|
||||||
|
const models = {
|
||||||
|
user: {
|
||||||
|
get: async () => ({
|
||||||
|
id: 'u1',
|
||||||
|
name: 'User',
|
||||||
|
email: 'u1@affine.pro',
|
||||||
|
avatarUrl: null,
|
||||||
|
emailVerifiedAt: new Date(0),
|
||||||
|
password: 'hash',
|
||||||
|
disabled: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
userSettings: {
|
||||||
|
get: async () => ({
|
||||||
|
receiveInvitationEmail: true,
|
||||||
|
receiveMentionEmail: false,
|
||||||
|
receiveCommentEmail: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
userFeature: {
|
||||||
|
list: async () => ['administrator'],
|
||||||
|
},
|
||||||
|
accessToken: {
|
||||||
|
list: async () => [
|
||||||
|
{
|
||||||
|
id: 'token',
|
||||||
|
name: 'Token',
|
||||||
|
createdAt: new Date('2026-01-01T00:00:00.000Z'),
|
||||||
|
expiresAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
new UserRealtimeProvider(models as never, registry).onModuleInit();
|
||||||
|
|
||||||
|
t.deepEqual(await registry.getRequest('user.profile.get').handle(user, {}), {
|
||||||
|
user: {
|
||||||
|
id: 'u1',
|
||||||
|
name: 'User',
|
||||||
|
email: 'u1@affine.pro',
|
||||||
|
emailVerified: true,
|
||||||
|
hasPassword: true,
|
||||||
|
avatarUrl: null,
|
||||||
|
features: ['Admin'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
t.deepEqual(
|
||||||
|
await registry
|
||||||
|
.getRequest('user.profile.get')
|
||||||
|
.handle(undefined as never, {}),
|
||||||
|
{ user: null }
|
||||||
|
);
|
||||||
|
t.is(
|
||||||
|
registry.getTopic('user.profile.changed').room(user, {}),
|
||||||
|
realtimeUserProfileRoom('u1')
|
||||||
|
);
|
||||||
|
t.is(
|
||||||
|
registry.getTopic('user.settings.changed').room(user, {}),
|
||||||
|
realtimeUserSettingsRoom('u1')
|
||||||
|
);
|
||||||
|
t.is(
|
||||||
|
registry.getTopic('user.access-tokens.changed').room(user, {}),
|
||||||
|
realtimeUserAccessTokensRoom('u1')
|
||||||
|
);
|
||||||
|
t.deepEqual(await registry.getRequest('user.settings.get').handle(user, {}), {
|
||||||
|
settings: {
|
||||||
|
receiveInvitationEmail: true,
|
||||||
|
receiveMentionEmail: false,
|
||||||
|
receiveCommentEmail: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
t.deepEqual(
|
||||||
|
await registry.getRequest('user.access-tokens.get').handle(user, {}),
|
||||||
|
{
|
||||||
|
tokens: [
|
||||||
|
{
|
||||||
|
id: 'token',
|
||||||
|
name: 'Token',
|
||||||
|
createdAt: '2026-01-01T00:00:00.000Z',
|
||||||
|
expiresAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('new realtime providers publish changed events from domain events', t => {
|
||||||
|
const published: unknown[][] = [];
|
||||||
|
const publisher = {
|
||||||
|
publish: (...args: unknown[]) => published.push(args),
|
||||||
|
publishChanged: (...args: unknown[]) => published.push(args),
|
||||||
|
} as unknown as RealtimePublisher;
|
||||||
|
|
||||||
|
const workspaceAccess = new WorkspaceAccessRealtimeProvider(
|
||||||
|
{} as never,
|
||||||
|
{} as never,
|
||||||
|
undefined,
|
||||||
|
publisher
|
||||||
|
);
|
||||||
|
workspaceAccess.onMembersUpdated({ workspaceId: 'space' });
|
||||||
|
|
||||||
|
const workspaceConfig = new WorkspaceConfigRealtimeProvider(
|
||||||
|
{} as never,
|
||||||
|
{} as never,
|
||||||
|
undefined,
|
||||||
|
publisher
|
||||||
|
);
|
||||||
|
workspaceConfig.onWorkspaceUpdated({ id: 'space' } as never);
|
||||||
|
|
||||||
|
const workspaceMembers = new WorkspaceMembersRealtimeProvider(
|
||||||
|
{} as never,
|
||||||
|
{} as never,
|
||||||
|
{} as never,
|
||||||
|
{} as never,
|
||||||
|
undefined,
|
||||||
|
publisher
|
||||||
|
);
|
||||||
|
workspaceMembers.onInviteLinkCreated({ workspaceId: 'space' });
|
||||||
|
|
||||||
|
const docShare = new DocShareRealtimeProvider(
|
||||||
|
{} as never,
|
||||||
|
{} as never,
|
||||||
|
undefined,
|
||||||
|
publisher
|
||||||
|
);
|
||||||
|
docShare.onPublicStateChanged({ workspaceId: 'space', docId: 'doc' });
|
||||||
|
|
||||||
|
const docGrants = new DocGrantsRealtimeProvider(
|
||||||
|
{} as never,
|
||||||
|
{} as never,
|
||||||
|
{} as never,
|
||||||
|
undefined,
|
||||||
|
publisher
|
||||||
|
);
|
||||||
|
docGrants.onOwnerChanged({
|
||||||
|
workspaceId: 'space',
|
||||||
|
docId: 'doc',
|
||||||
|
userId: 'u2',
|
||||||
|
});
|
||||||
|
|
||||||
|
const userProvider = new UserRealtimeProvider(
|
||||||
|
{} as never,
|
||||||
|
undefined,
|
||||||
|
publisher
|
||||||
|
);
|
||||||
|
userProvider.onUserAccessTokenCreated({ userId: 'u1' });
|
||||||
|
|
||||||
|
t.deepEqual(
|
||||||
|
published.map(args => args[0]),
|
||||||
|
[
|
||||||
|
'workspace.access.changed',
|
||||||
|
'workspace.config.changed',
|
||||||
|
'workspace.invite-link.changed',
|
||||||
|
'doc.share-state.changed',
|
||||||
|
'doc.grants.changed',
|
||||||
|
'user.access-tokens.changed',
|
||||||
|
]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('quota realtime provider exposes effective quota state snapshots', async t => {
|
test('quota realtime provider exposes effective quota state snapshots', async t => {
|
||||||
|
|||||||
@@ -16,12 +16,21 @@ export { RealtimePublisher } from './publisher';
|
|||||||
export { RealtimeRegistry } from './registry';
|
export { RealtimeRegistry } from './registry';
|
||||||
export {
|
export {
|
||||||
realtimeCommentRoom,
|
realtimeCommentRoom,
|
||||||
|
realtimeDocGrantsRoom,
|
||||||
|
realtimeDocShareStateRoom,
|
||||||
realtimeNotificationRoom,
|
realtimeNotificationRoom,
|
||||||
realtimeTranscriptTaskRoom,
|
realtimeTranscriptTaskRoom,
|
||||||
|
realtimeUserAccessTokensRoom,
|
||||||
|
realtimeUserProfileRoom,
|
||||||
realtimeUserQuotaStateRoom,
|
realtimeUserQuotaStateRoom,
|
||||||
realtimeUserRoom,
|
realtimeUserRoom,
|
||||||
|
realtimeUserSettingsRoom,
|
||||||
|
realtimeWorkspaceAccessRoom,
|
||||||
|
realtimeWorkspaceConfigRoom,
|
||||||
realtimeWorkspaceDocRoom,
|
realtimeWorkspaceDocRoom,
|
||||||
realtimeWorkspaceEmbeddingProgressRoom,
|
realtimeWorkspaceEmbeddingProgressRoom,
|
||||||
|
realtimeWorkspaceInviteLinkRoom,
|
||||||
|
realtimeWorkspaceMembersRoom,
|
||||||
realtimeWorkspaceQuotaStateRoom,
|
realtimeWorkspaceQuotaStateRoom,
|
||||||
realtimeWorkspaceRoom,
|
realtimeWorkspaceRoom,
|
||||||
} from './rooms';
|
} from './rooms';
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
getRealtimeInputKey,
|
getRealtimeInputKey,
|
||||||
type RealtimeEvent,
|
type RealtimeEvent,
|
||||||
|
type RealtimeTopicEventOf,
|
||||||
|
type RealtimeTopicInputOf,
|
||||||
type RealtimeTopicName,
|
type RealtimeTopicName,
|
||||||
} from '@affine/realtime';
|
} from '@affine/realtime';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
@@ -44,6 +46,20 @@ export class RealtimePublisher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publishChanged<Topic extends RealtimeTopicName>(
|
||||||
|
topic: Topic,
|
||||||
|
input: RealtimeTopicInputOf<Topic>,
|
||||||
|
reason: string,
|
||||||
|
options?: { room?: string }
|
||||||
|
) {
|
||||||
|
this.publish(
|
||||||
|
topic,
|
||||||
|
input,
|
||||||
|
{ changed: true, reason } as RealtimeTopicEventOf<Topic>,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
publishLocal(payload: RealtimePublishPayload) {
|
publishLocal(payload: RealtimePublishPayload) {
|
||||||
const handler = this.registry.getTopic(payload.topic);
|
const handler = this.registry.getTopic(payload.topic);
|
||||||
const room = payload.room ?? handler.room(null, payload.input as never);
|
const room = payload.room ?? handler.room(null, payload.input as never);
|
||||||
|
|||||||
@@ -40,3 +40,39 @@ export function realtimeUserQuotaStateRoom(userId: string) {
|
|||||||
export function realtimeWorkspaceQuotaStateRoom(workspaceId: string) {
|
export function realtimeWorkspaceQuotaStateRoom(workspaceId: string) {
|
||||||
return realtimeWorkspaceRoom(workspaceId, 'quota-state');
|
return realtimeWorkspaceRoom(workspaceId, 'quota-state');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function realtimeWorkspaceAccessRoom(workspaceId: string) {
|
||||||
|
return realtimeWorkspaceRoom(workspaceId, 'access');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function realtimeWorkspaceConfigRoom(workspaceId: string) {
|
||||||
|
return realtimeWorkspaceRoom(workspaceId, 'config');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function realtimeWorkspaceMembersRoom(workspaceId: string) {
|
||||||
|
return realtimeWorkspaceRoom(workspaceId, 'members');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function realtimeWorkspaceInviteLinkRoom(workspaceId: string) {
|
||||||
|
return realtimeWorkspaceRoom(workspaceId, 'invite-link');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function realtimeDocShareStateRoom(workspaceId: string, docId: string) {
|
||||||
|
return realtimeWorkspaceDocRoom(workspaceId, docId, 'share-state');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function realtimeDocGrantsRoom(workspaceId: string, docId: string) {
|
||||||
|
return realtimeWorkspaceDocRoom(workspaceId, docId, 'grants');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function realtimeUserProfileRoom(userId: string) {
|
||||||
|
return realtimeUserRoom(userId, 'profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function realtimeUserSettingsRoom(userId: string) {
|
||||||
|
return realtimeUserRoom(userId, 'settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function realtimeUserAccessTokensRoom(userId: string) {
|
||||||
|
return realtimeUserRoom(userId, 'access-tokens');
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { PermissionModule } from '../permission';
|
import { PermissionModule } from '../permission';
|
||||||
import { StorageModule } from '../storage';
|
import { StorageModule } from '../storage';
|
||||||
import { UserAvatarController } from './controller';
|
import { UserAvatarController } from './controller';
|
||||||
|
import { UserRealtimeProvider } from './realtime';
|
||||||
import {
|
import {
|
||||||
UserManagementResolver,
|
UserManagementResolver,
|
||||||
UserResolver,
|
UserResolver,
|
||||||
@@ -11,7 +12,12 @@ import {
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [StorageModule, PermissionModule],
|
imports: [StorageModule, PermissionModule],
|
||||||
providers: [UserResolver, UserManagementResolver, UserSettingsResolver],
|
providers: [
|
||||||
|
UserResolver,
|
||||||
|
UserManagementResolver,
|
||||||
|
UserSettingsResolver,
|
||||||
|
UserRealtimeProvider,
|
||||||
|
],
|
||||||
controllers: [UserAvatarController],
|
controllers: [UserAvatarController],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import type {
|
||||||
|
AccessTokenSnapshot,
|
||||||
|
CurrentUserProfileSnapshot,
|
||||||
|
UserSettingsSnapshot,
|
||||||
|
} from '@affine/realtime';
|
||||||
|
import { Injectable, OnModuleInit, Optional } from '@nestjs/common';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { AuthenticationRequired, OnEvent, UserNotFound } from '../../base';
|
||||||
|
import { Feature, Models } from '../../models';
|
||||||
|
import { sessionUser } from '../auth/service';
|
||||||
|
import { AvailableUserFeatureConfig } from '../features/types';
|
||||||
|
import { registerRealtimeLiveQuery } from '../realtime/provider';
|
||||||
|
import { RealtimePublisher } from '../realtime/publisher';
|
||||||
|
import { RealtimeRegistry } from '../realtime/registry';
|
||||||
|
import {
|
||||||
|
realtimeUserAccessTokensRoom,
|
||||||
|
realtimeUserProfileRoom,
|
||||||
|
realtimeUserSettingsRoom,
|
||||||
|
} from '../realtime/rooms';
|
||||||
|
|
||||||
|
const emptyInput = z.object({}).strict();
|
||||||
|
|
||||||
|
function assertAuthenticated(user?: { id: string }) {
|
||||||
|
if (!user) {
|
||||||
|
throw new AuthenticationRequired();
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserRealtimeProvider
|
||||||
|
extends AvailableUserFeatureConfig
|
||||||
|
implements OnModuleInit
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private readonly models: Models,
|
||||||
|
@Optional() private readonly registry?: RealtimeRegistry,
|
||||||
|
@Optional() private readonly publisher?: RealtimePublisher
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
if (!this.registry) return;
|
||||||
|
|
||||||
|
registerRealtimeLiveQuery(this.registry, {
|
||||||
|
request: {
|
||||||
|
name: 'user.profile.get',
|
||||||
|
input: emptyInput,
|
||||||
|
handle: async user => ({
|
||||||
|
user: user ? await this.getProfile(user.id) : null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
topic: {
|
||||||
|
name: 'user.profile.changed',
|
||||||
|
input: emptyInput,
|
||||||
|
authorize: async () => {},
|
||||||
|
room: user => {
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('Authenticated user is required');
|
||||||
|
}
|
||||||
|
return realtimeUserProfileRoom(user.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
registerRealtimeLiveQuery(this.registry, {
|
||||||
|
request: {
|
||||||
|
name: 'user.settings.get',
|
||||||
|
input: emptyInput,
|
||||||
|
handle: async user => ({
|
||||||
|
settings: await this.getSettings(assertAuthenticated(user).id),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
topic: {
|
||||||
|
name: 'user.settings.changed',
|
||||||
|
input: emptyInput,
|
||||||
|
authorize: async () => {},
|
||||||
|
room: user => {
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('Authenticated user is required');
|
||||||
|
}
|
||||||
|
return realtimeUserSettingsRoom(user.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
registerRealtimeLiveQuery(this.registry, {
|
||||||
|
request: {
|
||||||
|
name: 'user.access-tokens.get',
|
||||||
|
input: emptyInput,
|
||||||
|
handle: async user => ({
|
||||||
|
tokens: await this.getAccessTokens(assertAuthenticated(user).id),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
topic: {
|
||||||
|
name: 'user.access-tokens.changed',
|
||||||
|
input: emptyInput,
|
||||||
|
authorize: async () => {},
|
||||||
|
room: user => {
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('Authenticated user is required');
|
||||||
|
}
|
||||||
|
return realtimeUserAccessTokensRoom(user.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('user.updated', { suppressError: true })
|
||||||
|
onUserUpdated(user: Events['user.updated']) {
|
||||||
|
this.publisher?.publishChanged('user.profile.changed', {}, 'user-updated', {
|
||||||
|
room: realtimeUserProfileRoom(user.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('user.settings.updated', { suppressError: true })
|
||||||
|
onUserSettingsUpdated({ userId }: Events['user.settings.updated']) {
|
||||||
|
this.publisher?.publishChanged(
|
||||||
|
'user.settings.changed',
|
||||||
|
{},
|
||||||
|
'settings-updated',
|
||||||
|
{ room: realtimeUserSettingsRoom(userId) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('user.access_token.created', { suppressError: true })
|
||||||
|
onUserAccessTokenCreated({ userId }: Events['user.access_token.created']) {
|
||||||
|
this.publishAccessTokens(userId, 'access-token-created');
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('user.access_token.revoked', { suppressError: true })
|
||||||
|
onUserAccessTokenRevoked({ userId }: Events['user.access_token.revoked']) {
|
||||||
|
this.publishAccessTokens(userId, 'access-token-revoked');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getProfile(
|
||||||
|
userId: string
|
||||||
|
): Promise<CurrentUserProfileSnapshot> {
|
||||||
|
const user = await this.models.user.get(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new UserNotFound();
|
||||||
|
}
|
||||||
|
const current = sessionUser(user);
|
||||||
|
return {
|
||||||
|
id: current.id,
|
||||||
|
name: current.name,
|
||||||
|
email: current.email,
|
||||||
|
emailVerified: current.emailVerified,
|
||||||
|
hasPassword: current.hasPassword,
|
||||||
|
avatarUrl: current.avatarUrl ?? null,
|
||||||
|
features: (await this.models.userFeature.list(userId))
|
||||||
|
.filter(feature => this.availableUserFeatures().has(feature))
|
||||||
|
.map(feature => this.serializeFeature(feature)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private serializeFeature(feature: string) {
|
||||||
|
return (
|
||||||
|
Object.entries(Feature).find(([, value]) => value === feature)?.[0] ??
|
||||||
|
feature
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSettings(userId: string): Promise<UserSettingsSnapshot> {
|
||||||
|
return await this.models.userSettings.get(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAccessTokens(
|
||||||
|
userId: string
|
||||||
|
): Promise<AccessTokenSnapshot[]> {
|
||||||
|
const tokens = await this.models.accessToken.list(userId);
|
||||||
|
return tokens.map(token => ({
|
||||||
|
id: token.id,
|
||||||
|
name: token.name,
|
||||||
|
createdAt: token.createdAt.toISOString(),
|
||||||
|
expiresAt: token.expiresAt?.toISOString() ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private publishAccessTokens(userId: string, reason: string) {
|
||||||
|
this.publisher?.publishChanged('user.access-tokens.changed', {}, reason, {
|
||||||
|
room: realtimeUserAccessTokensRoom(userId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import { isNil, omitBy } from 'lodash-es';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CannotDeleteOwnAccount,
|
CannotDeleteOwnAccount,
|
||||||
|
EventBus,
|
||||||
type FileUpload,
|
type FileUpload,
|
||||||
ImageFormatNotSupported,
|
ImageFormatNotSupported,
|
||||||
OneMB,
|
OneMB,
|
||||||
@@ -186,7 +187,10 @@ export class UserResolver {
|
|||||||
|
|
||||||
@Resolver(() => UserType)
|
@Resolver(() => UserType)
|
||||||
export class UserSettingsResolver {
|
export class UserSettingsResolver {
|
||||||
constructor(private readonly models: Models) {}
|
constructor(
|
||||||
|
private readonly models: Models,
|
||||||
|
private readonly event: EventBus
|
||||||
|
) {}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@Mutation(() => Boolean, {
|
||||||
name: 'updateSettings',
|
name: 'updateSettings',
|
||||||
@@ -199,6 +203,7 @@ export class UserSettingsResolver {
|
|||||||
) {
|
) {
|
||||||
UserSettingsSchema.parse(input);
|
UserSettingsSchema.parse(input);
|
||||||
await this.models.userSettings.set(user.id, input);
|
await this.models.userSettings.set(user.id, input);
|
||||||
|
this.event.emit('user.settings.updated', { userId: user.id });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { paginate, PaginationInput } from '../../base';
|
||||||
|
import { Models } from '../../models';
|
||||||
|
import type { WorkspaceUserType } from '../user';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DocGrantsService {
|
||||||
|
constructor(private readonly models: Models) {}
|
||||||
|
|
||||||
|
async paginateGrantedUsers(
|
||||||
|
workspaceId: string,
|
||||||
|
docId: string,
|
||||||
|
pagination: PaginationInput
|
||||||
|
) {
|
||||||
|
const [permissions, totalCount] = await this.models.docUser.paginate(
|
||||||
|
workspaceId,
|
||||||
|
docId,
|
||||||
|
pagination
|
||||||
|
);
|
||||||
|
const workspaceUsers = await this.models.user.getWorkspaceUsers(
|
||||||
|
permissions.map(p => p.userId)
|
||||||
|
);
|
||||||
|
const workspaceUsersMap = new Map(
|
||||||
|
workspaceUsers.map(user => [user.id, user])
|
||||||
|
);
|
||||||
|
|
||||||
|
return paginate(
|
||||||
|
permissions.map(permission => {
|
||||||
|
const user = workspaceUsersMap.get(permission.userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error(`Doc grant user ${permission.userId} not found`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...permission,
|
||||||
|
user: user as WorkspaceUserType,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
'createdAt',
|
||||||
|
pagination,
|
||||||
|
totalCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import type {
|
||||||
|
DocShareStateSnapshot,
|
||||||
|
PaginatedDocGrantedUsersSnapshot,
|
||||||
|
} from '@affine/realtime';
|
||||||
|
import { Injectable, OnModuleInit, Optional } from '@nestjs/common';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { OnEvent, PaginationInput } from '../../base';
|
||||||
|
import { DocRole, Models, PublicDocMode } from '../../models';
|
||||||
|
import { PermissionAccess } from '../permission';
|
||||||
|
import { registerRealtimeLiveQuery } from '../realtime/provider';
|
||||||
|
import { RealtimePublisher } from '../realtime/publisher';
|
||||||
|
import { RealtimeRegistry } from '../realtime/registry';
|
||||||
|
import {
|
||||||
|
realtimeDocGrantsRoom,
|
||||||
|
realtimeDocShareStateRoom,
|
||||||
|
} from '../realtime/rooms';
|
||||||
|
import { DocGrantsService } from './doc-grants';
|
||||||
|
|
||||||
|
const docInput = z
|
||||||
|
.object({ workspaceId: z.string(), docId: z.string() })
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DocShareRealtimeProvider implements OnModuleInit {
|
||||||
|
constructor(
|
||||||
|
private readonly ac: PermissionAccess,
|
||||||
|
private readonly models: Models,
|
||||||
|
@Optional() private readonly registry?: RealtimeRegistry,
|
||||||
|
@Optional() private readonly publisher?: RealtimePublisher
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
if (!this.registry) return;
|
||||||
|
|
||||||
|
registerRealtimeLiveQuery(this.registry, {
|
||||||
|
request: {
|
||||||
|
name: 'doc.share-state.get',
|
||||||
|
input: docInput,
|
||||||
|
handle: async (user, input) => ({
|
||||||
|
state: await this.getShareState(
|
||||||
|
user.id,
|
||||||
|
input.workspaceId,
|
||||||
|
input.docId
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
topic: {
|
||||||
|
name: 'doc.share-state.changed',
|
||||||
|
input: docInput,
|
||||||
|
authorize: async (user, input) => {
|
||||||
|
await this.assertRead(user.id, input.workspaceId, input.docId);
|
||||||
|
},
|
||||||
|
room: (_user, input) =>
|
||||||
|
realtimeDocShareStateRoom(input.workspaceId, input.docId),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('doc.public_state.changed', { suppressError: true })
|
||||||
|
onPublicStateChanged({
|
||||||
|
workspaceId,
|
||||||
|
docId,
|
||||||
|
}: Events['doc.public_state.changed']) {
|
||||||
|
this.publish(workspaceId, docId, 'public-state-changed');
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('doc.default_role.changed', { suppressError: true })
|
||||||
|
onDefaultRoleChanged({
|
||||||
|
workspaceId,
|
||||||
|
docId,
|
||||||
|
}: Events['doc.default_role.changed']) {
|
||||||
|
this.publish(workspaceId, docId, 'default-role-changed');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getShareState(
|
||||||
|
userId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
docId: string
|
||||||
|
): Promise<DocShareStateSnapshot | null> {
|
||||||
|
await this.assertRead(userId, workspaceId, docId);
|
||||||
|
const doc = await this.models.doc.getDocInfo(workspaceId, docId);
|
||||||
|
if (!doc) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
public: doc.public,
|
||||||
|
mode: PublicDocMode[doc.mode],
|
||||||
|
defaultRole: DocRole[doc.defaultRole],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async assertRead(userId: string, workspaceId: string, docId: string) {
|
||||||
|
await this.ac.user(userId).doc(workspaceId, docId).assert('Doc.Read');
|
||||||
|
}
|
||||||
|
|
||||||
|
private publish(workspaceId: string, docId: string, reason: string) {
|
||||||
|
this.publisher?.publishChanged(
|
||||||
|
'doc.share-state.changed',
|
||||||
|
{ workspaceId, docId },
|
||||||
|
reason,
|
||||||
|
{ room: realtimeDocShareStateRoom(workspaceId, docId) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DocGrantsRealtimeProvider implements OnModuleInit {
|
||||||
|
constructor(
|
||||||
|
private readonly ac: PermissionAccess,
|
||||||
|
private readonly models: Models,
|
||||||
|
private readonly grants: DocGrantsService,
|
||||||
|
@Optional() private readonly registry?: RealtimeRegistry,
|
||||||
|
@Optional() private readonly publisher?: RealtimePublisher
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
if (!this.registry) return;
|
||||||
|
|
||||||
|
registerRealtimeLiveQuery(this.registry, {
|
||||||
|
request: {
|
||||||
|
name: 'doc.grants.get',
|
||||||
|
input: z
|
||||||
|
.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
docId: z.string(),
|
||||||
|
pagination: z
|
||||||
|
.object({
|
||||||
|
first: z.number().int().positive(),
|
||||||
|
offset: z.number().int().nonnegative().optional(),
|
||||||
|
after: z.string().optional(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
handle: async (user, input) =>
|
||||||
|
this.getGrants(user.id, input.workspaceId, input.docId, {
|
||||||
|
first: input.pagination.first,
|
||||||
|
offset: input.pagination.offset ?? 0,
|
||||||
|
after: input.pagination.after,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
topic: {
|
||||||
|
name: 'doc.grants.changed',
|
||||||
|
input: docInput,
|
||||||
|
authorize: async (user, input) => {
|
||||||
|
await this.assertRead(user.id, input.workspaceId, input.docId);
|
||||||
|
},
|
||||||
|
room: (_user, input) =>
|
||||||
|
realtimeDocGrantsRoom(input.workspaceId, input.docId),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('doc.grants.changed', { suppressError: true })
|
||||||
|
onGrantsChanged({ workspaceId, docId }: Events['doc.grants.changed']) {
|
||||||
|
this.publish(workspaceId, docId, 'grants-changed');
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('doc.owner.changed', { suppressError: true })
|
||||||
|
onOwnerChanged({ workspaceId, docId }: Events['doc.owner.changed']) {
|
||||||
|
this.publish(workspaceId, docId, 'owner-changed');
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('user.updated', { suppressError: true })
|
||||||
|
async onUserUpdated(user: Events['user.updated']) {
|
||||||
|
const grants = await this.models.docUser.findDirectGrantDocIdsByUser(
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
for (const grant of grants) {
|
||||||
|
this.publish(grant.workspaceId, grant.docId, 'user-updated');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getGrants(
|
||||||
|
userId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
docId: string,
|
||||||
|
input: PaginationInput
|
||||||
|
): Promise<PaginatedDocGrantedUsersSnapshot> {
|
||||||
|
await this.assertRead(userId, workspaceId, docId);
|
||||||
|
const pagination = PaginationInput.decode.transform(input, {} as never);
|
||||||
|
const page = await this.grants.paginateGrantedUsers(
|
||||||
|
workspaceId,
|
||||||
|
docId,
|
||||||
|
pagination
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalCount: page.totalCount,
|
||||||
|
pageInfo: {
|
||||||
|
endCursor: page.pageInfo.endCursor ?? null,
|
||||||
|
hasNextPage: page.pageInfo.hasNextPage,
|
||||||
|
},
|
||||||
|
edges: page.edges.map(edge => ({
|
||||||
|
node: {
|
||||||
|
role: DocRole[edge.node.type],
|
||||||
|
user: {
|
||||||
|
id: edge.node.user.id,
|
||||||
|
name: edge.node.user.name,
|
||||||
|
email: edge.node.user.email,
|
||||||
|
avatarUrl: edge.node.user.avatarUrl ?? null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async assertRead(userId: string, workspaceId: string, docId: string) {
|
||||||
|
await this.ac.user(userId).doc(workspaceId, docId).assert('Doc.Users.Read');
|
||||||
|
}
|
||||||
|
|
||||||
|
private publish(workspaceId: string, docId: string, reason: string) {
|
||||||
|
this.publisher?.publishChanged(
|
||||||
|
'doc.grants.changed',
|
||||||
|
{ workspaceId, docId },
|
||||||
|
reason,
|
||||||
|
{ room: realtimeDocGrantsRoom(workspaceId, docId) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,12 @@ declare global {
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
};
|
};
|
||||||
|
'workspace.invite_link.created': {
|
||||||
|
workspaceId: string;
|
||||||
|
};
|
||||||
|
'workspace.invite_link.revoked': {
|
||||||
|
workspaceId: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,17 @@ import { QuotaModule } from '../quota';
|
|||||||
import { StorageModule } from '../storage';
|
import { StorageModule } from '../storage';
|
||||||
import { UserModule } from '../user';
|
import { UserModule } from '../user';
|
||||||
import { WorkspacesController } from './controller';
|
import { WorkspacesController } from './controller';
|
||||||
|
import { DocGrantsService } from './doc-grants';
|
||||||
|
import {
|
||||||
|
DocGrantsRealtimeProvider,
|
||||||
|
DocShareRealtimeProvider,
|
||||||
|
} from './doc-realtime';
|
||||||
import { WorkspaceEvents } from './event';
|
import { WorkspaceEvents } from './event';
|
||||||
|
import {
|
||||||
|
WorkspaceAccessRealtimeProvider,
|
||||||
|
WorkspaceConfigRealtimeProvider,
|
||||||
|
WorkspaceMembersRealtimeProvider,
|
||||||
|
} from './realtime';
|
||||||
import {
|
import {
|
||||||
DocHistoryResolver,
|
DocHistoryResolver,
|
||||||
DocResolver,
|
DocResolver,
|
||||||
@@ -44,7 +54,13 @@ import { WorkspaceStatsJob } from './stats.job';
|
|||||||
DocHistoryResolver,
|
DocHistoryResolver,
|
||||||
WorkspaceBlobResolver,
|
WorkspaceBlobResolver,
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
|
DocGrantsService,
|
||||||
WorkspaceEvents,
|
WorkspaceEvents,
|
||||||
|
WorkspaceAccessRealtimeProvider,
|
||||||
|
WorkspaceConfigRealtimeProvider,
|
||||||
|
WorkspaceMembersRealtimeProvider,
|
||||||
|
DocShareRealtimeProvider,
|
||||||
|
DocGrantsRealtimeProvider,
|
||||||
AdminWorkspaceResolver,
|
AdminWorkspaceResolver,
|
||||||
WorkspaceStatsJob,
|
WorkspaceStatsJob,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,401 @@
|
|||||||
|
import {
|
||||||
|
WORKSPACE_MEMBERS_REQUEST_TAKE_MAX,
|
||||||
|
type WorkspaceAccessSnapshot,
|
||||||
|
type WorkspaceConfigSnapshot,
|
||||||
|
type WorkspaceInviteLinkSnapshot,
|
||||||
|
type WorkspaceMemberSnapshot,
|
||||||
|
} from '@affine/realtime';
|
||||||
|
import { Injectable, OnModuleInit, Optional } from '@nestjs/common';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Cache,
|
||||||
|
isValidCacheTtl,
|
||||||
|
OnEvent,
|
||||||
|
QueryTooLong,
|
||||||
|
URLHelper,
|
||||||
|
} from '../../base';
|
||||||
|
import { Models } from '../../models';
|
||||||
|
import type { WorkspaceUserCompat } from '../../models/workspace-user-compat';
|
||||||
|
import type { CurrentUser } from '../auth';
|
||||||
|
import {
|
||||||
|
mapPermissionsToGraphqlPermissions,
|
||||||
|
PermissionAccess,
|
||||||
|
WorkspaceRole,
|
||||||
|
} from '../permission';
|
||||||
|
import { registerRealtimeLiveQuery } from '../realtime/provider';
|
||||||
|
import { RealtimePublisher } from '../realtime/publisher';
|
||||||
|
import { RealtimeRegistry } from '../realtime/registry';
|
||||||
|
import {
|
||||||
|
realtimeWorkspaceAccessRoom,
|
||||||
|
realtimeWorkspaceConfigRoom,
|
||||||
|
realtimeWorkspaceInviteLinkRoom,
|
||||||
|
realtimeWorkspaceMembersRoom,
|
||||||
|
} from '../realtime/rooms';
|
||||||
|
import { WorkspaceService } from './service';
|
||||||
|
|
||||||
|
const workspaceInput = z.object({ workspaceId: z.string() }).strict();
|
||||||
|
|
||||||
|
function serializeWorkspaceMember(
|
||||||
|
row: WorkspaceUserCompat
|
||||||
|
): WorkspaceMemberSnapshot {
|
||||||
|
if (!row.user) {
|
||||||
|
throw new Error('Workspace member user is required');
|
||||||
|
}
|
||||||
|
const role = WorkspaceRole[row.type as WorkspaceRole];
|
||||||
|
return {
|
||||||
|
...row.user,
|
||||||
|
avatarUrl: row.user.avatarUrl ?? null,
|
||||||
|
permission: role,
|
||||||
|
role,
|
||||||
|
inviteId: row.id,
|
||||||
|
emailVerified: null,
|
||||||
|
status: row.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WorkspaceAccessRealtimeProvider implements OnModuleInit {
|
||||||
|
constructor(
|
||||||
|
private readonly ac: PermissionAccess,
|
||||||
|
private readonly workspaceService: WorkspaceService,
|
||||||
|
@Optional() private readonly registry?: RealtimeRegistry,
|
||||||
|
@Optional() private readonly publisher?: RealtimePublisher
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
if (!this.registry) return;
|
||||||
|
|
||||||
|
registerRealtimeLiveQuery(this.registry, {
|
||||||
|
request: {
|
||||||
|
name: 'workspace.access.get',
|
||||||
|
input: workspaceInput,
|
||||||
|
handle: async (user, input) => ({
|
||||||
|
access: await this.getAccess(user, input.workspaceId),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
topic: {
|
||||||
|
name: 'workspace.access.changed',
|
||||||
|
input: workspaceInput,
|
||||||
|
authorize: async (user, input) => {
|
||||||
|
await this.ac
|
||||||
|
.user(user.id)
|
||||||
|
.workspace(input.workspaceId)
|
||||||
|
.assert('Workspace.Read');
|
||||||
|
},
|
||||||
|
room: (_user, input) => realtimeWorkspaceAccessRoom(input.workspaceId),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('workspace.members.updated', { suppressError: true })
|
||||||
|
onMembersUpdated({ workspaceId }: Events['workspace.members.updated']) {
|
||||||
|
this.publish(workspaceId, 'members-updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('workspace.members.roleChanged', { suppressError: true })
|
||||||
|
onMemberRoleChanged({
|
||||||
|
workspaceId,
|
||||||
|
}: Events['workspace.members.roleChanged']) {
|
||||||
|
this.publish(workspaceId, 'member-role-changed');
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('workspace.owner.changed', { suppressError: true })
|
||||||
|
onWorkspaceOwnerChanged({ workspaceId }: Events['workspace.owner.changed']) {
|
||||||
|
this.publish(workspaceId, 'owner-changed');
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('workspace.quota_state.changed', { suppressError: true })
|
||||||
|
onWorkspaceQuotaStateChanged({
|
||||||
|
workspaceId,
|
||||||
|
}: Events['workspace.quota_state.changed']) {
|
||||||
|
this.publish(workspaceId, 'quota-state-changed');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAccess(
|
||||||
|
user: CurrentUser,
|
||||||
|
workspaceId: string
|
||||||
|
): Promise<WorkspaceAccessSnapshot> {
|
||||||
|
await this.ac.user(user.id).workspace(workspaceId).assert('Workspace.Read');
|
||||||
|
const { role, permissions } = await this.ac
|
||||||
|
.user(user.id)
|
||||||
|
.workspace(workspaceId)
|
||||||
|
.permissions();
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: role ? WorkspaceRole[role] : WorkspaceRole[WorkspaceRole.External],
|
||||||
|
permissions: mapPermissionsToGraphqlPermissions(permissions),
|
||||||
|
team: await this.workspaceService.isTeamWorkspace(workspaceId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private publish(workspaceId: string, reason: string) {
|
||||||
|
this.publisher?.publishChanged(
|
||||||
|
'workspace.access.changed',
|
||||||
|
{ workspaceId },
|
||||||
|
reason,
|
||||||
|
{ room: realtimeWorkspaceAccessRoom(workspaceId) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WorkspaceConfigRealtimeProvider implements OnModuleInit {
|
||||||
|
constructor(
|
||||||
|
private readonly ac: PermissionAccess,
|
||||||
|
private readonly models: Models,
|
||||||
|
@Optional() private readonly registry?: RealtimeRegistry,
|
||||||
|
@Optional() private readonly publisher?: RealtimePublisher
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
if (!this.registry) return;
|
||||||
|
|
||||||
|
registerRealtimeLiveQuery(this.registry, {
|
||||||
|
request: {
|
||||||
|
name: 'workspace.config.get',
|
||||||
|
input: workspaceInput,
|
||||||
|
handle: async (user, input) => ({
|
||||||
|
config: await this.getConfig(user, input.workspaceId),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
topic: {
|
||||||
|
name: 'workspace.config.changed',
|
||||||
|
input: workspaceInput,
|
||||||
|
authorize: async (user, input) => {
|
||||||
|
await this.assertRead(user.id, input.workspaceId);
|
||||||
|
},
|
||||||
|
room: (_user, input) => realtimeWorkspaceConfigRoom(input.workspaceId),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('workspace.updated', { suppressError: true })
|
||||||
|
onWorkspaceUpdated(workspace: Events['workspace.updated']) {
|
||||||
|
this.publisher?.publishChanged(
|
||||||
|
'workspace.config.changed',
|
||||||
|
{ workspaceId: workspace.id },
|
||||||
|
'workspace-updated',
|
||||||
|
{ room: realtimeWorkspaceConfigRoom(workspace.id) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getConfig(
|
||||||
|
user: CurrentUser,
|
||||||
|
workspaceId: string
|
||||||
|
): Promise<WorkspaceConfigSnapshot> {
|
||||||
|
await this.assertRead(user.id, workspaceId);
|
||||||
|
const workspace = await this.models.workspace.get(workspaceId);
|
||||||
|
return {
|
||||||
|
enableAi: Boolean(workspace?.enableAi),
|
||||||
|
enableSharing: Boolean(workspace?.enableSharing),
|
||||||
|
enableUrlPreview: Boolean(workspace?.enableUrlPreview),
|
||||||
|
enableDocEmbedding: Boolean(workspace?.enableDocEmbedding),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async assertRead(userId: string, workspaceId: string) {
|
||||||
|
await this.ac
|
||||||
|
.user(userId)
|
||||||
|
.workspace(workspaceId)
|
||||||
|
.assert('Workspace.Settings.Read');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WorkspaceMembersRealtimeProvider implements OnModuleInit {
|
||||||
|
constructor(
|
||||||
|
private readonly cache: Cache,
|
||||||
|
private readonly url: URLHelper,
|
||||||
|
private readonly ac: PermissionAccess,
|
||||||
|
private readonly models: Models,
|
||||||
|
@Optional() private readonly registry?: RealtimeRegistry,
|
||||||
|
@Optional() private readonly publisher?: RealtimePublisher
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
if (!this.registry) return;
|
||||||
|
|
||||||
|
registerRealtimeLiveQuery(this.registry, {
|
||||||
|
request: {
|
||||||
|
name: 'workspace.members.get',
|
||||||
|
input: z
|
||||||
|
.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
skip: z.number().int().nonnegative().optional(),
|
||||||
|
take: z.number().int().nonnegative().optional(),
|
||||||
|
query: z.string().optional(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
handle: async (user, input) => this.getMembers(user, input),
|
||||||
|
},
|
||||||
|
topic: {
|
||||||
|
name: 'workspace.members.changed',
|
||||||
|
input: workspaceInput,
|
||||||
|
authorize: async (user, input) => {
|
||||||
|
await this.assertMembersRead(user.id, input.workspaceId);
|
||||||
|
},
|
||||||
|
room: (_user, input) => realtimeWorkspaceMembersRoom(input.workspaceId),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
registerRealtimeLiveQuery(this.registry, {
|
||||||
|
request: {
|
||||||
|
name: 'workspace.invite-link.get',
|
||||||
|
input: workspaceInput,
|
||||||
|
handle: async (user, input) => ({
|
||||||
|
inviteLink: await this.getInviteLink(user, input.workspaceId),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
topic: {
|
||||||
|
name: 'workspace.invite-link.changed',
|
||||||
|
input: workspaceInput,
|
||||||
|
authorize: async (user, input) => {
|
||||||
|
await this.assertInviteManage(user.id, input.workspaceId);
|
||||||
|
},
|
||||||
|
room: (_user, input) =>
|
||||||
|
realtimeWorkspaceInviteLinkRoom(input.workspaceId),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('workspace.members.updated', { suppressError: true })
|
||||||
|
onMembersUpdated({ workspaceId }: Events['workspace.members.updated']) {
|
||||||
|
this.publishMembers(workspaceId, 'members-updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('workspace.members.roleChanged', { suppressError: true })
|
||||||
|
onMemberRoleChanged({
|
||||||
|
workspaceId,
|
||||||
|
}: Events['workspace.members.roleChanged']) {
|
||||||
|
this.publishMembers(workspaceId, 'member-role-changed');
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('workspace.owner.changed', { suppressError: true })
|
||||||
|
onWorkspaceOwnerChanged({ workspaceId }: Events['workspace.owner.changed']) {
|
||||||
|
this.publishMembers(workspaceId, 'owner-changed');
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('workspace.invite_link.created', { suppressError: true })
|
||||||
|
onInviteLinkCreated({
|
||||||
|
workspaceId,
|
||||||
|
}: Events['workspace.invite_link.created']) {
|
||||||
|
this.publishInviteLink(workspaceId, 'invite-link-created');
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('workspace.invite_link.revoked', { suppressError: true })
|
||||||
|
onInviteLinkRevoked({
|
||||||
|
workspaceId,
|
||||||
|
}: Events['workspace.invite_link.revoked']) {
|
||||||
|
this.publishInviteLink(workspaceId, 'invite-link-revoked');
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('user.updated', { suppressError: true })
|
||||||
|
async onUserUpdated(user: Events['user.updated']) {
|
||||||
|
const workspaceIds = await this.models.workspaceUser.getUserWorkspaceIds(
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
for (const workspaceId of workspaceIds) {
|
||||||
|
this.publishMembers(workspaceId, 'user-updated');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getMembers(
|
||||||
|
user: CurrentUser,
|
||||||
|
input: {
|
||||||
|
workspaceId: string;
|
||||||
|
skip?: number;
|
||||||
|
take?: number;
|
||||||
|
query?: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
await this.assertMembersRead(user.id, input.workspaceId);
|
||||||
|
|
||||||
|
const pagination = {
|
||||||
|
offset: Math.max(input.skip ?? 0, 0),
|
||||||
|
first: Math.min(
|
||||||
|
Math.max(input.take ?? 8, 1),
|
||||||
|
WORKSPACE_MEMBERS_REQUEST_TAKE_MAX
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (input.query) {
|
||||||
|
if (input.query.length > 255) {
|
||||||
|
throw new QueryTooLong({ max: 255 });
|
||||||
|
}
|
||||||
|
const members = await this.models.workspaceUser.search(
|
||||||
|
input.workspaceId,
|
||||||
|
input.query,
|
||||||
|
pagination
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
members: members.map(serializeWorkspaceMember),
|
||||||
|
memberCount: await this.models.workspaceUser.count(input.workspaceId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [members, memberCount] = await this.models.workspaceUser.paginate(
|
||||||
|
input.workspaceId,
|
||||||
|
pagination
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
members: members.map(serializeWorkspaceMember),
|
||||||
|
memberCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getInviteLink(
|
||||||
|
user: CurrentUser,
|
||||||
|
workspaceId: string
|
||||||
|
): Promise<WorkspaceInviteLinkSnapshot | null> {
|
||||||
|
await this.assertInviteManage(user.id, workspaceId);
|
||||||
|
|
||||||
|
const cacheId = `workspace:inviteLink:${workspaceId}`;
|
||||||
|
const id = await this.cache.get<{ inviteId: string }>(cacheId);
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expireTime = await this.cache.ttl(cacheId);
|
||||||
|
if (!isValidCacheTtl(expireTime)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
link: this.url.link(`/invite/${id.inviteId}`),
|
||||||
|
expireTime: new Date(Date.now() + expireTime * 1000).toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async assertMembersRead(userId: string, workspaceId: string) {
|
||||||
|
await this.ac
|
||||||
|
.user(userId)
|
||||||
|
.workspace(workspaceId)
|
||||||
|
.assert('Workspace.Users.Read');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async assertInviteManage(userId: string, workspaceId: string) {
|
||||||
|
await this.ac
|
||||||
|
.user(userId)
|
||||||
|
.workspace(workspaceId)
|
||||||
|
.assert('Workspace.Users.Manage');
|
||||||
|
}
|
||||||
|
|
||||||
|
private publishMembers(workspaceId: string, reason: string) {
|
||||||
|
this.publisher?.publishChanged(
|
||||||
|
'workspace.members.changed',
|
||||||
|
{ workspaceId },
|
||||||
|
reason,
|
||||||
|
{ room: realtimeWorkspaceMembersRoom(workspaceId) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private publishInviteLink(workspaceId: string, reason: string) {
|
||||||
|
this.publisher?.publishChanged(
|
||||||
|
'workspace.invite-link.changed',
|
||||||
|
{ workspaceId },
|
||||||
|
reason,
|
||||||
|
{ room: realtimeWorkspaceInviteLinkRoom(workspaceId) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
DocActionDenied,
|
DocActionDenied,
|
||||||
DocDefaultRoleCanNotBeOwner,
|
DocDefaultRoleCanNotBeOwner,
|
||||||
DocNotFound,
|
DocNotFound,
|
||||||
|
EventBus,
|
||||||
ExpectToGrantDocUserRoles,
|
ExpectToGrantDocUserRoles,
|
||||||
ExpectToPublishDoc,
|
ExpectToPublishDoc,
|
||||||
ExpectToRevokeDocUserRoles,
|
ExpectToRevokeDocUserRoles,
|
||||||
@@ -37,16 +38,15 @@ import {
|
|||||||
DOC_ACTIONS,
|
DOC_ACTIONS,
|
||||||
DocAction,
|
DocAction,
|
||||||
DocRole,
|
DocRole,
|
||||||
|
type DotToUnderline,
|
||||||
|
mapPermissionsToGraphqlPermissions,
|
||||||
PermissionAccess,
|
PermissionAccess,
|
||||||
PermissionService,
|
PermissionService,
|
||||||
} from '../../permission';
|
} from '../../permission';
|
||||||
import { PublicUserType, WorkspaceUserType } from '../../user';
|
import { PublicUserType, WorkspaceUserType } from '../../user';
|
||||||
|
import { DocGrantsService } from '../doc-grants';
|
||||||
import { WorkspaceType } from '../types';
|
import { WorkspaceType } from '../types';
|
||||||
import { TimeBucket, TimeWindow } from './analytics-types';
|
import { TimeBucket, TimeWindow } from './analytics-types';
|
||||||
import {
|
|
||||||
DotToUnderline,
|
|
||||||
mapPermissionsToGraphqlPermissions,
|
|
||||||
} from './workspace';
|
|
||||||
|
|
||||||
registerEnumType(PublicDocMode, {
|
registerEnumType(PublicDocMode, {
|
||||||
name: 'PublicDocMode',
|
name: 'PublicDocMode',
|
||||||
@@ -298,7 +298,8 @@ export class WorkspaceDocResolver {
|
|||||||
private readonly ac: PermissionAccess,
|
private readonly ac: PermissionAccess,
|
||||||
private readonly permission: PermissionService,
|
private readonly permission: PermissionService,
|
||||||
private readonly models: Models,
|
private readonly models: Models,
|
||||||
private readonly cache: Cache
|
private readonly cache: Cache,
|
||||||
|
private readonly event: EventBus
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ResolveField(() => WorkspaceDocMeta, {
|
@ResolveField(() => WorkspaceDocMeta, {
|
||||||
@@ -442,6 +443,7 @@ export class WorkspaceDocResolver {
|
|||||||
await this.ac.user(user.id).doc(workspaceId, docId).assert('Doc.Publish');
|
await this.ac.user(user.id).doc(workspaceId, docId).assert('Doc.Publish');
|
||||||
|
|
||||||
const doc = await this.models.doc.publish(workspaceId, docId, mode);
|
const doc = await this.models.doc.publish(workspaceId, docId, mode);
|
||||||
|
this.event.emit('doc.public_state.changed', { workspaceId, docId });
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Publish page ${docId} with mode ${mode} in workspace ${workspaceId}`
|
`Publish page ${docId} with mode ${mode} in workspace ${workspaceId}`
|
||||||
@@ -467,6 +469,7 @@ export class WorkspaceDocResolver {
|
|||||||
await this.ac.user(user.id).doc(workspaceId, docId).assert('Doc.Publish');
|
await this.ac.user(user.id).doc(workspaceId, docId).assert('Doc.Publish');
|
||||||
|
|
||||||
const doc = await this.models.doc.unpublish(workspaceId, docId);
|
const doc = await this.models.doc.unpublish(workspaceId, docId);
|
||||||
|
this.event.emit('doc.public_state.changed', { workspaceId, docId });
|
||||||
|
|
||||||
this.logger.log(`Revoke public doc ${docId} in workspace ${workspaceId}`);
|
this.logger.log(`Revoke public doc ${docId} in workspace ${workspaceId}`);
|
||||||
|
|
||||||
@@ -535,7 +538,9 @@ export class DocResolver {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly ac: PermissionAccess,
|
private readonly ac: PermissionAccess,
|
||||||
private readonly models: Models
|
private readonly models: Models,
|
||||||
|
private readonly grants: DocGrantsService,
|
||||||
|
private readonly event: EventBus
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ResolveField(() => PublicUserType, {
|
@ResolveField(() => PublicUserType, {
|
||||||
@@ -660,28 +665,11 @@ export class DocResolver {
|
|||||||
@Args('pagination', PaginationInput.decode) pagination: PaginationInput
|
@Args('pagination', PaginationInput.decode) pagination: PaginationInput
|
||||||
): Promise<PaginatedGrantedDocUserType> {
|
): Promise<PaginatedGrantedDocUserType> {
|
||||||
await this.ac.user(user.id).doc(doc).assert('Doc.Users.Read');
|
await this.ac.user(user.id).doc(doc).assert('Doc.Users.Read');
|
||||||
|
return await this.grants.paginateGrantedUsers(
|
||||||
const [permissions, totalCount] = await this.models.docUser.paginate(
|
|
||||||
doc.workspaceId,
|
doc.workspaceId,
|
||||||
doc.docId,
|
doc.docId,
|
||||||
pagination
|
pagination
|
||||||
);
|
);
|
||||||
|
|
||||||
const workspaceUsers = await this.models.user.getWorkspaceUsers(
|
|
||||||
permissions.map(p => p.userId)
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspaceUsersMap = new Map(workspaceUsers.map(wu => [wu.id, wu]));
|
|
||||||
|
|
||||||
return paginate(
|
|
||||||
permissions.map(p => ({
|
|
||||||
...p,
|
|
||||||
user: workspaceUsersMap.get(p.userId) as WorkspaceUserType,
|
|
||||||
})),
|
|
||||||
'createdAt',
|
|
||||||
pagination,
|
|
||||||
totalCount
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
@@ -713,6 +701,10 @@ export class DocResolver {
|
|||||||
input.userIds,
|
input.userIds,
|
||||||
input.role
|
input.role
|
||||||
);
|
);
|
||||||
|
this.event.emit('doc.grants.changed', {
|
||||||
|
workspaceId: input.workspaceId,
|
||||||
|
docId: input.docId,
|
||||||
|
});
|
||||||
|
|
||||||
const info = {
|
const info = {
|
||||||
...pairs,
|
...pairs,
|
||||||
@@ -749,6 +741,10 @@ export class DocResolver {
|
|||||||
input.docId,
|
input.docId,
|
||||||
input.userId
|
input.userId
|
||||||
);
|
);
|
||||||
|
this.event.emit('doc.grants.changed', {
|
||||||
|
workspaceId: input.workspaceId,
|
||||||
|
docId: input.docId,
|
||||||
|
});
|
||||||
|
|
||||||
const info = {
|
const info = {
|
||||||
...pairs,
|
...pairs,
|
||||||
@@ -791,6 +787,11 @@ export class DocResolver {
|
|||||||
input.docId,
|
input.docId,
|
||||||
input.userId
|
input.userId
|
||||||
);
|
);
|
||||||
|
this.event.emit('doc.owner.changed', {
|
||||||
|
workspaceId: input.workspaceId,
|
||||||
|
docId: input.docId,
|
||||||
|
userId: input.userId,
|
||||||
|
});
|
||||||
this.logger.log(`Transfer doc owner (${JSON.stringify(info)})`);
|
this.logger.log(`Transfer doc owner (${JSON.stringify(info)})`);
|
||||||
} else {
|
} else {
|
||||||
await this.ac.user(user.id).doc(input).assert('Doc.Users.Manage');
|
await this.ac.user(user.id).doc(input).assert('Doc.Users.Manage');
|
||||||
@@ -800,6 +801,10 @@ export class DocResolver {
|
|||||||
input.userId,
|
input.userId,
|
||||||
input.role
|
input.role
|
||||||
);
|
);
|
||||||
|
this.event.emit('doc.grants.changed', {
|
||||||
|
workspaceId: input.workspaceId,
|
||||||
|
docId: input.docId,
|
||||||
|
});
|
||||||
this.logger.log(`Update doc user role (${JSON.stringify(info)})`);
|
this.logger.log(`Update doc user role (${JSON.stringify(info)})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -851,6 +856,10 @@ export class DocResolver {
|
|||||||
input.docId,
|
input.docId,
|
||||||
input.role
|
input.role
|
||||||
);
|
);
|
||||||
|
this.event.emit('doc.default_role.changed', {
|
||||||
|
workspaceId: input.workspaceId,
|
||||||
|
docId: input.docId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
CanNotRevokeYourself,
|
CanNotRevokeYourself,
|
||||||
EventBus,
|
EventBus,
|
||||||
InvalidInvitation,
|
InvalidInvitation,
|
||||||
|
isValidCacheTtl,
|
||||||
mapAnyError,
|
mapAnyError,
|
||||||
MemberNotFoundInSpace,
|
MemberNotFoundInSpace,
|
||||||
NoMoreSeat,
|
NoMoreSeat,
|
||||||
@@ -258,7 +259,7 @@ export class WorkspaceMemberResolver {
|
|||||||
const id = await this.cache.get<{ inviteId: string }>(cacheId);
|
const id = await this.cache.get<{ inviteId: string }>(cacheId);
|
||||||
if (id) {
|
if (id) {
|
||||||
const expireTime = await this.cache.ttl(cacheId);
|
const expireTime = await this.cache.ttl(cacheId);
|
||||||
if (Number.isSafeInteger(expireTime)) {
|
if (isValidCacheTtl(expireTime)) {
|
||||||
return {
|
return {
|
||||||
link: this.url.link(`/invite/${id.inviteId}`),
|
link: this.url.link(`/invite/${id.inviteId}`),
|
||||||
expireTime: new Date(Date.now() + expireTime * 1000), // Convert seconds to milliseconds
|
expireTime: new Date(Date.now() + expireTime * 1000), // Convert seconds to milliseconds
|
||||||
@@ -284,7 +285,7 @@ export class WorkspaceMemberResolver {
|
|||||||
const invite = await this.cache.get<{ inviteId: string }>(cacheWorkspaceId);
|
const invite = await this.cache.get<{ inviteId: string }>(cacheWorkspaceId);
|
||||||
if (typeof invite?.inviteId === 'string') {
|
if (typeof invite?.inviteId === 'string') {
|
||||||
const expireTime = await this.cache.ttl(cacheWorkspaceId);
|
const expireTime = await this.cache.ttl(cacheWorkspaceId);
|
||||||
if (Number.isSafeInteger(expireTime)) {
|
if (isValidCacheTtl(expireTime)) {
|
||||||
return {
|
return {
|
||||||
link: this.url.link(`/invite/${invite.inviteId}`),
|
link: this.url.link(`/invite/${invite.inviteId}`),
|
||||||
expireTime: new Date(Date.now() + expireTime * 1000), // Convert seconds to milliseconds
|
expireTime: new Date(Date.now() + expireTime * 1000), // Convert seconds to milliseconds
|
||||||
@@ -300,6 +301,7 @@ export class WorkspaceMemberResolver {
|
|||||||
{ workspaceId, inviterUserId: user.id },
|
{ workspaceId, inviterUserId: user.id },
|
||||||
{ ttl: expireTime }
|
{ ttl: expireTime }
|
||||||
);
|
);
|
||||||
|
this.event.emit('workspace.invite_link.created', { workspaceId });
|
||||||
return {
|
return {
|
||||||
link: this.url.link(`/invite/${inviteId}`),
|
link: this.url.link(`/invite/${inviteId}`),
|
||||||
expireTime: new Date(Date.now() + expireTime),
|
expireTime: new Date(Date.now() + expireTime),
|
||||||
@@ -317,7 +319,13 @@ export class WorkspaceMemberResolver {
|
|||||||
.assert('Workspace.Users.Manage');
|
.assert('Workspace.Users.Manage');
|
||||||
|
|
||||||
const cacheId = `workspace:inviteLink:${workspaceId}`;
|
const cacheId = `workspace:inviteLink:${workspaceId}`;
|
||||||
return await this.cache.delete(cacheId);
|
const invite = await this.cache.get<{ inviteId: string }>(cacheId);
|
||||||
|
const deleted = await this.cache.delete(cacheId);
|
||||||
|
if (invite?.inviteId) {
|
||||||
|
await this.cache.delete(`workspace:inviteLinkId:${invite.inviteId}`);
|
||||||
|
}
|
||||||
|
this.event.emit('workspace.invite_link.revoked', { workspaceId });
|
||||||
|
return deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
@@ -406,6 +414,11 @@ export class WorkspaceMemberResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.models.workspaceUser.set(workspaceId, userId, newRole);
|
await this.models.workspaceUser.set(workspaceId, userId, newRole);
|
||||||
|
if (role.status !== WorkspaceMemberStatus.Accepted) {
|
||||||
|
this.event.emit('workspace.members.updated', {
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -608,6 +621,10 @@ export class WorkspaceMemberResolver {
|
|||||||
WorkspaceMemberStatus.Accepted
|
WorkspaceMemberStatus.Accepted
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.event.emit('workspace.members.updated', {
|
||||||
|
workspaceId: role.workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
await this.workspaceService.sendInvitationAcceptedNotification(
|
await this.workspaceService.sendInvitationAcceptedNotification(
|
||||||
role.inviterId ??
|
role.inviterId ??
|
||||||
(await this.models.workspaceUser.getOwner(role.workspaceId)).id,
|
(await this.models.workspaceUser.getOwner(role.workspaceId)).id,
|
||||||
@@ -640,6 +657,7 @@ export class WorkspaceMemberResolver {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await this.workspaceService.sendReviewRequestNotification(role.id);
|
await this.workspaceService.sendReviewRequestNotification(role.id);
|
||||||
|
this.event.emit('workspace.members.updated', { workspaceId });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ import {
|
|||||||
} from '../../../base';
|
} from '../../../base';
|
||||||
import { Models } from '../../../models';
|
import { Models } from '../../../models';
|
||||||
import { CurrentUser } from '../../auth';
|
import { CurrentUser } from '../../auth';
|
||||||
|
import type { DotToUnderline } from '../../permission';
|
||||||
import {
|
import {
|
||||||
|
mapPermissionsToGraphqlPermissions,
|
||||||
PermissionAccess,
|
PermissionAccess,
|
||||||
WORKSPACE_ACTIONS,
|
WORKSPACE_ACTIONS,
|
||||||
WorkspaceAction,
|
WorkspaceAction,
|
||||||
@@ -29,22 +31,6 @@ import { QuotaService, WorkspaceQuotaType } from '../../quota';
|
|||||||
import { WorkspaceService } from '../service';
|
import { WorkspaceService } from '../service';
|
||||||
import { UpdateWorkspaceInput, WorkspaceType } from '../types';
|
import { UpdateWorkspaceInput, WorkspaceType } from '../types';
|
||||||
|
|
||||||
export type DotToUnderline<T extends string> =
|
|
||||||
T extends `${infer Prefix}.${infer Suffix}`
|
|
||||||
? `${Prefix}_${DotToUnderline<Suffix>}`
|
|
||||||
: T;
|
|
||||||
|
|
||||||
export function mapPermissionsToGraphqlPermissions<A extends string>(
|
|
||||||
permission: Record<A, boolean>
|
|
||||||
): Record<DotToUnderline<A>, boolean> {
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(permission).map(([key, value]) => [
|
|
||||||
key.replaceAll('.', '_'),
|
|
||||||
value,
|
|
||||||
])
|
|
||||||
) as Record<DotToUnderline<A>, boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const WorkspacePermissions = registerObjectType<
|
const WorkspacePermissions = registerObjectType<
|
||||||
Record<DotToUnderline<WorkspaceAction>, boolean>
|
Record<DotToUnderline<WorkspaceAction>, boolean>
|
||||||
>(
|
>(
|
||||||
|
|||||||
@@ -5,6 +5,17 @@ import { BaseModel } from './base';
|
|||||||
|
|
||||||
const REDACTED_TOKEN = '[REDACTED]';
|
const REDACTED_TOKEN = '[REDACTED]';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Events {
|
||||||
|
'user.access_token.created': {
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
'user.access_token.revoked': {
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateAccessTokenInput {
|
export interface CreateAccessTokenInput {
|
||||||
userId: string;
|
userId: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -10,6 +10,20 @@ import { BaseModel } from './base';
|
|||||||
import { DocRole } from './common';
|
import { DocRole } from './common';
|
||||||
import { docRoleFromNew } from './permission-write';
|
import { docRoleFromNew } from './permission-write';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Events {
|
||||||
|
'doc.grants.changed': {
|
||||||
|
workspaceId: string;
|
||||||
|
docId: string;
|
||||||
|
};
|
||||||
|
'doc.owner.changed': {
|
||||||
|
workspaceId: string;
|
||||||
|
docId: string;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DocUserModel extends BaseModel {
|
export class DocUserModel extends BaseModel {
|
||||||
/**
|
/**
|
||||||
@@ -124,6 +138,14 @@ export class DocUserModel extends BaseModel {
|
|||||||
return grants.map(grant => this.docGrantToCompat(grant));
|
return grants.map(grant => this.docGrantToCompat(grant));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findDirectGrantDocIdsByUser(userId: string) {
|
||||||
|
return await this.db.docGrant.findMany({
|
||||||
|
where: { principalType: 'user', principalId: userId },
|
||||||
|
select: { workspaceId: true, docId: true },
|
||||||
|
distinct: ['workspaceId', 'docId'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
count(workspaceId: string, docId: string) {
|
count(workspaceId: string, docId: string) {
|
||||||
return this.db.docGrant.count({
|
return this.db.docGrant.count({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ declare global {
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
docId: string;
|
docId: string;
|
||||||
};
|
};
|
||||||
|
'doc.public_state.changed': {
|
||||||
|
workspaceId: string;
|
||||||
|
docId: string;
|
||||||
|
};
|
||||||
|
'doc.default_role.changed': {
|
||||||
|
workspaceId: string;
|
||||||
|
docId: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ import z from 'zod';
|
|||||||
|
|
||||||
import { BaseModel } from './base';
|
import { BaseModel } from './base';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Events {
|
||||||
|
'user.settings.updated': {
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const UserSettingsSchema = z.object({
|
export const UserSettingsSchema = z.object({
|
||||||
receiveInvitationEmail: z.boolean().default(true),
|
receiveInvitationEmail: z.boolean().default(true),
|
||||||
receiveMentionEmail: z.boolean().default(true),
|
receiveMentionEmail: z.boolean().default(true),
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ export class UserModel extends BaseModel {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.logger.debug(`User [${user.id}] updated`);
|
this.logger.debug(`User [${user.id}] updated`);
|
||||||
this.event.emit('user.updated', user);
|
this.event.emitDetached('user.updated', user);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -186,36 +186,68 @@ export async function searchCompatRows(
|
|||||||
pagination: { first: number; offset: number; after?: string | Date }
|
pagination: { first: number; offset: number; after?: string | Date }
|
||||||
) {
|
) {
|
||||||
const after = pagination.after
|
const after = pagination.after
|
||||||
? Prisma.sql`AND wm.created_at >= ${pagination.after}::timestamptz`
|
? Prisma.sql`AND created_at >= ${pagination.after}::timestamptz`
|
||||||
: Prisma.empty;
|
: Prisma.empty;
|
||||||
const rows = await db.$queryRaw<WorkspaceUserCompatRow[]>`
|
const rows = await db.$queryRaw<WorkspaceUserCompatRow[]>`
|
||||||
SELECT
|
SELECT *
|
||||||
COALESCE(wm.legacy_permission_id, wm.id) AS id,
|
FROM (
|
||||||
wm.workspace_id AS "workspaceId",
|
SELECT
|
||||||
wm.user_id AS "userId",
|
COALESCE(wm.legacy_permission_id, wm.id) AS id,
|
||||||
CASE wm.role
|
wm.workspace_id AS "workspaceId",
|
||||||
WHEN 'owner' THEN ${WorkspaceRole.Owner}
|
wm.user_id AS "userId",
|
||||||
WHEN 'admin' THEN ${WorkspaceRole.Admin}
|
CASE wm.role
|
||||||
ELSE ${WorkspaceRole.Collaborator}
|
WHEN 'owner' THEN ${WorkspaceRole.Owner}
|
||||||
END AS type,
|
WHEN 'admin' THEN ${WorkspaceRole.Admin}
|
||||||
'Accepted'::"WorkspaceMemberStatus" AS status,
|
ELSE ${WorkspaceRole.Collaborator}
|
||||||
CASE wm.source
|
END AS type,
|
||||||
WHEN 'link' THEN 'Link'::"WorkspaceMemberSource"
|
'Accepted'::"WorkspaceMemberStatus" AS status,
|
||||||
ELSE 'Email'::"WorkspaceMemberSource"
|
CASE wm.source
|
||||||
END AS source,
|
WHEN 'link' THEN 'Link'::"WorkspaceMemberSource"
|
||||||
NULL::varchar AS "inviterId",
|
ELSE 'Email'::"WorkspaceMemberSource"
|
||||||
wm.created_at AS "createdAt",
|
END AS source,
|
||||||
wm.updated_at AS "updatedAt",
|
NULL::varchar AS "inviterId",
|
||||||
u.name AS "userName",
|
wm.created_at AS "createdAt",
|
||||||
u.email AS "userEmail",
|
wm.updated_at AS "updatedAt",
|
||||||
u.avatar_url AS "userAvatarUrl"
|
u.name AS "userName",
|
||||||
FROM workspace_members wm
|
u.email AS "userEmail",
|
||||||
INNER JOIN users u ON u.id = wm.user_id
|
u.avatar_url AS "userAvatarUrl",
|
||||||
WHERE wm.workspace_id = ${workspaceId}
|
wm.created_at AS created_at
|
||||||
AND wm.state = 'active'
|
FROM workspace_members wm
|
||||||
AND (u.email ILIKE ${`%${query}%`} OR u.name ILIKE ${`%${query}%`})
|
INNER JOIN users u ON u.id = wm.user_id
|
||||||
|
WHERE wm.workspace_id = ${workspaceId}
|
||||||
|
AND wm.state = 'active'
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
COALESCE(wi.legacy_permission_id, wi.id) AS id,
|
||||||
|
wi.workspace_id AS "workspaceId",
|
||||||
|
wi.invitee_user_id AS "userId",
|
||||||
|
CASE wi.requested_role
|
||||||
|
WHEN 'admin' THEN ${WorkspaceRole.Admin}
|
||||||
|
ELSE ${WorkspaceRole.Collaborator}
|
||||||
|
END AS type,
|
||||||
|
CASE wi.status
|
||||||
|
WHEN 'waiting_review' THEN 'UnderReview'::"WorkspaceMemberStatus"
|
||||||
|
WHEN 'waiting_seat' THEN 'NeedMoreSeat'::"WorkspaceMemberStatus"
|
||||||
|
ELSE 'Pending'::"WorkspaceMemberStatus"
|
||||||
|
END AS status,
|
||||||
|
CASE wi.kind
|
||||||
|
WHEN 'link' THEN 'Link'::"WorkspaceMemberSource"
|
||||||
|
ELSE 'Email'::"WorkspaceMemberSource"
|
||||||
|
END AS source,
|
||||||
|
wi.inviter_user_id AS "inviterId",
|
||||||
|
wi.created_at AS "createdAt",
|
||||||
|
wi.updated_at AS "updatedAt",
|
||||||
|
u.name AS "userName",
|
||||||
|
u.email AS "userEmail",
|
||||||
|
u.avatar_url AS "userAvatarUrl",
|
||||||
|
wi.created_at AS created_at
|
||||||
|
FROM workspace_invitations wi
|
||||||
|
INNER JOIN users u ON u.id = wi.invitee_user_id
|
||||||
|
WHERE wi.workspace_id = ${workspaceId}
|
||||||
|
) roles
|
||||||
|
WHERE ("userEmail" ILIKE ${`%${query}%`} OR "userName" ILIKE ${`%${query}%`})
|
||||||
${after}
|
${after}
|
||||||
ORDER BY wm.created_at ASC
|
ORDER BY created_at ASC
|
||||||
OFFSET ${pagination.offset}
|
OFFSET ${pagination.offset}
|
||||||
LIMIT ${pagination.first}
|
LIMIT ${pagination.first}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -422,6 +422,26 @@ export class WorkspaceUserModel extends BaseModel {
|
|||||||
return await findUserActiveWorkspaceRoles(this.db, userId, filter);
|
return await findUserActiveWorkspaceRoles(this.db, userId, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserWorkspaceIds(userId: string) {
|
||||||
|
const [roles, invitations] = await Promise.all([
|
||||||
|
this.db.workspaceMember.findMany({
|
||||||
|
where: { userId, state: 'active' },
|
||||||
|
select: { workspaceId: true },
|
||||||
|
}),
|
||||||
|
this.db.workspaceInvitation.findMany({
|
||||||
|
where: { inviteeUserId: userId },
|
||||||
|
select: { workspaceId: true },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return Array.from(
|
||||||
|
new Set([
|
||||||
|
...roles.map(role => role.workspaceId),
|
||||||
|
...invitations.map(invitation => invitation.workspaceId),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async hasSharedWorkspace(userId: string, otherUserId: string) {
|
async hasSharedWorkspace(userId: string, otherUserId: string) {
|
||||||
return await hasSharedWorkspace(this.db, userId, otherUserId);
|
return await hasSharedWorkspace(this.db, userId, otherUserId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
query listUserAccessTokens {
|
|
||||||
currentUser {
|
|
||||||
revealedAccessTokens {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
createdAt
|
|
||||||
expiresAt
|
|
||||||
token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
query getWorkspaceEmbeddingStatus($workspaceId: String!) {
|
|
||||||
queryWorkspaceEmbeddingStatus(workspaceId: $workspaceId) {
|
|
||||||
total
|
|
||||||
embedded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
fragment CurrentUserProfile on UserType {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
email
|
|
||||||
avatarUrl
|
|
||||||
emailVerified
|
|
||||||
features
|
|
||||||
settings {
|
|
||||||
receiveInvitationEmail
|
|
||||||
receiveMentionEmail
|
|
||||||
receiveCommentEmail
|
|
||||||
}
|
|
||||||
quota {
|
|
||||||
name
|
|
||||||
blobLimit
|
|
||||||
storageQuota
|
|
||||||
historyPeriod
|
|
||||||
memberLimit
|
|
||||||
humanReadable {
|
|
||||||
name
|
|
||||||
blobLimit
|
|
||||||
storageQuota
|
|
||||||
historyPeriod
|
|
||||||
memberLimit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
quotaUsage {
|
|
||||||
storageQuota
|
|
||||||
}
|
|
||||||
copilot {
|
|
||||||
quota {
|
|
||||||
limit
|
|
||||||
used
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#import './fragments/current-user-profile.gql'
|
|
||||||
|
|
||||||
query getCurrentUserProfile {
|
|
||||||
currentUser {
|
|
||||||
...CurrentUserProfile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
query getDocDefaultRole($workspaceId: String!, $docId: String!) {
|
|
||||||
workspace(id: $workspaceId) {
|
|
||||||
doc(docId: $docId) {
|
|
||||||
defaultRole
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
query getMembersByWorkspaceId(
|
|
||||||
$workspaceId: String!
|
|
||||||
$skip: Int
|
|
||||||
$take: Int
|
|
||||||
$query: String
|
|
||||||
) {
|
|
||||||
workspace(id: $workspaceId) {
|
|
||||||
memberCount
|
|
||||||
members(skip: $skip, take: $take, query: $query) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
email
|
|
||||||
avatarUrl
|
|
||||||
permission
|
|
||||||
inviteId
|
|
||||||
emailVerified
|
|
||||||
status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
query getPageGrantedUsersList(
|
|
||||||
$pagination: PaginationInput!
|
|
||||||
$docId: String!
|
|
||||||
$workspaceId: String!
|
|
||||||
) {
|
|
||||||
workspace(id: $workspaceId) {
|
|
||||||
doc(docId: $docId) {
|
|
||||||
grantedUsersList(pagination: $pagination) {
|
|
||||||
totalCount
|
|
||||||
pageInfo {
|
|
||||||
endCursor
|
|
||||||
hasNextPage
|
|
||||||
}
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
role
|
|
||||||
user {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
email
|
|
||||||
avatarUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
query getWorkspaceInfo($workspaceId: String!) {
|
|
||||||
workspace(id: $workspaceId) {
|
|
||||||
permissions {
|
|
||||||
Workspace_Administrators_Manage
|
|
||||||
Workspace_Blobs_List
|
|
||||||
Workspace_Blobs_Read
|
|
||||||
Workspace_Blobs_Write
|
|
||||||
Workspace_Copilot
|
|
||||||
Workspace_CreateDoc
|
|
||||||
Workspace_Delete
|
|
||||||
Workspace_Organize_Read
|
|
||||||
Workspace_Payment_Manage
|
|
||||||
Workspace_Properties_Create
|
|
||||||
Workspace_Properties_Delete
|
|
||||||
Workspace_Properties_Read
|
|
||||||
Workspace_Properties_Update
|
|
||||||
Workspace_Read
|
|
||||||
Workspace_Settings_Read
|
|
||||||
Workspace_Settings_Update
|
|
||||||
Workspace_Sync
|
|
||||||
Workspace_TransferOwner
|
|
||||||
Workspace_Users_Manage
|
|
||||||
Workspace_Users_Read
|
|
||||||
}
|
|
||||||
role
|
|
||||||
team
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -41,42 +41,6 @@ export const credentialsRequirementsFragment = `fragment CredentialsRequirements
|
|||||||
...PasswordLimits
|
...PasswordLimits
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
export const currentUserProfileFragment = `fragment CurrentUserProfile on UserType {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
email
|
|
||||||
avatarUrl
|
|
||||||
emailVerified
|
|
||||||
features
|
|
||||||
settings {
|
|
||||||
receiveInvitationEmail
|
|
||||||
receiveMentionEmail
|
|
||||||
receiveCommentEmail
|
|
||||||
}
|
|
||||||
quota {
|
|
||||||
name
|
|
||||||
blobLimit
|
|
||||||
storageQuota
|
|
||||||
historyPeriod
|
|
||||||
memberLimit
|
|
||||||
humanReadable {
|
|
||||||
name
|
|
||||||
blobLimit
|
|
||||||
storageQuota
|
|
||||||
historyPeriod
|
|
||||||
memberLimit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
quotaUsage {
|
|
||||||
storageQuota
|
|
||||||
}
|
|
||||||
copilot {
|
|
||||||
quota {
|
|
||||||
limit
|
|
||||||
used
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
export const paginatedCopilotChatsFragment = `fragment PaginatedCopilotChats on PaginatedCopilotHistoriesType {
|
export const paginatedCopilotChatsFragment = `fragment PaginatedCopilotChats on PaginatedCopilotHistoriesType {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
hasNextPage
|
hasNextPage
|
||||||
@@ -117,22 +81,6 @@ export const generateUserAccessTokenMutation = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listUserAccessTokensQuery = {
|
|
||||||
id: 'listUserAccessTokensQuery' as const,
|
|
||||||
op: 'listUserAccessTokens',
|
|
||||||
query: `query listUserAccessTokens {
|
|
||||||
currentUser {
|
|
||||||
revealedAccessTokens {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
createdAt
|
|
||||||
expiresAt
|
|
||||||
token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const revokeUserAccessTokenMutation = {
|
export const revokeUserAccessTokenMutation = {
|
||||||
id: 'revokeUserAccessTokenMutation' as const,
|
id: 'revokeUserAccessTokenMutation' as const,
|
||||||
op: 'revokeUserAccessToken',
|
op: 'revokeUserAccessToken',
|
||||||
@@ -1287,18 +1235,6 @@ export const matchFilesQuery = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWorkspaceEmbeddingStatusQuery = {
|
|
||||||
id: 'getWorkspaceEmbeddingStatusQuery' as const,
|
|
||||||
op: 'getWorkspaceEmbeddingStatus',
|
|
||||||
query: `query getWorkspaceEmbeddingStatus($workspaceId: String!) {
|
|
||||||
queryWorkspaceEmbeddingStatus(workspaceId: $workspaceId) {
|
|
||||||
total
|
|
||||||
embedded
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
deprecations: ["'queryWorkspaceEmbeddingStatus' is deprecated: Use realtime subscription \"workspace.embedding.progress.changed\" instead."],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const queueWorkspaceEmbeddingMutation = {
|
export const queueWorkspaceEmbeddingMutation = {
|
||||||
id: 'queueWorkspaceEmbeddingMutation' as const,
|
id: 'queueWorkspaceEmbeddingMutation' as const,
|
||||||
op: 'queueWorkspaceEmbedding',
|
op: 'queueWorkspaceEmbedding',
|
||||||
@@ -1936,17 +1872,6 @@ export const getCurrentUserFeaturesQuery = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCurrentUserProfileQuery = {
|
|
||||||
id: 'getCurrentUserProfileQuery' as const,
|
|
||||||
op: 'getCurrentUserProfile',
|
|
||||||
query: `query getCurrentUserProfile {
|
|
||||||
currentUser {
|
|
||||||
...CurrentUserProfile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${currentUserProfileFragment}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCurrentUserQuery = {
|
export const getCurrentUserQuery = {
|
||||||
id: 'getCurrentUserQuery' as const,
|
id: 'getCurrentUserQuery' as const,
|
||||||
op: 'getCurrentUser',
|
op: 'getCurrentUser',
|
||||||
@@ -1988,18 +1913,6 @@ export const getDocCreatedByUpdatedByListQuery = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDocDefaultRoleQuery = {
|
|
||||||
id: 'getDocDefaultRoleQuery' as const,
|
|
||||||
op: 'getDocDefaultRole',
|
|
||||||
query: `query getDocDefaultRole($workspaceId: String!, $docId: String!) {
|
|
||||||
workspace(id: $workspaceId) {
|
|
||||||
doc(docId: $docId) {
|
|
||||||
defaultRole
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDocLastAccessedMembersQuery = {
|
export const getDocLastAccessedMembersQuery = {
|
||||||
id: 'getDocLastAccessedMembersQuery' as const,
|
id: 'getDocLastAccessedMembersQuery' as const,
|
||||||
op: 'getDocLastAccessedMembers',
|
op: 'getDocLastAccessedMembers',
|
||||||
@@ -2118,27 +2031,6 @@ export const getMemberCountByWorkspaceIdQuery = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMembersByWorkspaceIdQuery = {
|
|
||||||
id: 'getMembersByWorkspaceIdQuery' as const,
|
|
||||||
op: 'getMembersByWorkspaceId',
|
|
||||||
query: `query getMembersByWorkspaceId($workspaceId: String!, $skip: Int, $take: Int, $query: String) {
|
|
||||||
workspace(id: $workspaceId) {
|
|
||||||
memberCount
|
|
||||||
members(skip: $skip, take: $take, query: $query) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
email
|
|
||||||
avatarUrl
|
|
||||||
permission
|
|
||||||
inviteId
|
|
||||||
emailVerified
|
|
||||||
status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
deprecations: ["'permission' is deprecated: Use role instead"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const oauthProvidersQuery = {
|
export const oauthProvidersQuery = {
|
||||||
id: 'oauthProvidersQuery' as const,
|
id: 'oauthProvidersQuery' as const,
|
||||||
op: 'oauthProviders',
|
op: 'oauthProviders',
|
||||||
@@ -2149,35 +2041,6 @@ export const oauthProvidersQuery = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPageGrantedUsersListQuery = {
|
|
||||||
id: 'getPageGrantedUsersListQuery' as const,
|
|
||||||
op: 'getPageGrantedUsersList',
|
|
||||||
query: `query getPageGrantedUsersList($pagination: PaginationInput!, $docId: String!, $workspaceId: String!) {
|
|
||||||
workspace(id: $workspaceId) {
|
|
||||||
doc(docId: $docId) {
|
|
||||||
grantedUsersList(pagination: $pagination) {
|
|
||||||
totalCount
|
|
||||||
pageInfo {
|
|
||||||
endCursor
|
|
||||||
hasNextPage
|
|
||||||
}
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
role
|
|
||||||
user {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
email
|
|
||||||
avatarUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPublicUserByIdQuery = {
|
export const getPublicUserByIdQuery = {
|
||||||
id: 'getPublicUserByIdQuery' as const,
|
id: 'getPublicUserByIdQuery' as const,
|
||||||
op: 'getPublicUserById',
|
op: 'getPublicUserById',
|
||||||
@@ -2262,39 +2125,6 @@ export const getUserQuery = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWorkspaceInfoQuery = {
|
|
||||||
id: 'getWorkspaceInfoQuery' as const,
|
|
||||||
op: 'getWorkspaceInfo',
|
|
||||||
query: `query getWorkspaceInfo($workspaceId: String!) {
|
|
||||||
workspace(id: $workspaceId) {
|
|
||||||
permissions {
|
|
||||||
Workspace_Administrators_Manage
|
|
||||||
Workspace_Blobs_List
|
|
||||||
Workspace_Blobs_Read
|
|
||||||
Workspace_Blobs_Write
|
|
||||||
Workspace_Copilot
|
|
||||||
Workspace_CreateDoc
|
|
||||||
Workspace_Delete
|
|
||||||
Workspace_Organize_Read
|
|
||||||
Workspace_Payment_Manage
|
|
||||||
Workspace_Properties_Create
|
|
||||||
Workspace_Properties_Delete
|
|
||||||
Workspace_Properties_Read
|
|
||||||
Workspace_Properties_Update
|
|
||||||
Workspace_Read
|
|
||||||
Workspace_Settings_Read
|
|
||||||
Workspace_Settings_Update
|
|
||||||
Workspace_Sync
|
|
||||||
Workspace_TransferOwner
|
|
||||||
Workspace_Users_Manage
|
|
||||||
Workspace_Users_Read
|
|
||||||
}
|
|
||||||
role
|
|
||||||
team
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getWorkspacePageByIdQuery = {
|
export const getWorkspacePageByIdQuery = {
|
||||||
id: 'getWorkspacePageByIdQuery' as const,
|
id: 'getWorkspacePageByIdQuery' as const,
|
||||||
op: 'getWorkspacePageById',
|
op: 'getWorkspacePageById',
|
||||||
@@ -2643,17 +2473,6 @@ export const mentionUserMutation = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const notificationCountQuery = {
|
|
||||||
id: 'notificationCountQuery' as const,
|
|
||||||
op: 'notificationCount',
|
|
||||||
query: `query notificationCount {
|
|
||||||
currentUser {
|
|
||||||
notificationCount
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
deprecations: ["'notificationCount' is deprecated: Use realtime subscription \"notification.count.changed\" instead."],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const pricesQuery = {
|
export const pricesQuery = {
|
||||||
id: 'pricesQuery' as const,
|
id: 'pricesQuery' as const,
|
||||||
op: 'prices',
|
op: 'prices',
|
||||||
@@ -3117,19 +2936,6 @@ export const workspaceByokSettingsQuery = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWorkspaceConfigQuery = {
|
|
||||||
id: 'getWorkspaceConfigQuery' as const,
|
|
||||||
op: 'getWorkspaceConfig',
|
|
||||||
query: `query getWorkspaceConfig($id: String!) {
|
|
||||||
workspace(id: $id) {
|
|
||||||
enableAi
|
|
||||||
enableSharing
|
|
||||||
enableUrlPreview
|
|
||||||
enableDocEmbedding
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setEnableAiMutation = {
|
export const setEnableAiMutation = {
|
||||||
id: 'setEnableAiMutation' as const,
|
id: 'setEnableAiMutation' as const,
|
||||||
op: 'setEnableAi',
|
op: 'setEnableAi',
|
||||||
@@ -3191,19 +2997,6 @@ export const acceptInviteByInviteIdMutation = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWorkspaceInviteLinkQuery = {
|
|
||||||
id: 'getWorkspaceInviteLinkQuery' as const,
|
|
||||||
op: 'getWorkspaceInviteLink',
|
|
||||||
query: `query getWorkspaceInviteLink($id: String!) {
|
|
||||||
workspace(id: $id) {
|
|
||||||
inviteLink {
|
|
||||||
link
|
|
||||||
expireTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createInviteLinkMutation = {
|
export const createInviteLinkMutation = {
|
||||||
id: 'createInviteLinkMutation' as const,
|
id: 'createInviteLinkMutation' as const,
|
||||||
op: 'createInviteLink',
|
op: 'createInviteLink',
|
||||||
@@ -3244,34 +3037,6 @@ export const workspaceInvoicesQuery = {
|
|||||||
deprecations: ["'id' is deprecated: removed"],
|
deprecations: ["'id' is deprecated: removed"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const workspaceQuotaQuery = {
|
|
||||||
id: 'workspaceQuotaQuery' as const,
|
|
||||||
op: 'workspaceQuota',
|
|
||||||
query: `query workspaceQuota($id: String!) {
|
|
||||||
workspace(id: $id) {
|
|
||||||
quota {
|
|
||||||
name
|
|
||||||
blobLimit
|
|
||||||
storageQuota
|
|
||||||
usedStorageQuota
|
|
||||||
historyPeriod
|
|
||||||
memberLimit
|
|
||||||
memberCount
|
|
||||||
overcapacityMemberCount
|
|
||||||
humanReadable {
|
|
||||||
name
|
|
||||||
blobLimit
|
|
||||||
storageQuota
|
|
||||||
historyPeriod
|
|
||||||
memberLimit
|
|
||||||
memberCount
|
|
||||||
overcapacityMemberCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getWorkspaceRolePermissionsQuery = {
|
export const getWorkspaceRolePermissionsQuery = {
|
||||||
id: 'getWorkspaceRolePermissionsQuery' as const,
|
id: 'getWorkspaceRolePermissionsQuery' as const,
|
||||||
op: 'getWorkspaceRolePermissions',
|
op: 'getWorkspaceRolePermissions',
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
query notificationCount {
|
|
||||||
currentUser {
|
|
||||||
notificationCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
query getWorkspaceConfig($id: String!) {
|
|
||||||
workspace(id: $id) {
|
|
||||||
enableAi
|
|
||||||
enableSharing
|
|
||||||
enableUrlPreview
|
|
||||||
enableDocEmbedding
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
query getWorkspaceInviteLink($id: String!) {
|
|
||||||
workspace(id: $id) {
|
|
||||||
inviteLink {
|
|
||||||
link
|
|
||||||
expireTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
query workspaceQuota($id: String!) {
|
|
||||||
workspace(id: $id) {
|
|
||||||
quota {
|
|
||||||
name
|
|
||||||
blobLimit
|
|
||||||
storageQuota
|
|
||||||
usedStorageQuota
|
|
||||||
historyPeriod
|
|
||||||
memberLimit
|
|
||||||
memberCount
|
|
||||||
overcapacityMemberCount
|
|
||||||
humanReadable {
|
|
||||||
name
|
|
||||||
blobLimit
|
|
||||||
storageQuota
|
|
||||||
historyPeriod
|
|
||||||
memberLimit
|
|
||||||
memberCount
|
|
||||||
overcapacityMemberCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3785,25 +3785,6 @@ export type GenerateUserAccessTokenMutation = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ListUserAccessTokensQueryVariables = Exact<{
|
|
||||||
[key: string]: never;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type ListUserAccessTokensQuery = {
|
|
||||||
__typename?: 'Query';
|
|
||||||
currentUser: {
|
|
||||||
__typename?: 'UserType';
|
|
||||||
revealedAccessTokens: Array<{
|
|
||||||
__typename?: 'RevealedAccessToken';
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
createdAt: string;
|
|
||||||
expiresAt: string | null;
|
|
||||||
token: string;
|
|
||||||
}>;
|
|
||||||
} | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RevokeUserAccessTokenMutationVariables = Exact<{
|
export type RevokeUserAccessTokenMutationVariables = Exact<{
|
||||||
id: Scalars['String']['input'];
|
id: Scalars['String']['input'];
|
||||||
}>;
|
}>;
|
||||||
@@ -5112,19 +5093,6 @@ export type MatchFilesQuery = {
|
|||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetWorkspaceEmbeddingStatusQueryVariables = Exact<{
|
|
||||||
workspaceId: Scalars['String']['input'];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type GetWorkspaceEmbeddingStatusQuery = {
|
|
||||||
__typename?: 'Query';
|
|
||||||
queryWorkspaceEmbeddingStatus: {
|
|
||||||
__typename?: 'ContextWorkspaceEmbeddingStatus';
|
|
||||||
total: number;
|
|
||||||
embedded: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QueueWorkspaceEmbeddingMutationVariables = Exact<{
|
export type QueueWorkspaceEmbeddingMutationVariables = Exact<{
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
docId: Array<Scalars['String']['input']> | Scalars['String']['input'];
|
docId: Array<Scalars['String']['input']> | Scalars['String']['input'];
|
||||||
@@ -6271,43 +6239,6 @@ export type CredentialsRequirementsFragment = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CurrentUserProfileFragment = {
|
|
||||||
__typename?: 'UserType';
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
avatarUrl: string | null;
|
|
||||||
emailVerified: boolean;
|
|
||||||
features: Array<FeatureType>;
|
|
||||||
settings: {
|
|
||||||
__typename?: 'UserSettingsType';
|
|
||||||
receiveInvitationEmail: boolean;
|
|
||||||
receiveMentionEmail: boolean;
|
|
||||||
receiveCommentEmail: boolean;
|
|
||||||
};
|
|
||||||
quota: {
|
|
||||||
__typename?: 'UserQuotaType';
|
|
||||||
name: string;
|
|
||||||
blobLimit: number;
|
|
||||||
storageQuota: number;
|
|
||||||
historyPeriod: number;
|
|
||||||
memberLimit: number;
|
|
||||||
humanReadable: {
|
|
||||||
__typename?: 'UserQuotaHumanReadableType';
|
|
||||||
name: string;
|
|
||||||
blobLimit: string;
|
|
||||||
storageQuota: string;
|
|
||||||
historyPeriod: string;
|
|
||||||
memberLimit: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
quotaUsage: { __typename?: 'UserQuotaUsageType'; storageQuota: number };
|
|
||||||
copilot: {
|
|
||||||
__typename?: 'Copilot';
|
|
||||||
quota: { __typename?: 'CopilotQuota'; limit: number | null; used: number };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PaginatedCopilotChatsFragment = {
|
export type PaginatedCopilotChatsFragment = {
|
||||||
__typename?: 'PaginatedCopilotHistoriesType';
|
__typename?: 'PaginatedCopilotHistoriesType';
|
||||||
pageInfo: {
|
pageInfo: {
|
||||||
@@ -6388,54 +6319,6 @@ export type GetCurrentUserFeaturesQuery = {
|
|||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetCurrentUserProfileQueryVariables = Exact<{
|
|
||||||
[key: string]: never;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type GetCurrentUserProfileQuery = {
|
|
||||||
__typename?: 'Query';
|
|
||||||
currentUser: {
|
|
||||||
__typename?: 'UserType';
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
avatarUrl: string | null;
|
|
||||||
emailVerified: boolean;
|
|
||||||
features: Array<FeatureType>;
|
|
||||||
settings: {
|
|
||||||
__typename?: 'UserSettingsType';
|
|
||||||
receiveInvitationEmail: boolean;
|
|
||||||
receiveMentionEmail: boolean;
|
|
||||||
receiveCommentEmail: boolean;
|
|
||||||
};
|
|
||||||
quota: {
|
|
||||||
__typename?: 'UserQuotaType';
|
|
||||||
name: string;
|
|
||||||
blobLimit: number;
|
|
||||||
storageQuota: number;
|
|
||||||
historyPeriod: number;
|
|
||||||
memberLimit: number;
|
|
||||||
humanReadable: {
|
|
||||||
__typename?: 'UserQuotaHumanReadableType';
|
|
||||||
name: string;
|
|
||||||
blobLimit: string;
|
|
||||||
storageQuota: string;
|
|
||||||
historyPeriod: string;
|
|
||||||
memberLimit: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
quotaUsage: { __typename?: 'UserQuotaUsageType'; storageQuota: number };
|
|
||||||
copilot: {
|
|
||||||
__typename?: 'Copilot';
|
|
||||||
quota: {
|
|
||||||
__typename?: 'CopilotQuota';
|
|
||||||
limit: number | null;
|
|
||||||
used: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
} | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never }>;
|
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never }>;
|
||||||
|
|
||||||
export type GetCurrentUserQuery = {
|
export type GetCurrentUserQuery = {
|
||||||
@@ -6481,19 +6364,6 @@ export type GetDocCreatedByUpdatedByListQuery = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetDocDefaultRoleQueryVariables = Exact<{
|
|
||||||
workspaceId: Scalars['String']['input'];
|
|
||||||
docId: Scalars['String']['input'];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type GetDocDefaultRoleQuery = {
|
|
||||||
__typename?: 'Query';
|
|
||||||
workspace: {
|
|
||||||
__typename?: 'WorkspaceType';
|
|
||||||
doc: { __typename?: 'DocType'; defaultRole: DocRole };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetDocLastAccessedMembersQueryVariables = Exact<{
|
export type GetDocLastAccessedMembersQueryVariables = Exact<{
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
docId: Scalars['String']['input'];
|
docId: Scalars['String']['input'];
|
||||||
@@ -6634,32 +6504,6 @@ export type GetMemberCountByWorkspaceIdQuery = {
|
|||||||
workspace: { __typename?: 'WorkspaceType'; memberCount: number };
|
workspace: { __typename?: 'WorkspaceType'; memberCount: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetMembersByWorkspaceIdQueryVariables = Exact<{
|
|
||||||
workspaceId: Scalars['String']['input'];
|
|
||||||
skip?: InputMaybe<Scalars['Int']['input']>;
|
|
||||||
take?: InputMaybe<Scalars['Int']['input']>;
|
|
||||||
query?: InputMaybe<Scalars['String']['input']>;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type GetMembersByWorkspaceIdQuery = {
|
|
||||||
__typename?: 'Query';
|
|
||||||
workspace: {
|
|
||||||
__typename?: 'WorkspaceType';
|
|
||||||
memberCount: number;
|
|
||||||
members: Array<{
|
|
||||||
__typename?: 'InviteUserType';
|
|
||||||
id: string;
|
|
||||||
name: string | null;
|
|
||||||
email: string | null;
|
|
||||||
avatarUrl: string | null;
|
|
||||||
permission: Permission;
|
|
||||||
inviteId: string;
|
|
||||||
emailVerified: boolean | null;
|
|
||||||
status: WorkspaceMemberStatus;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OauthProvidersQueryVariables = Exact<{ [key: string]: never }>;
|
export type OauthProvidersQueryVariables = Exact<{ [key: string]: never }>;
|
||||||
|
|
||||||
export type OauthProvidersQuery = {
|
export type OauthProvidersQuery = {
|
||||||
@@ -6670,45 +6514,6 @@ export type OauthProvidersQuery = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetPageGrantedUsersListQueryVariables = Exact<{
|
|
||||||
pagination: PaginationInput;
|
|
||||||
docId: Scalars['String']['input'];
|
|
||||||
workspaceId: Scalars['String']['input'];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type GetPageGrantedUsersListQuery = {
|
|
||||||
__typename?: 'Query';
|
|
||||||
workspace: {
|
|
||||||
__typename?: 'WorkspaceType';
|
|
||||||
doc: {
|
|
||||||
__typename?: 'DocType';
|
|
||||||
grantedUsersList: {
|
|
||||||
__typename?: 'PaginatedGrantedDocUserType';
|
|
||||||
totalCount: number;
|
|
||||||
pageInfo: {
|
|
||||||
__typename?: 'PageInfo';
|
|
||||||
endCursor: string | null;
|
|
||||||
hasNextPage: boolean;
|
|
||||||
};
|
|
||||||
edges: Array<{
|
|
||||||
__typename?: 'GrantedDocUserTypeEdge';
|
|
||||||
node: {
|
|
||||||
__typename?: 'GrantedDocUserType';
|
|
||||||
role: DocRole;
|
|
||||||
user: {
|
|
||||||
__typename?: 'WorkspaceUserType';
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
avatarUrl: string | null;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetPublicUserByIdQueryVariables = Exact<{
|
export type GetPublicUserByIdQueryVariables = Exact<{
|
||||||
id: Scalars['String']['input'];
|
id: Scalars['String']['input'];
|
||||||
}>;
|
}>;
|
||||||
@@ -6805,42 +6610,6 @@ export type GetUserQuery = {
|
|||||||
| null;
|
| null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetWorkspaceInfoQueryVariables = Exact<{
|
|
||||||
workspaceId: Scalars['String']['input'];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type GetWorkspaceInfoQuery = {
|
|
||||||
__typename?: 'Query';
|
|
||||||
workspace: {
|
|
||||||
__typename?: 'WorkspaceType';
|
|
||||||
role: Permission;
|
|
||||||
team: boolean;
|
|
||||||
permissions: {
|
|
||||||
__typename?: 'WorkspacePermissions';
|
|
||||||
Workspace_Administrators_Manage: boolean;
|
|
||||||
Workspace_Blobs_List: boolean;
|
|
||||||
Workspace_Blobs_Read: boolean;
|
|
||||||
Workspace_Blobs_Write: boolean;
|
|
||||||
Workspace_Copilot: boolean;
|
|
||||||
Workspace_CreateDoc: boolean;
|
|
||||||
Workspace_Delete: boolean;
|
|
||||||
Workspace_Organize_Read: boolean;
|
|
||||||
Workspace_Payment_Manage: boolean;
|
|
||||||
Workspace_Properties_Create: boolean;
|
|
||||||
Workspace_Properties_Delete: boolean;
|
|
||||||
Workspace_Properties_Read: boolean;
|
|
||||||
Workspace_Properties_Update: boolean;
|
|
||||||
Workspace_Read: boolean;
|
|
||||||
Workspace_Settings_Read: boolean;
|
|
||||||
Workspace_Settings_Update: boolean;
|
|
||||||
Workspace_Sync: boolean;
|
|
||||||
Workspace_TransferOwner: boolean;
|
|
||||||
Workspace_Users_Manage: boolean;
|
|
||||||
Workspace_Users_Read: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetWorkspacePageByIdQueryVariables = Exact<{
|
export type GetWorkspacePageByIdQueryVariables = Exact<{
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
pageId: Scalars['String']['input'];
|
pageId: Scalars['String']['input'];
|
||||||
@@ -7268,13 +7037,6 @@ export type MentionUserMutation = {
|
|||||||
mentionUser: string;
|
mentionUser: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NotificationCountQueryVariables = Exact<{ [key: string]: never }>;
|
|
||||||
|
|
||||||
export type NotificationCountQuery = {
|
|
||||||
__typename?: 'Query';
|
|
||||||
currentUser: { __typename?: 'UserType'; notificationCount: number } | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PricesQueryVariables = Exact<{ [key: string]: never }>;
|
export type PricesQueryVariables = Exact<{ [key: string]: never }>;
|
||||||
|
|
||||||
export type PricesQuery = {
|
export type PricesQuery = {
|
||||||
@@ -7784,21 +7546,6 @@ export type WorkspaceByokSettingsQuery = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetWorkspaceConfigQueryVariables = Exact<{
|
|
||||||
id: Scalars['String']['input'];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type GetWorkspaceConfigQuery = {
|
|
||||||
__typename?: 'Query';
|
|
||||||
workspace: {
|
|
||||||
__typename?: 'WorkspaceType';
|
|
||||||
enableAi: boolean;
|
|
||||||
enableSharing: boolean;
|
|
||||||
enableUrlPreview: boolean;
|
|
||||||
enableDocEmbedding: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SetEnableAiMutationVariables = Exact<{
|
export type SetEnableAiMutationVariables = Exact<{
|
||||||
id: Scalars['ID']['input'];
|
id: Scalars['ID']['input'];
|
||||||
enableAi: Scalars['Boolean']['input'];
|
enableAi: Scalars['Boolean']['input'];
|
||||||
@@ -7864,22 +7611,6 @@ export type AcceptInviteByInviteIdMutation = {
|
|||||||
acceptInviteById: boolean;
|
acceptInviteById: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetWorkspaceInviteLinkQueryVariables = Exact<{
|
|
||||||
id: Scalars['String']['input'];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type GetWorkspaceInviteLinkQuery = {
|
|
||||||
__typename?: 'Query';
|
|
||||||
workspace: {
|
|
||||||
__typename?: 'WorkspaceType';
|
|
||||||
inviteLink: {
|
|
||||||
__typename?: 'InviteLink';
|
|
||||||
link: string;
|
|
||||||
expireTime: string;
|
|
||||||
} | null;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CreateInviteLinkMutationVariables = Exact<{
|
export type CreateInviteLinkMutationVariables = Exact<{
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
expireTime: WorkspaceInviteLinkExpireTime;
|
expireTime: WorkspaceInviteLinkExpireTime;
|
||||||
@@ -7928,38 +7659,6 @@ export type WorkspaceInvoicesQuery = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkspaceQuotaQueryVariables = Exact<{
|
|
||||||
id: Scalars['String']['input'];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type WorkspaceQuotaQuery = {
|
|
||||||
__typename?: 'Query';
|
|
||||||
workspace: {
|
|
||||||
__typename?: 'WorkspaceType';
|
|
||||||
quota: {
|
|
||||||
__typename?: 'WorkspaceQuotaType';
|
|
||||||
name: string;
|
|
||||||
blobLimit: number;
|
|
||||||
storageQuota: number;
|
|
||||||
usedStorageQuota: number;
|
|
||||||
historyPeriod: number;
|
|
||||||
memberLimit: number;
|
|
||||||
memberCount: number;
|
|
||||||
overcapacityMemberCount: number;
|
|
||||||
humanReadable: {
|
|
||||||
__typename?: 'WorkspaceQuotaHumanReadableType';
|
|
||||||
name: string;
|
|
||||||
blobLimit: string;
|
|
||||||
storageQuota: string;
|
|
||||||
historyPeriod: string;
|
|
||||||
memberLimit: string;
|
|
||||||
memberCount: string;
|
|
||||||
overcapacityMemberCount: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetWorkspaceRolePermissionsQueryVariables = Exact<{
|
export type GetWorkspaceRolePermissionsQueryVariables = Exact<{
|
||||||
id: Scalars['String']['input'];
|
id: Scalars['String']['input'];
|
||||||
}>;
|
}>;
|
||||||
@@ -8016,11 +7715,6 @@ export type GrantWorkspaceTeamMemberMutation = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Queries =
|
export type Queries =
|
||||||
| {
|
|
||||||
name: 'listUserAccessTokensQuery';
|
|
||||||
variables: ListUserAccessTokensQueryVariables;
|
|
||||||
response: ListUserAccessTokensQuery;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'adminAllSharedLinksQuery';
|
name: 'adminAllSharedLinksQuery';
|
||||||
variables: AdminAllSharedLinksQueryVariables;
|
variables: AdminAllSharedLinksQueryVariables;
|
||||||
@@ -8136,11 +7830,6 @@ export type Queries =
|
|||||||
variables: MatchFilesQueryVariables;
|
variables: MatchFilesQueryVariables;
|
||||||
response: MatchFilesQuery;
|
response: MatchFilesQuery;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: 'getWorkspaceEmbeddingStatusQuery';
|
|
||||||
variables: GetWorkspaceEmbeddingStatusQueryVariables;
|
|
||||||
response: GetWorkspaceEmbeddingStatusQuery;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'getCopilotHistoryIdsQuery';
|
name: 'getCopilotHistoryIdsQuery';
|
||||||
variables: GetCopilotHistoryIdsQueryVariables;
|
variables: GetCopilotHistoryIdsQueryVariables;
|
||||||
@@ -8226,11 +7915,6 @@ export type Queries =
|
|||||||
variables: GetCurrentUserFeaturesQueryVariables;
|
variables: GetCurrentUserFeaturesQueryVariables;
|
||||||
response: GetCurrentUserFeaturesQuery;
|
response: GetCurrentUserFeaturesQuery;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: 'getCurrentUserProfileQuery';
|
|
||||||
variables: GetCurrentUserProfileQueryVariables;
|
|
||||||
response: GetCurrentUserProfileQuery;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'getCurrentUserQuery';
|
name: 'getCurrentUserQuery';
|
||||||
variables: GetCurrentUserQueryVariables;
|
variables: GetCurrentUserQueryVariables;
|
||||||
@@ -8241,11 +7925,6 @@ export type Queries =
|
|||||||
variables: GetDocCreatedByUpdatedByListQueryVariables;
|
variables: GetDocCreatedByUpdatedByListQueryVariables;
|
||||||
response: GetDocCreatedByUpdatedByListQuery;
|
response: GetDocCreatedByUpdatedByListQuery;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: 'getDocDefaultRoleQuery';
|
|
||||||
variables: GetDocDefaultRoleQueryVariables;
|
|
||||||
response: GetDocDefaultRoleQuery;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'getDocLastAccessedMembersQuery';
|
name: 'getDocLastAccessedMembersQuery';
|
||||||
variables: GetDocLastAccessedMembersQueryVariables;
|
variables: GetDocLastAccessedMembersQueryVariables;
|
||||||
@@ -8271,21 +7950,11 @@ export type Queries =
|
|||||||
variables: GetMemberCountByWorkspaceIdQueryVariables;
|
variables: GetMemberCountByWorkspaceIdQueryVariables;
|
||||||
response: GetMemberCountByWorkspaceIdQuery;
|
response: GetMemberCountByWorkspaceIdQuery;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: 'getMembersByWorkspaceIdQuery';
|
|
||||||
variables: GetMembersByWorkspaceIdQueryVariables;
|
|
||||||
response: GetMembersByWorkspaceIdQuery;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'oauthProvidersQuery';
|
name: 'oauthProvidersQuery';
|
||||||
variables: OauthProvidersQueryVariables;
|
variables: OauthProvidersQueryVariables;
|
||||||
response: OauthProvidersQuery;
|
response: OauthProvidersQuery;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: 'getPageGrantedUsersListQuery';
|
|
||||||
variables: GetPageGrantedUsersListQueryVariables;
|
|
||||||
response: GetPageGrantedUsersListQuery;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'getPublicUserByIdQuery';
|
name: 'getPublicUserByIdQuery';
|
||||||
variables: GetPublicUserByIdQueryVariables;
|
variables: GetPublicUserByIdQueryVariables;
|
||||||
@@ -8311,11 +7980,6 @@ export type Queries =
|
|||||||
variables: GetUserQueryVariables;
|
variables: GetUserQueryVariables;
|
||||||
response: GetUserQuery;
|
response: GetUserQuery;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: 'getWorkspaceInfoQuery';
|
|
||||||
variables: GetWorkspaceInfoQueryVariables;
|
|
||||||
response: GetWorkspaceInfoQuery;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'getWorkspacePageByIdQuery';
|
name: 'getWorkspacePageByIdQuery';
|
||||||
variables: GetWorkspacePageByIdQueryVariables;
|
variables: GetWorkspacePageByIdQueryVariables;
|
||||||
@@ -8391,11 +8055,6 @@ export type Queries =
|
|||||||
variables: ListNotificationsQueryVariables;
|
variables: ListNotificationsQueryVariables;
|
||||||
response: ListNotificationsQuery;
|
response: ListNotificationsQuery;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: 'notificationCountQuery';
|
|
||||||
variables: NotificationCountQueryVariables;
|
|
||||||
response: NotificationCountQuery;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'pricesQuery';
|
name: 'pricesQuery';
|
||||||
variables: PricesQueryVariables;
|
variables: PricesQueryVariables;
|
||||||
@@ -8426,26 +8085,11 @@ export type Queries =
|
|||||||
variables: WorkspaceByokSettingsQueryVariables;
|
variables: WorkspaceByokSettingsQueryVariables;
|
||||||
response: WorkspaceByokSettingsQuery;
|
response: WorkspaceByokSettingsQuery;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: 'getWorkspaceConfigQuery';
|
|
||||||
variables: GetWorkspaceConfigQueryVariables;
|
|
||||||
response: GetWorkspaceConfigQuery;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
name: 'getWorkspaceInviteLinkQuery';
|
|
||||||
variables: GetWorkspaceInviteLinkQueryVariables;
|
|
||||||
response: GetWorkspaceInviteLinkQuery;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'workspaceInvoicesQuery';
|
name: 'workspaceInvoicesQuery';
|
||||||
variables: WorkspaceInvoicesQueryVariables;
|
variables: WorkspaceInvoicesQueryVariables;
|
||||||
response: WorkspaceInvoicesQuery;
|
response: WorkspaceInvoicesQuery;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
name: 'workspaceQuotaQuery';
|
|
||||||
variables: WorkspaceQuotaQueryVariables;
|
|
||||||
response: WorkspaceQuotaQuery;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
name: 'getWorkspaceRolePermissionsQuery';
|
name: 'getWorkspaceRolePermissionsQuery';
|
||||||
variables: GetWorkspaceRolePermissionsQueryVariables;
|
variables: GetWorkspaceRolePermissionsQueryVariables;
|
||||||
|
|||||||
@@ -235,11 +235,19 @@ class SocketManager {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.socket.disconnect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SOCKET_MANAGER_CACHE = new Map<string, SocketManager>();
|
const SOCKET_MANAGER_CACHE = new Map<string, SocketManager>();
|
||||||
|
function getSocketManagerKey(endpoint: string, isSelfHosted: boolean) {
|
||||||
|
return `${endpoint}:${isSelfHosted ? 'selfhosted' : 'cloud'}`;
|
||||||
|
}
|
||||||
|
|
||||||
function getSocketManager(endpoint: string, isSelfHosted: boolean) {
|
function getSocketManager(endpoint: string, isSelfHosted: boolean) {
|
||||||
const key = `${endpoint}:${isSelfHosted ? 'selfhosted' : 'cloud'}`;
|
const key = getSocketManagerKey(endpoint, isSelfHosted);
|
||||||
let manager = SOCKET_MANAGER_CACHE.get(key);
|
let manager = SOCKET_MANAGER_CACHE.get(key);
|
||||||
if (!manager) {
|
if (!manager) {
|
||||||
manager = new SocketManager(endpoint, isSelfHosted);
|
manager = new SocketManager(endpoint, isSelfHosted);
|
||||||
@@ -252,6 +260,12 @@ export class SocketConnection extends AutoReconnectConnection<{
|
|||||||
socket: Socket;
|
socket: Socket;
|
||||||
disconnect: () => void;
|
disconnect: () => void;
|
||||||
}> {
|
}> {
|
||||||
|
static resetSharedConnection(endpoint: string, isSelfHosted: boolean) {
|
||||||
|
SOCKET_MANAGER_CACHE.get(
|
||||||
|
getSocketManagerKey(endpoint, isSelfHosted)
|
||||||
|
)?.reset();
|
||||||
|
}
|
||||||
|
|
||||||
manager = getSocketManager(this.endpoint, this.isSelfHosted);
|
manager = getSocketManager(this.endpoint, this.isSelfHosted);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -41,10 +41,15 @@ class FakeSocket {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { resetSharedConnection } = vi.hoisted(() => ({
|
||||||
|
resetSharedConnection: vi.fn(),
|
||||||
|
}));
|
||||||
const socket = new FakeSocket();
|
const socket = new FakeSocket();
|
||||||
|
|
||||||
vi.mock('../../impls/cloud/socket', () => ({
|
vi.mock('../../impls/cloud/socket', () => ({
|
||||||
SocketConnection: class {
|
SocketConnection: class {
|
||||||
|
static resetSharedConnection = resetSharedConnection;
|
||||||
|
|
||||||
readonly inner = { socket };
|
readonly inner = { socket };
|
||||||
status = 'connected';
|
status = 'connected';
|
||||||
readonly maybeConnection = { socket };
|
readonly maybeConnection = { socket };
|
||||||
@@ -68,6 +73,7 @@ beforeEach(() => {
|
|||||||
socket.nextSubscriptionId = 0;
|
socket.nextSubscriptionId = 0;
|
||||||
socket.connected = true;
|
socket.connected = true;
|
||||||
socket.disconnected = false;
|
socket.disconnected = false;
|
||||||
|
resetSharedConnection.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getRealtimeInputKey is deterministic for realtime subscription inputs', () => {
|
test('getRealtimeInputKey is deterministic for realtime subscription inputs', () => {
|
||||||
@@ -124,6 +130,33 @@ test('request rejects server ack error', async () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('user profile request can bootstrap without authenticated context', async () => {
|
||||||
|
const manager = new RealtimeManager();
|
||||||
|
manager.setContext({
|
||||||
|
endpoint: 'http://server',
|
||||||
|
isSelfHosted: false,
|
||||||
|
authenticated: false,
|
||||||
|
});
|
||||||
|
socket.nextRequestAck = { data: { user: null } };
|
||||||
|
|
||||||
|
await expect(manager.request('user.profile.get', {})).resolves.toEqual({
|
||||||
|
user: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-bootstrap request still requires authenticated context', async () => {
|
||||||
|
const manager = new RealtimeManager();
|
||||||
|
manager.setContext({
|
||||||
|
endpoint: 'http://server',
|
||||||
|
isSelfHosted: false,
|
||||||
|
authenticated: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(manager.request('notification.count.get', {})).rejects.toThrow(
|
||||||
|
'Realtime is not authenticated'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('request rejects when aborted', async () => {
|
test('request rejects when aborted', async () => {
|
||||||
const manager = new RealtimeManager();
|
const manager = new RealtimeManager();
|
||||||
manager.setContext({
|
manager.setContext({
|
||||||
@@ -203,7 +236,7 @@ test('unsubscribe leaves server room and clears status', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('context switch disconnects socket and completes subscriptions', async () => {
|
test('context switch disconnects socket and keeps subscriptions for reauth', async () => {
|
||||||
const manager = new RealtimeManager();
|
const manager = new RealtimeManager();
|
||||||
manager.setContext({
|
manager.setContext({
|
||||||
endpoint: 'http://server',
|
endpoint: 'http://server',
|
||||||
@@ -223,11 +256,95 @@ test('context switch disconnects socket and completes subscriptions', async () =
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(socket.disconnected).toBe(true);
|
expect(socket.disconnected).toBe(true);
|
||||||
expect(completed).toHaveBeenCalled();
|
expect(completed).not.toHaveBeenCalled();
|
||||||
expect(manager.getStatus()).toMatchObject({
|
expect(manager.getStatus()).toMatchObject({
|
||||||
endpoint: 'http://other-server',
|
endpoint: 'http://other-server',
|
||||||
connected: false,
|
connected: false,
|
||||||
subscriptions: 0,
|
subscriptions: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('auth context switch resets shared socket connection', () => {
|
||||||
|
const manager = new RealtimeManager();
|
||||||
|
manager.setContext({
|
||||||
|
endpoint: 'http://server',
|
||||||
|
isSelfHosted: false,
|
||||||
|
authenticated: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
manager.setContext({
|
||||||
|
endpoint: 'http://server',
|
||||||
|
isSelfHosted: false,
|
||||||
|
authenticated: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resetSharedConnection).toHaveBeenCalledWith('http://server', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('context switch resubscribes existing subscriptions on next connect', async () => {
|
||||||
|
const manager = new RealtimeManager();
|
||||||
|
manager.setContext({
|
||||||
|
endpoint: 'http://server',
|
||||||
|
isSelfHosted: false,
|
||||||
|
authenticated: true,
|
||||||
|
});
|
||||||
|
const received: unknown[] = [];
|
||||||
|
const subscription = manager
|
||||||
|
.subscribe('notification.count.changed', {})
|
||||||
|
.subscribe(event => received.push(event));
|
||||||
|
await vi.waitFor(() => expect(received).toEqual([{ type: 'ready' }]));
|
||||||
|
|
||||||
|
manager.setContext({
|
||||||
|
endpoint: 'http://other-server',
|
||||||
|
isSelfHosted: false,
|
||||||
|
authenticated: true,
|
||||||
|
});
|
||||||
|
await manager.request('notification.count.get', {});
|
||||||
|
|
||||||
|
await vi.waitFor(() =>
|
||||||
|
expect(
|
||||||
|
socket.emitted.filter(item => item.event === 'realtime:subscribe')
|
||||||
|
).toHaveLength(2)
|
||||||
|
);
|
||||||
|
expect(received).toEqual([{ type: 'ready' }, { type: 'ready' }]);
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unsubscribe uses resubscribed server subscription id', async () => {
|
||||||
|
const manager = new RealtimeManager();
|
||||||
|
manager.setContext({
|
||||||
|
endpoint: 'http://server',
|
||||||
|
isSelfHosted: false,
|
||||||
|
authenticated: true,
|
||||||
|
});
|
||||||
|
const subscription = manager
|
||||||
|
.subscribe('notification.count.changed', {})
|
||||||
|
.subscribe();
|
||||||
|
await vi.waitFor(() => expect(manager.getStatus().subscriptions).toBe(1));
|
||||||
|
|
||||||
|
manager.setContext({
|
||||||
|
endpoint: 'http://other-server',
|
||||||
|
isSelfHosted: false,
|
||||||
|
authenticated: true,
|
||||||
|
});
|
||||||
|
await manager.request('notification.count.get', {});
|
||||||
|
await vi.waitFor(() =>
|
||||||
|
expect(
|
||||||
|
socket.emitted.filter(item => item.event === 'realtime:subscribe')
|
||||||
|
).toHaveLength(2)
|
||||||
|
);
|
||||||
|
|
||||||
|
subscription.unsubscribe();
|
||||||
|
|
||||||
|
expect(manager.getStatus().subscriptions).toBe(0);
|
||||||
|
expect(socket.emitted.at(-1)).toEqual({
|
||||||
|
event: 'realtime:unsubscribe',
|
||||||
|
payload: {
|
||||||
|
subscriptionId: 'sub-2',
|
||||||
|
topic: 'notification.count.changed',
|
||||||
|
input: {},
|
||||||
|
clientVersion: 'test',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export class RealtimeManager {
|
|||||||
private socketConnection?: SocketConnection;
|
private socketConnection?: SocketConnection;
|
||||||
private socketKey?: string;
|
private socketKey?: string;
|
||||||
private lastError?: { name: string; message: string };
|
private lastError?: { name: string; message: string };
|
||||||
|
private subscriptionsNeedResubscribe = false;
|
||||||
private readonly subscriptions = new Map<
|
private readonly subscriptions = new Map<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
@@ -44,21 +45,32 @@ export class RealtimeManager {
|
|||||||
input: RealtimeTopicInputOf<RealtimeTopicName>;
|
input: RealtimeTopicInputOf<RealtimeTopicName>;
|
||||||
inputKey: string;
|
inputKey: string;
|
||||||
subject$: Subject<RealtimeEvent | RealtimeSubscriptionReady>;
|
subject$: Subject<RealtimeEvent | RealtimeSubscriptionReady>;
|
||||||
|
onResubscribed: (subscriptionId: string) => void;
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
setContext(context: RealtimeContext) {
|
setContext(context: RealtimeContext) {
|
||||||
const nextContext = { ...context };
|
const nextContext = { ...context };
|
||||||
|
const previousContext = this.context;
|
||||||
const changed =
|
const changed =
|
||||||
!this.context ||
|
!previousContext ||
|
||||||
this.context.endpoint !== nextContext.endpoint ||
|
previousContext.endpoint !== nextContext.endpoint ||
|
||||||
this.context.isSelfHosted !== nextContext.isSelfHosted ||
|
previousContext.isSelfHosted !== nextContext.isSelfHosted ||
|
||||||
this.context.authenticated !== nextContext.authenticated;
|
previousContext.authenticated !== nextContext.authenticated;
|
||||||
|
|
||||||
this.context = nextContext;
|
this.context = nextContext;
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
this.resetConnection();
|
this.resetConnection();
|
||||||
|
if (
|
||||||
|
previousContext &&
|
||||||
|
previousContext.authenticated !== nextContext.authenticated
|
||||||
|
) {
|
||||||
|
SocketConnection.resetSharedConnection(
|
||||||
|
previousContext.endpoint,
|
||||||
|
previousContext.isSelfHosted
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +79,7 @@ export class RealtimeManager {
|
|||||||
input: RealtimeRequestInputOf<Op>,
|
input: RealtimeRequestInputOf<Op>,
|
||||||
options?: { timeoutMs?: number; signal?: AbortSignal }
|
options?: { timeoutMs?: number; signal?: AbortSignal }
|
||||||
): Promise<RealtimeRequestOutputOf<Op>> {
|
): Promise<RealtimeRequestOutputOf<Op>> {
|
||||||
const socket = await this.connect();
|
const socket = await this.connect(op === 'user.profile.get');
|
||||||
const timeoutMs = options?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT;
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT;
|
||||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||||
let abortHandler: (() => void) | undefined;
|
let abortHandler: (() => void) | undefined;
|
||||||
@@ -154,6 +166,9 @@ export class RealtimeManager {
|
|||||||
input,
|
input,
|
||||||
inputKey: getRealtimeInputKey(input),
|
inputKey: getRealtimeInputKey(input),
|
||||||
subject$,
|
subject$,
|
||||||
|
onResubscribed: nextSubscriptionId => {
|
||||||
|
subscriptionId = nextSubscriptionId;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
subscriber.next({
|
subscriber.next({
|
||||||
type: 'ready',
|
type: 'ready',
|
||||||
@@ -167,7 +182,7 @@ export class RealtimeManager {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: error => subscriber.error(error),
|
error: error => subscriber.error(error),
|
||||||
complete: () => subscriber.complete(),
|
complete: () => {},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.lastError = normalizeError(error);
|
this.lastError = normalizeError(error);
|
||||||
@@ -209,8 +224,11 @@ export class RealtimeManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async connect() {
|
private async connect(allowUnauthenticated = false) {
|
||||||
if (!this.context?.endpoint || !this.context.authenticated) {
|
if (
|
||||||
|
!this.context?.endpoint ||
|
||||||
|
(!this.context.authenticated && !allowUnauthenticated)
|
||||||
|
) {
|
||||||
const error = new Error('Realtime is not authenticated');
|
const error = new Error('Realtime is not authenticated');
|
||||||
error.name = 'RealtimeUnauthenticated';
|
error.name = 'RealtimeUnauthenticated';
|
||||||
throw error;
|
throw error;
|
||||||
@@ -232,6 +250,9 @@ export class RealtimeManager {
|
|||||||
this.socketConnection.inner.socket.on('realtime:event', this.handleEvent);
|
this.socketConnection.inner.socket.on('realtime:event', this.handleEvent);
|
||||||
this.socketConnection.inner.socket.off('connect', this.handleReconnect);
|
this.socketConnection.inner.socket.off('connect', this.handleReconnect);
|
||||||
this.socketConnection.inner.socket.on('connect', this.handleReconnect);
|
this.socketConnection.inner.socket.on('connect', this.handleReconnect);
|
||||||
|
if (this.subscriptionsNeedResubscribe && this.context.authenticated) {
|
||||||
|
await this.resubscribeAll();
|
||||||
|
}
|
||||||
return this.socketConnection.inner.socket;
|
return this.socketConnection.inner.socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,6 +293,7 @@ export class RealtimeManager {
|
|||||||
|
|
||||||
this.subscriptions.delete(subscriptionId);
|
this.subscriptions.delete(subscriptionId);
|
||||||
this.subscriptions.set(ack.data.subscriptionId, subscription);
|
this.subscriptions.set(ack.data.subscriptionId, subscription);
|
||||||
|
subscription.onResubscribed(ack.data.subscriptionId);
|
||||||
subscription.subject$.next({
|
subscription.subject$.next({
|
||||||
type: 'ready',
|
type: 'ready',
|
||||||
});
|
});
|
||||||
@@ -281,6 +303,7 @@ export class RealtimeManager {
|
|||||||
subscription.subject$.error(error);
|
subscription.subject$.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.subscriptionsNeedResubscribe = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetConnection() {
|
private resetConnection() {
|
||||||
@@ -297,9 +320,6 @@ export class RealtimeManager {
|
|||||||
}
|
}
|
||||||
this.socketConnection = undefined;
|
this.socketConnection = undefined;
|
||||||
this.socketKey = undefined;
|
this.socketKey = undefined;
|
||||||
for (const subscription of this.subscriptions.values()) {
|
this.subscriptionsNeedResubscribe = this.subscriptions.size > 0;
|
||||||
subscription.subject$.complete();
|
|
||||||
}
|
|
||||||
this.subscriptions.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,50 @@
|
|||||||
import type { CommentChangeObjectType } from '@affine/graphql';
|
|
||||||
|
|
||||||
export type RealtimeRequestName = keyof RealtimeRequestMap;
|
export type RealtimeRequestName = keyof RealtimeRequestMap;
|
||||||
export type RealtimeTopicName = keyof RealtimeTopicMap;
|
export type RealtimeTopicName = keyof RealtimeTopicMap;
|
||||||
|
|
||||||
|
export const WORKSPACE_MEMBERS_REQUEST_TAKE_MAX = 100;
|
||||||
|
|
||||||
export interface RealtimeRequestMap {
|
export interface RealtimeRequestMap {
|
||||||
|
'workspace.access.get': {
|
||||||
|
input: { workspaceId: string };
|
||||||
|
output: { access: WorkspaceAccessSnapshot };
|
||||||
|
};
|
||||||
|
'workspace.config.get': {
|
||||||
|
input: { workspaceId: string };
|
||||||
|
output: { config: WorkspaceConfigSnapshot };
|
||||||
|
};
|
||||||
|
'workspace.members.get': {
|
||||||
|
input: {
|
||||||
|
workspaceId: string;
|
||||||
|
skip?: number;
|
||||||
|
take?: number;
|
||||||
|
query?: string;
|
||||||
|
};
|
||||||
|
output: { members: WorkspaceMemberSnapshot[]; memberCount: number };
|
||||||
|
};
|
||||||
|
'workspace.invite-link.get': {
|
||||||
|
input: { workspaceId: string };
|
||||||
|
output: { inviteLink: WorkspaceInviteLinkSnapshot | null };
|
||||||
|
};
|
||||||
|
'doc.share-state.get': {
|
||||||
|
input: { workspaceId: string; docId: string };
|
||||||
|
output: { state: DocShareStateSnapshot | null };
|
||||||
|
};
|
||||||
|
'doc.grants.get': {
|
||||||
|
input: { workspaceId: string; docId: string; pagination: PaginationInput };
|
||||||
|
output: PaginatedDocGrantedUsersSnapshot;
|
||||||
|
};
|
||||||
|
'user.profile.get': {
|
||||||
|
input: Record<string, never>;
|
||||||
|
output: { user: CurrentUserProfileSnapshot | null };
|
||||||
|
};
|
||||||
|
'user.settings.get': {
|
||||||
|
input: Record<string, never>;
|
||||||
|
output: { settings: UserSettingsSnapshot };
|
||||||
|
};
|
||||||
|
'user.access-tokens.get': {
|
||||||
|
input: Record<string, never>;
|
||||||
|
output: { tokens: AccessTokenSnapshot[] };
|
||||||
|
};
|
||||||
'notification.count.get': {
|
'notification.count.get': {
|
||||||
input: Record<string, never>;
|
input: Record<string, never>;
|
||||||
output: { count: number };
|
output: { count: number };
|
||||||
@@ -16,7 +57,7 @@ export interface RealtimeRequestMap {
|
|||||||
first?: number;
|
first?: number;
|
||||||
};
|
};
|
||||||
output: {
|
output: {
|
||||||
changes: CommentChangeObjectType[];
|
changes: CommentChangeSnapshot[];
|
||||||
startCursor: string;
|
startCursor: string;
|
||||||
endCursor: string;
|
endCursor: string;
|
||||||
hasNextPage: boolean;
|
hasNextPage: boolean;
|
||||||
@@ -44,6 +85,117 @@ export interface RealtimeRequestMap {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkspaceRoleSnapshot =
|
||||||
|
| 'Owner'
|
||||||
|
| 'Admin'
|
||||||
|
| 'Collaborator'
|
||||||
|
| 'External'
|
||||||
|
| string;
|
||||||
|
|
||||||
|
export type DocRoleSnapshot =
|
||||||
|
| 'Owner'
|
||||||
|
| 'Manager'
|
||||||
|
| 'Editor'
|
||||||
|
| 'Commenter'
|
||||||
|
| 'Reader'
|
||||||
|
| 'External'
|
||||||
|
| string;
|
||||||
|
|
||||||
|
export type PublicDocModeSnapshot = 'Page' | 'Edgeless' | string;
|
||||||
|
|
||||||
|
export interface WorkspaceAccessSnapshot {
|
||||||
|
role: WorkspaceRoleSnapshot;
|
||||||
|
permissions: Record<string, boolean>;
|
||||||
|
team: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkspaceConfigSnapshot {
|
||||||
|
enableAi: boolean;
|
||||||
|
enableSharing: boolean;
|
||||||
|
enableUrlPreview: boolean;
|
||||||
|
enableDocEmbedding: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkspaceMemberSnapshot {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
permission: WorkspaceRoleSnapshot;
|
||||||
|
role: WorkspaceRoleSnapshot;
|
||||||
|
inviteId: string;
|
||||||
|
emailVerified: boolean | null;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkspaceInviteLinkSnapshot {
|
||||||
|
link: string;
|
||||||
|
expireTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocShareStateSnapshot {
|
||||||
|
public: boolean;
|
||||||
|
mode: PublicDocModeSnapshot;
|
||||||
|
defaultRole: DocRoleSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationInput {
|
||||||
|
first: number;
|
||||||
|
offset?: number;
|
||||||
|
after?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocGrantedUserSnapshot {
|
||||||
|
role: DocRoleSnapshot;
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedDocGrantedUsersSnapshot {
|
||||||
|
totalCount: number;
|
||||||
|
pageInfo: {
|
||||||
|
endCursor: string | null;
|
||||||
|
hasNextPage: boolean;
|
||||||
|
};
|
||||||
|
edges: { node: DocGrantedUserSnapshot }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurrentUserProfileSnapshot {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
emailVerified: boolean;
|
||||||
|
hasPassword: boolean | null;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
features?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserSettingsSnapshot {
|
||||||
|
receiveInvitationEmail: boolean;
|
||||||
|
receiveMentionEmail: boolean;
|
||||||
|
receiveCommentEmail: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccessTokenSnapshot {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
createdAt: string;
|
||||||
|
expiresAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommentChangeActionSnapshot = 'update' | 'delete';
|
||||||
|
|
||||||
|
export interface CommentChangeSnapshot {
|
||||||
|
id: string;
|
||||||
|
action: CommentChangeActionSnapshot;
|
||||||
|
item: object;
|
||||||
|
commentId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserQuotaStateSnapshot {
|
export interface UserQuotaStateSnapshot {
|
||||||
userId: string;
|
userId: string;
|
||||||
plan: string;
|
plan: string;
|
||||||
@@ -101,6 +253,42 @@ export type WorkspaceEmbeddingProgressReason =
|
|||||||
| 'resync';
|
| 'resync';
|
||||||
|
|
||||||
export interface RealtimeTopicMap {
|
export interface RealtimeTopicMap {
|
||||||
|
'workspace.access.changed': {
|
||||||
|
input: { workspaceId: string };
|
||||||
|
event: { changed: true; reason: string };
|
||||||
|
};
|
||||||
|
'workspace.config.changed': {
|
||||||
|
input: { workspaceId: string };
|
||||||
|
event: { changed: true; reason: string };
|
||||||
|
};
|
||||||
|
'workspace.members.changed': {
|
||||||
|
input: { workspaceId: string };
|
||||||
|
event: { changed: true; reason: string };
|
||||||
|
};
|
||||||
|
'workspace.invite-link.changed': {
|
||||||
|
input: { workspaceId: string };
|
||||||
|
event: { changed: true; reason: string };
|
||||||
|
};
|
||||||
|
'doc.share-state.changed': {
|
||||||
|
input: { workspaceId: string; docId: string };
|
||||||
|
event: { changed: true; reason: string };
|
||||||
|
};
|
||||||
|
'doc.grants.changed': {
|
||||||
|
input: { workspaceId: string; docId: string };
|
||||||
|
event: { changed: true; reason: string };
|
||||||
|
};
|
||||||
|
'user.profile.changed': {
|
||||||
|
input: Record<string, never>;
|
||||||
|
event: { changed: true; reason: string };
|
||||||
|
};
|
||||||
|
'user.settings.changed': {
|
||||||
|
input: Record<string, never>;
|
||||||
|
event: { changed: true; reason: string };
|
||||||
|
};
|
||||||
|
'user.access-tokens.changed': {
|
||||||
|
input: Record<string, never>;
|
||||||
|
event: { changed: true; reason: string };
|
||||||
|
};
|
||||||
'notification.count.changed': {
|
'notification.count.changed': {
|
||||||
input: Record<string, never>;
|
input: Record<string, never>;
|
||||||
event: {
|
event: {
|
||||||
|
|||||||
-191
@@ -1,191 +0,0 @@
|
|||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
@_exported import ApolloAPI
|
|
||||||
|
|
||||||
public struct CurrentUserProfile: AffineGraphQL.SelectionSet, Fragment {
|
|
||||||
public static var fragmentDefinition: StaticString {
|
|
||||||
#"fragment CurrentUserProfile on UserType { __typename id name email avatarUrl emailVerified features settings { __typename receiveInvitationEmail receiveMentionEmail receiveCommentEmail } quota { __typename name blobLimit storageQuota historyPeriod memberLimit humanReadable { __typename name blobLimit storageQuota historyPeriod memberLimit } } quotaUsage { __typename storageQuota } copilot { __typename quota { __typename limit used } } }"#
|
|
||||||
}
|
|
||||||
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.UserType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("id", AffineGraphQL.ID.self),
|
|
||||||
.field("name", String.self),
|
|
||||||
.field("email", String.self),
|
|
||||||
.field("avatarUrl", String?.self),
|
|
||||||
.field("emailVerified", Bool.self),
|
|
||||||
.field("features", [GraphQLEnum<AffineGraphQL.FeatureType>].self),
|
|
||||||
.field("settings", Settings.self),
|
|
||||||
.field("quota", Quota.self),
|
|
||||||
.field("quotaUsage", QuotaUsage.self),
|
|
||||||
.field("copilot", Copilot.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
CurrentUserProfile.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var id: AffineGraphQL.ID { __data["id"] }
|
|
||||||
/// User name
|
|
||||||
public var name: String { __data["name"] }
|
|
||||||
/// User email
|
|
||||||
public var email: String { __data["email"] }
|
|
||||||
/// User avatar url
|
|
||||||
public var avatarUrl: String? { __data["avatarUrl"] }
|
|
||||||
/// User email verified
|
|
||||||
public var emailVerified: Bool { __data["emailVerified"] }
|
|
||||||
/// Enabled features of a user
|
|
||||||
public var features: [GraphQLEnum<AffineGraphQL.FeatureType>] { __data["features"] }
|
|
||||||
/// Get user settings
|
|
||||||
public var settings: Settings { __data["settings"] }
|
|
||||||
public var quota: Quota { __data["quota"] }
|
|
||||||
public var quotaUsage: QuotaUsage { __data["quotaUsage"] }
|
|
||||||
public var copilot: Copilot { __data["copilot"] }
|
|
||||||
|
|
||||||
/// Settings
|
|
||||||
///
|
|
||||||
/// Parent Type: `UserSettingsType`
|
|
||||||
public struct Settings: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.UserSettingsType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("receiveInvitationEmail", Bool.self),
|
|
||||||
.field("receiveMentionEmail", Bool.self),
|
|
||||||
.field("receiveCommentEmail", Bool.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
CurrentUserProfile.Settings.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// Receive invitation email
|
|
||||||
public var receiveInvitationEmail: Bool { __data["receiveInvitationEmail"] }
|
|
||||||
/// Receive mention email
|
|
||||||
public var receiveMentionEmail: Bool { __data["receiveMentionEmail"] }
|
|
||||||
/// Receive comment email
|
|
||||||
public var receiveCommentEmail: Bool { __data["receiveCommentEmail"] }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Quota
|
|
||||||
///
|
|
||||||
/// Parent Type: `UserQuotaType`
|
|
||||||
public struct Quota: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.UserQuotaType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("name", String.self),
|
|
||||||
.field("blobLimit", AffineGraphQL.SafeInt.self),
|
|
||||||
.field("storageQuota", AffineGraphQL.SafeInt.self),
|
|
||||||
.field("historyPeriod", AffineGraphQL.SafeInt.self),
|
|
||||||
.field("memberLimit", Int.self),
|
|
||||||
.field("humanReadable", HumanReadable.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
CurrentUserProfile.Quota.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var name: String { __data["name"] }
|
|
||||||
public var blobLimit: AffineGraphQL.SafeInt { __data["blobLimit"] }
|
|
||||||
public var storageQuota: AffineGraphQL.SafeInt { __data["storageQuota"] }
|
|
||||||
public var historyPeriod: AffineGraphQL.SafeInt { __data["historyPeriod"] }
|
|
||||||
public var memberLimit: Int { __data["memberLimit"] }
|
|
||||||
public var humanReadable: HumanReadable { __data["humanReadable"] }
|
|
||||||
|
|
||||||
/// Quota.HumanReadable
|
|
||||||
///
|
|
||||||
/// Parent Type: `UserQuotaHumanReadableType`
|
|
||||||
public struct HumanReadable: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.UserQuotaHumanReadableType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("name", String.self),
|
|
||||||
.field("blobLimit", String.self),
|
|
||||||
.field("storageQuota", String.self),
|
|
||||||
.field("historyPeriod", String.self),
|
|
||||||
.field("memberLimit", String.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
CurrentUserProfile.Quota.HumanReadable.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var name: String { __data["name"] }
|
|
||||||
public var blobLimit: String { __data["blobLimit"] }
|
|
||||||
public var storageQuota: String { __data["storageQuota"] }
|
|
||||||
public var historyPeriod: String { __data["historyPeriod"] }
|
|
||||||
public var memberLimit: String { __data["memberLimit"] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// QuotaUsage
|
|
||||||
///
|
|
||||||
/// Parent Type: `UserQuotaUsageType`
|
|
||||||
public struct QuotaUsage: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.UserQuotaUsageType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("storageQuota", AffineGraphQL.SafeInt.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
CurrentUserProfile.QuotaUsage.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
@available(*, deprecated, message: "use `UserQuotaType[\'usedStorageQuota\']` instead")
|
|
||||||
public var storageQuota: AffineGraphQL.SafeInt { __data["storageQuota"] }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Copilot
|
|
||||||
///
|
|
||||||
/// Parent Type: `Copilot`
|
|
||||||
public struct Copilot: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Copilot }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("quota", Quota.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
CurrentUserProfile.Copilot.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// Get the quota of the user in the workspace
|
|
||||||
public var quota: Quota { __data["quota"] }
|
|
||||||
|
|
||||||
/// Copilot.Quota
|
|
||||||
///
|
|
||||||
/// Parent Type: `CopilotQuota`
|
|
||||||
public struct Quota: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.CopilotQuota }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("limit", AffineGraphQL.SafeInt?.self),
|
|
||||||
.field("used", AffineGraphQL.SafeInt.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
CurrentUserProfile.Copilot.Quota.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var limit: AffineGraphQL.SafeInt? { __data["limit"] }
|
|
||||||
public var used: AffineGraphQL.SafeInt { __data["used"] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-81
@@ -1,81 +0,0 @@
|
|||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
@_exported import ApolloAPI
|
|
||||||
|
|
||||||
public class GetCurrentUserProfileQuery: GraphQLQuery {
|
|
||||||
public static let operationName: String = "getCurrentUserProfile"
|
|
||||||
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
|
||||||
definition: .init(
|
|
||||||
#"query getCurrentUserProfile { currentUser { __typename ...CurrentUserProfile } }"#,
|
|
||||||
fragments: [CurrentUserProfile.self]
|
|
||||||
))
|
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
public struct Data: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Query }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("currentUser", CurrentUser?.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetCurrentUserProfileQuery.Data.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// Get current user
|
|
||||||
public var currentUser: CurrentUser? { __data["currentUser"] }
|
|
||||||
|
|
||||||
/// CurrentUser
|
|
||||||
///
|
|
||||||
/// Parent Type: `UserType`
|
|
||||||
public struct CurrentUser: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.UserType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.fragment(CurrentUserProfile.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetCurrentUserProfileQuery.Data.CurrentUser.self,
|
|
||||||
CurrentUserProfile.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var id: AffineGraphQL.ID { __data["id"] }
|
|
||||||
/// User name
|
|
||||||
public var name: String { __data["name"] }
|
|
||||||
/// User email
|
|
||||||
public var email: String { __data["email"] }
|
|
||||||
/// User avatar url
|
|
||||||
public var avatarUrl: String? { __data["avatarUrl"] }
|
|
||||||
/// User email verified
|
|
||||||
public var emailVerified: Bool { __data["emailVerified"] }
|
|
||||||
/// Enabled features of a user
|
|
||||||
public var features: [GraphQLEnum<AffineGraphQL.FeatureType>] { __data["features"] }
|
|
||||||
/// Get user settings
|
|
||||||
public var settings: Settings { __data["settings"] }
|
|
||||||
public var quota: Quota { __data["quota"] }
|
|
||||||
public var quotaUsage: QuotaUsage { __data["quotaUsage"] }
|
|
||||||
public var copilot: Copilot { __data["copilot"] }
|
|
||||||
|
|
||||||
public struct Fragments: FragmentContainer {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public var currentUserProfile: CurrentUserProfile { _toFragment() }
|
|
||||||
}
|
|
||||||
|
|
||||||
public typealias Settings = CurrentUserProfile.Settings
|
|
||||||
|
|
||||||
public typealias Quota = CurrentUserProfile.Quota
|
|
||||||
|
|
||||||
public typealias QuotaUsage = CurrentUserProfile.QuotaUsage
|
|
||||||
|
|
||||||
public typealias Copilot = CurrentUserProfile.Copilot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-83
@@ -1,83 +0,0 @@
|
|||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
@_exported import ApolloAPI
|
|
||||||
|
|
||||||
public class GetDocDefaultRoleQuery: GraphQLQuery {
|
|
||||||
public static let operationName: String = "getDocDefaultRole"
|
|
||||||
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
|
||||||
definition: .init(
|
|
||||||
#"query getDocDefaultRole($workspaceId: String!, $docId: String!) { workspace(id: $workspaceId) { __typename doc(docId: $docId) { __typename defaultRole } } }"#
|
|
||||||
))
|
|
||||||
|
|
||||||
public var workspaceId: String
|
|
||||||
public var docId: String
|
|
||||||
|
|
||||||
public init(
|
|
||||||
workspaceId: String,
|
|
||||||
docId: String
|
|
||||||
) {
|
|
||||||
self.workspaceId = workspaceId
|
|
||||||
self.docId = docId
|
|
||||||
}
|
|
||||||
|
|
||||||
public var __variables: Variables? { [
|
|
||||||
"workspaceId": workspaceId,
|
|
||||||
"docId": docId
|
|
||||||
] }
|
|
||||||
|
|
||||||
public struct Data: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Query }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("workspace", Workspace.self, arguments: ["id": .variable("workspaceId")]),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetDocDefaultRoleQuery.Data.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// Get workspace by id
|
|
||||||
public var workspace: Workspace { __data["workspace"] }
|
|
||||||
|
|
||||||
/// Workspace
|
|
||||||
///
|
|
||||||
/// Parent Type: `WorkspaceType`
|
|
||||||
public struct Workspace: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("doc", Doc.self, arguments: ["docId": .variable("docId")]),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetDocDefaultRoleQuery.Data.Workspace.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// Get get with given id
|
|
||||||
public var doc: Doc { __data["doc"] }
|
|
||||||
|
|
||||||
/// Workspace.Doc
|
|
||||||
///
|
|
||||||
/// Parent Type: `DocType`
|
|
||||||
public struct Doc: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.DocType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("defaultRole", GraphQLEnum<AffineGraphQL.DocRole>.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetDocDefaultRoleQuery.Data.Workspace.Doc.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var defaultRole: GraphQLEnum<AffineGraphQL.DocRole> { __data["defaultRole"] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-120
@@ -1,120 +0,0 @@
|
|||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
@_exported import ApolloAPI
|
|
||||||
|
|
||||||
public class GetMembersByWorkspaceIdQuery: GraphQLQuery {
|
|
||||||
public static let operationName: String = "getMembersByWorkspaceId"
|
|
||||||
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
|
||||||
definition: .init(
|
|
||||||
#"query getMembersByWorkspaceId($workspaceId: String!, $skip: Int, $take: Int, $query: String) { workspace(id: $workspaceId) { __typename memberCount members(skip: $skip, take: $take, query: $query) { __typename id name email avatarUrl permission inviteId emailVerified status } } }"#
|
|
||||||
))
|
|
||||||
|
|
||||||
public var workspaceId: String
|
|
||||||
public var skip: GraphQLNullable<Int>
|
|
||||||
public var take: GraphQLNullable<Int>
|
|
||||||
public var query: GraphQLNullable<String>
|
|
||||||
|
|
||||||
public init(
|
|
||||||
workspaceId: String,
|
|
||||||
skip: GraphQLNullable<Int>,
|
|
||||||
take: GraphQLNullable<Int>,
|
|
||||||
query: GraphQLNullable<String>
|
|
||||||
) {
|
|
||||||
self.workspaceId = workspaceId
|
|
||||||
self.skip = skip
|
|
||||||
self.take = take
|
|
||||||
self.query = query
|
|
||||||
}
|
|
||||||
|
|
||||||
public var __variables: Variables? { [
|
|
||||||
"workspaceId": workspaceId,
|
|
||||||
"skip": skip,
|
|
||||||
"take": take,
|
|
||||||
"query": query
|
|
||||||
] }
|
|
||||||
|
|
||||||
public struct Data: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Query }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("workspace", Workspace.self, arguments: ["id": .variable("workspaceId")]),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetMembersByWorkspaceIdQuery.Data.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// Get workspace by id
|
|
||||||
public var workspace: Workspace { __data["workspace"] }
|
|
||||||
|
|
||||||
/// Workspace
|
|
||||||
///
|
|
||||||
/// Parent Type: `WorkspaceType`
|
|
||||||
public struct Workspace: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("memberCount", Int.self),
|
|
||||||
.field("members", [Member].self, arguments: [
|
|
||||||
"skip": .variable("skip"),
|
|
||||||
"take": .variable("take"),
|
|
||||||
"query": .variable("query")
|
|
||||||
]),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetMembersByWorkspaceIdQuery.Data.Workspace.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// member count of workspace
|
|
||||||
public var memberCount: Int { __data["memberCount"] }
|
|
||||||
/// Members of workspace
|
|
||||||
public var members: [Member] { __data["members"] }
|
|
||||||
|
|
||||||
/// Workspace.Member
|
|
||||||
///
|
|
||||||
/// Parent Type: `InviteUserType`
|
|
||||||
public struct Member: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.InviteUserType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("id", AffineGraphQL.ID.self),
|
|
||||||
.field("name", String?.self),
|
|
||||||
.field("email", String?.self),
|
|
||||||
.field("avatarUrl", String?.self),
|
|
||||||
.field("permission", GraphQLEnum<AffineGraphQL.Permission>.self),
|
|
||||||
.field("inviteId", String.self),
|
|
||||||
.field("emailVerified", Bool?.self),
|
|
||||||
.field("status", GraphQLEnum<AffineGraphQL.WorkspaceMemberStatus>.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetMembersByWorkspaceIdQuery.Data.Workspace.Member.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var id: AffineGraphQL.ID { __data["id"] }
|
|
||||||
/// User name
|
|
||||||
public var name: String? { __data["name"] }
|
|
||||||
/// User email
|
|
||||||
public var email: String? { __data["email"] }
|
|
||||||
/// User avatar url
|
|
||||||
public var avatarUrl: String? { __data["avatarUrl"] }
|
|
||||||
/// User permission in workspace
|
|
||||||
@available(*, deprecated, message: "Use role instead")
|
|
||||||
public var permission: GraphQLEnum<AffineGraphQL.Permission> { __data["permission"] }
|
|
||||||
/// Invite id
|
|
||||||
public var inviteId: String { __data["inviteId"] }
|
|
||||||
/// User email verified
|
|
||||||
public var emailVerified: Bool? { __data["emailVerified"] }
|
|
||||||
/// Member invite status in workspace
|
|
||||||
public var status: GraphQLEnum<AffineGraphQL.WorkspaceMemberStatus> { __data["status"] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-197
@@ -1,197 +0,0 @@
|
|||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
@_exported import ApolloAPI
|
|
||||||
|
|
||||||
public class GetPageGrantedUsersListQuery: GraphQLQuery {
|
|
||||||
public static let operationName: String = "getPageGrantedUsersList"
|
|
||||||
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
|
||||||
definition: .init(
|
|
||||||
#"query getPageGrantedUsersList($pagination: PaginationInput!, $docId: String!, $workspaceId: String!) { workspace(id: $workspaceId) { __typename doc(docId: $docId) { __typename grantedUsersList(pagination: $pagination) { __typename totalCount pageInfo { __typename endCursor hasNextPage } edges { __typename node { __typename role user { __typename id name email avatarUrl } } } } } } }"#
|
|
||||||
))
|
|
||||||
|
|
||||||
public var pagination: PaginationInput
|
|
||||||
public var docId: String
|
|
||||||
public var workspaceId: String
|
|
||||||
|
|
||||||
public init(
|
|
||||||
pagination: PaginationInput,
|
|
||||||
docId: String,
|
|
||||||
workspaceId: String
|
|
||||||
) {
|
|
||||||
self.pagination = pagination
|
|
||||||
self.docId = docId
|
|
||||||
self.workspaceId = workspaceId
|
|
||||||
}
|
|
||||||
|
|
||||||
public var __variables: Variables? { [
|
|
||||||
"pagination": pagination,
|
|
||||||
"docId": docId,
|
|
||||||
"workspaceId": workspaceId
|
|
||||||
] }
|
|
||||||
|
|
||||||
public struct Data: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Query }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("workspace", Workspace.self, arguments: ["id": .variable("workspaceId")]),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetPageGrantedUsersListQuery.Data.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// Get workspace by id
|
|
||||||
public var workspace: Workspace { __data["workspace"] }
|
|
||||||
|
|
||||||
/// Workspace
|
|
||||||
///
|
|
||||||
/// Parent Type: `WorkspaceType`
|
|
||||||
public struct Workspace: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("doc", Doc.self, arguments: ["docId": .variable("docId")]),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetPageGrantedUsersListQuery.Data.Workspace.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// Get get with given id
|
|
||||||
public var doc: Doc { __data["doc"] }
|
|
||||||
|
|
||||||
/// Workspace.Doc
|
|
||||||
///
|
|
||||||
/// Parent Type: `DocType`
|
|
||||||
public struct Doc: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.DocType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("grantedUsersList", GrantedUsersList.self, arguments: ["pagination": .variable("pagination")]),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetPageGrantedUsersListQuery.Data.Workspace.Doc.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// paginated doc granted users list
|
|
||||||
public var grantedUsersList: GrantedUsersList { __data["grantedUsersList"] }
|
|
||||||
|
|
||||||
/// Workspace.Doc.GrantedUsersList
|
|
||||||
///
|
|
||||||
/// Parent Type: `PaginatedGrantedDocUserType`
|
|
||||||
public struct GrantedUsersList: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.PaginatedGrantedDocUserType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("totalCount", Int.self),
|
|
||||||
.field("pageInfo", PageInfo.self),
|
|
||||||
.field("edges", [Edge].self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetPageGrantedUsersListQuery.Data.Workspace.Doc.GrantedUsersList.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var totalCount: Int { __data["totalCount"] }
|
|
||||||
public var pageInfo: PageInfo { __data["pageInfo"] }
|
|
||||||
public var edges: [Edge] { __data["edges"] }
|
|
||||||
|
|
||||||
/// Workspace.Doc.GrantedUsersList.PageInfo
|
|
||||||
///
|
|
||||||
/// Parent Type: `PageInfo`
|
|
||||||
public struct PageInfo: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.PageInfo }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("endCursor", String?.self),
|
|
||||||
.field("hasNextPage", Bool.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetPageGrantedUsersListQuery.Data.Workspace.Doc.GrantedUsersList.PageInfo.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var endCursor: String? { __data["endCursor"] }
|
|
||||||
public var hasNextPage: Bool { __data["hasNextPage"] }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Workspace.Doc.GrantedUsersList.Edge
|
|
||||||
///
|
|
||||||
/// Parent Type: `GrantedDocUserTypeEdge`
|
|
||||||
public struct Edge: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.GrantedDocUserTypeEdge }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("node", Node.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetPageGrantedUsersListQuery.Data.Workspace.Doc.GrantedUsersList.Edge.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var node: Node { __data["node"] }
|
|
||||||
|
|
||||||
/// Workspace.Doc.GrantedUsersList.Edge.Node
|
|
||||||
///
|
|
||||||
/// Parent Type: `GrantedDocUserType`
|
|
||||||
public struct Node: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.GrantedDocUserType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("role", GraphQLEnum<AffineGraphQL.DocRole>.self),
|
|
||||||
.field("user", User.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetPageGrantedUsersListQuery.Data.Workspace.Doc.GrantedUsersList.Edge.Node.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var role: GraphQLEnum<AffineGraphQL.DocRole> { __data["role"] }
|
|
||||||
public var user: User { __data["user"] }
|
|
||||||
|
|
||||||
/// Workspace.Doc.GrantedUsersList.Edge.Node.User
|
|
||||||
///
|
|
||||||
/// Parent Type: `WorkspaceUserType`
|
|
||||||
public struct User: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceUserType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("id", String.self),
|
|
||||||
.field("name", String.self),
|
|
||||||
.field("email", String.self),
|
|
||||||
.field("avatarUrl", String?.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetPageGrantedUsersListQuery.Data.Workspace.Doc.GrantedUsersList.Edge.Node.User.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var id: String { __data["id"] }
|
|
||||||
public var name: String { __data["name"] }
|
|
||||||
public var email: String { __data["email"] }
|
|
||||||
public var avatarUrl: String? { __data["avatarUrl"] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-65
@@ -1,65 +0,0 @@
|
|||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
@_exported import ApolloAPI
|
|
||||||
|
|
||||||
public class GetWorkspaceConfigQuery: GraphQLQuery {
|
|
||||||
public static let operationName: String = "getWorkspaceConfig"
|
|
||||||
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
|
||||||
definition: .init(
|
|
||||||
#"query getWorkspaceConfig($id: String!) { workspace(id: $id) { __typename enableAi enableSharing enableUrlPreview enableDocEmbedding } }"#
|
|
||||||
))
|
|
||||||
|
|
||||||
public var id: String
|
|
||||||
|
|
||||||
public init(id: String) {
|
|
||||||
self.id = id
|
|
||||||
}
|
|
||||||
|
|
||||||
public var __variables: Variables? { ["id": id] }
|
|
||||||
|
|
||||||
public struct Data: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Query }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("workspace", Workspace.self, arguments: ["id": .variable("id")]),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetWorkspaceConfigQuery.Data.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// Get workspace by id
|
|
||||||
public var workspace: Workspace { __data["workspace"] }
|
|
||||||
|
|
||||||
/// Workspace
|
|
||||||
///
|
|
||||||
/// Parent Type: `WorkspaceType`
|
|
||||||
public struct Workspace: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("enableAi", Bool.self),
|
|
||||||
.field("enableSharing", Bool.self),
|
|
||||||
.field("enableUrlPreview", Bool.self),
|
|
||||||
.field("enableDocEmbedding", Bool.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetWorkspaceConfigQuery.Data.Workspace.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// Enable AI
|
|
||||||
public var enableAi: Bool { __data["enableAi"] }
|
|
||||||
/// Enable workspace sharing
|
|
||||||
public var enableSharing: Bool { __data["enableSharing"] }
|
|
||||||
/// Enable url previous when sharing
|
|
||||||
public var enableUrlPreview: Bool { __data["enableUrlPreview"] }
|
|
||||||
/// Enable doc embedding
|
|
||||||
public var enableDocEmbedding: Bool { __data["enableDocEmbedding"] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-57
@@ -1,57 +0,0 @@
|
|||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
@_exported import ApolloAPI
|
|
||||||
|
|
||||||
public class GetWorkspaceEmbeddingStatusQuery: GraphQLQuery {
|
|
||||||
public static let operationName: String = "getWorkspaceEmbeddingStatus"
|
|
||||||
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
|
||||||
definition: .init(
|
|
||||||
#"query getWorkspaceEmbeddingStatus($workspaceId: String!) { queryWorkspaceEmbeddingStatus(workspaceId: $workspaceId) { __typename total embedded } }"#
|
|
||||||
))
|
|
||||||
|
|
||||||
public var workspaceId: String
|
|
||||||
|
|
||||||
public init(workspaceId: String) {
|
|
||||||
self.workspaceId = workspaceId
|
|
||||||
}
|
|
||||||
|
|
||||||
public var __variables: Variables? { ["workspaceId": workspaceId] }
|
|
||||||
|
|
||||||
public struct Data: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Query }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("queryWorkspaceEmbeddingStatus", QueryWorkspaceEmbeddingStatus.self, arguments: ["workspaceId": .variable("workspaceId")]),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetWorkspaceEmbeddingStatusQuery.Data.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// query workspace embedding status
|
|
||||||
public var queryWorkspaceEmbeddingStatus: QueryWorkspaceEmbeddingStatus { __data["queryWorkspaceEmbeddingStatus"] }
|
|
||||||
|
|
||||||
/// QueryWorkspaceEmbeddingStatus
|
|
||||||
///
|
|
||||||
/// Parent Type: `ContextWorkspaceEmbeddingStatus`
|
|
||||||
public struct QueryWorkspaceEmbeddingStatus: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.ContextWorkspaceEmbeddingStatus }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("total", AffineGraphQL.SafeInt.self),
|
|
||||||
.field("embedded", AffineGraphQL.SafeInt.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetWorkspaceEmbeddingStatusQuery.Data.QueryWorkspaceEmbeddingStatus.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var total: AffineGraphQL.SafeInt { __data["total"] }
|
|
||||||
public var embedded: AffineGraphQL.SafeInt { __data["embedded"] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-119
@@ -1,119 +0,0 @@
|
|||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
@_exported import ApolloAPI
|
|
||||||
|
|
||||||
public class GetWorkspaceInfoQuery: GraphQLQuery {
|
|
||||||
public static let operationName: String = "getWorkspaceInfo"
|
|
||||||
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
|
||||||
definition: .init(
|
|
||||||
#"query getWorkspaceInfo($workspaceId: String!) { workspace(id: $workspaceId) { __typename permissions { __typename Workspace_Administrators_Manage Workspace_Blobs_List Workspace_Blobs_Read Workspace_Blobs_Write Workspace_Copilot Workspace_CreateDoc Workspace_Delete Workspace_Organize_Read Workspace_Payment_Manage Workspace_Properties_Create Workspace_Properties_Delete Workspace_Properties_Read Workspace_Properties_Update Workspace_Read Workspace_Settings_Read Workspace_Settings_Update Workspace_Sync Workspace_TransferOwner Workspace_Users_Manage Workspace_Users_Read } role team } }"#
|
|
||||||
))
|
|
||||||
|
|
||||||
public var workspaceId: String
|
|
||||||
|
|
||||||
public init(workspaceId: String) {
|
|
||||||
self.workspaceId = workspaceId
|
|
||||||
}
|
|
||||||
|
|
||||||
public var __variables: Variables? { ["workspaceId": workspaceId] }
|
|
||||||
|
|
||||||
public struct Data: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Query }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("workspace", Workspace.self, arguments: ["id": .variable("workspaceId")]),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetWorkspaceInfoQuery.Data.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// Get workspace by id
|
|
||||||
public var workspace: Workspace { __data["workspace"] }
|
|
||||||
|
|
||||||
/// Workspace
|
|
||||||
///
|
|
||||||
/// Parent Type: `WorkspaceType`
|
|
||||||
public struct Workspace: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("permissions", Permissions.self),
|
|
||||||
.field("role", GraphQLEnum<AffineGraphQL.Permission>.self),
|
|
||||||
.field("team", Bool.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetWorkspaceInfoQuery.Data.Workspace.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// map of action permissions
|
|
||||||
public var permissions: Permissions { __data["permissions"] }
|
|
||||||
/// Role of current signed in user in workspace
|
|
||||||
public var role: GraphQLEnum<AffineGraphQL.Permission> { __data["role"] }
|
|
||||||
/// if workspace is team workspace
|
|
||||||
public var team: Bool { __data["team"] }
|
|
||||||
|
|
||||||
/// Workspace.Permissions
|
|
||||||
///
|
|
||||||
/// Parent Type: `WorkspacePermissions`
|
|
||||||
public struct Permissions: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspacePermissions }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("Workspace_Administrators_Manage", Bool.self),
|
|
||||||
.field("Workspace_Blobs_List", Bool.self),
|
|
||||||
.field("Workspace_Blobs_Read", Bool.self),
|
|
||||||
.field("Workspace_Blobs_Write", Bool.self),
|
|
||||||
.field("Workspace_Copilot", Bool.self),
|
|
||||||
.field("Workspace_CreateDoc", Bool.self),
|
|
||||||
.field("Workspace_Delete", Bool.self),
|
|
||||||
.field("Workspace_Organize_Read", Bool.self),
|
|
||||||
.field("Workspace_Payment_Manage", Bool.self),
|
|
||||||
.field("Workspace_Properties_Create", Bool.self),
|
|
||||||
.field("Workspace_Properties_Delete", Bool.self),
|
|
||||||
.field("Workspace_Properties_Read", Bool.self),
|
|
||||||
.field("Workspace_Properties_Update", Bool.self),
|
|
||||||
.field("Workspace_Read", Bool.self),
|
|
||||||
.field("Workspace_Settings_Read", Bool.self),
|
|
||||||
.field("Workspace_Settings_Update", Bool.self),
|
|
||||||
.field("Workspace_Sync", Bool.self),
|
|
||||||
.field("Workspace_TransferOwner", Bool.self),
|
|
||||||
.field("Workspace_Users_Manage", Bool.self),
|
|
||||||
.field("Workspace_Users_Read", Bool.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetWorkspaceInfoQuery.Data.Workspace.Permissions.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var workspace_Administrators_Manage: Bool { __data["Workspace_Administrators_Manage"] }
|
|
||||||
public var workspace_Blobs_List: Bool { __data["Workspace_Blobs_List"] }
|
|
||||||
public var workspace_Blobs_Read: Bool { __data["Workspace_Blobs_Read"] }
|
|
||||||
public var workspace_Blobs_Write: Bool { __data["Workspace_Blobs_Write"] }
|
|
||||||
public var workspace_Copilot: Bool { __data["Workspace_Copilot"] }
|
|
||||||
public var workspace_CreateDoc: Bool { __data["Workspace_CreateDoc"] }
|
|
||||||
public var workspace_Delete: Bool { __data["Workspace_Delete"] }
|
|
||||||
public var workspace_Organize_Read: Bool { __data["Workspace_Organize_Read"] }
|
|
||||||
public var workspace_Payment_Manage: Bool { __data["Workspace_Payment_Manage"] }
|
|
||||||
public var workspace_Properties_Create: Bool { __data["Workspace_Properties_Create"] }
|
|
||||||
public var workspace_Properties_Delete: Bool { __data["Workspace_Properties_Delete"] }
|
|
||||||
public var workspace_Properties_Read: Bool { __data["Workspace_Properties_Read"] }
|
|
||||||
public var workspace_Properties_Update: Bool { __data["Workspace_Properties_Update"] }
|
|
||||||
public var workspace_Read: Bool { __data["Workspace_Read"] }
|
|
||||||
public var workspace_Settings_Read: Bool { __data["Workspace_Settings_Read"] }
|
|
||||||
public var workspace_Settings_Update: Bool { __data["Workspace_Settings_Update"] }
|
|
||||||
public var workspace_Sync: Bool { __data["Workspace_Sync"] }
|
|
||||||
public var workspace_TransferOwner: Bool { __data["Workspace_TransferOwner"] }
|
|
||||||
public var workspace_Users_Manage: Bool { __data["Workspace_Users_Manage"] }
|
|
||||||
public var workspace_Users_Read: Bool { __data["Workspace_Users_Read"] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-76
@@ -1,76 +0,0 @@
|
|||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
@_exported import ApolloAPI
|
|
||||||
|
|
||||||
public class ListUserAccessTokensQuery: GraphQLQuery {
|
|
||||||
public static let operationName: String = "listUserAccessTokens"
|
|
||||||
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
|
||||||
definition: .init(
|
|
||||||
#"query listUserAccessTokens { currentUser { __typename revealedAccessTokens { __typename id name createdAt expiresAt token } } }"#
|
|
||||||
))
|
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
public struct Data: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Query }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("currentUser", CurrentUser?.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
ListUserAccessTokensQuery.Data.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// Get current user
|
|
||||||
public var currentUser: CurrentUser? { __data["currentUser"] }
|
|
||||||
|
|
||||||
/// CurrentUser
|
|
||||||
///
|
|
||||||
/// Parent Type: `UserType`
|
|
||||||
public struct CurrentUser: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.UserType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("revealedAccessTokens", [RevealedAccessToken].self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
ListUserAccessTokensQuery.Data.CurrentUser.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var revealedAccessTokens: [RevealedAccessToken] { __data["revealedAccessTokens"] }
|
|
||||||
|
|
||||||
/// CurrentUser.RevealedAccessToken
|
|
||||||
///
|
|
||||||
/// Parent Type: `RevealedAccessToken`
|
|
||||||
public struct RevealedAccessToken: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.RevealedAccessToken }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("id", String.self),
|
|
||||||
.field("name", String.self),
|
|
||||||
.field("createdAt", AffineGraphQL.DateTime.self),
|
|
||||||
.field("expiresAt", AffineGraphQL.DateTime?.self),
|
|
||||||
.field("token", String.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
ListUserAccessTokensQuery.Data.CurrentUser.RevealedAccessToken.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var id: String { __data["id"] }
|
|
||||||
public var name: String { __data["name"] }
|
|
||||||
public var createdAt: AffineGraphQL.DateTime { __data["createdAt"] }
|
|
||||||
public var expiresAt: AffineGraphQL.DateTime? { __data["expiresAt"] }
|
|
||||||
public var token: String { __data["token"] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-69
@@ -1,69 +0,0 @@
|
|||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
@_exported import ApolloAPI
|
|
||||||
|
|
||||||
public class NotificationCountQuery: GraphQLQuery {
|
|
||||||
public static let operationName: String = "notificationCount"
|
|
||||||
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
|
||||||
definition: .init(
|
|
||||||
#"query notificationCount { currentUser { __typename notifications(pagination: { first: 1 }) { __typename totalCount } } }"#
|
|
||||||
))
|
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
public struct Data: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Query }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("currentUser", CurrentUser?.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
NotificationCountQuery.Data.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// Get current user
|
|
||||||
public var currentUser: CurrentUser? { __data["currentUser"] }
|
|
||||||
|
|
||||||
/// CurrentUser
|
|
||||||
///
|
|
||||||
/// Parent Type: `UserType`
|
|
||||||
public struct CurrentUser: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.UserType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("notifications", Notifications.self, arguments: ["pagination": ["first": 1]]),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
NotificationCountQuery.Data.CurrentUser.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// Get current user notifications
|
|
||||||
public var notifications: Notifications { __data["notifications"] }
|
|
||||||
|
|
||||||
/// CurrentUser.Notifications
|
|
||||||
///
|
|
||||||
/// Parent Type: `PaginatedNotificationObjectType`
|
|
||||||
public struct Notifications: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.PaginatedNotificationObjectType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("totalCount", Int.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
NotificationCountQuery.Data.CurrentUser.Notifications.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var totalCount: Int { __data["totalCount"] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-122
@@ -1,122 +0,0 @@
|
|||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
@_exported import ApolloAPI
|
|
||||||
|
|
||||||
public class WorkspaceQuotaQuery: GraphQLQuery {
|
|
||||||
public static let operationName: String = "workspaceQuota"
|
|
||||||
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
|
||||||
definition: .init(
|
|
||||||
#"query workspaceQuota($id: String!) { workspace(id: $id) { __typename quota { __typename name blobLimit storageQuota usedStorageQuota historyPeriod memberLimit memberCount overcapacityMemberCount humanReadable { __typename name blobLimit storageQuota historyPeriod memberLimit memberCount overcapacityMemberCount } } } }"#
|
|
||||||
))
|
|
||||||
|
|
||||||
public var id: String
|
|
||||||
|
|
||||||
public init(id: String) {
|
|
||||||
self.id = id
|
|
||||||
}
|
|
||||||
|
|
||||||
public var __variables: Variables? { ["id": id] }
|
|
||||||
|
|
||||||
public struct Data: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.Query }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("workspace", Workspace.self, arguments: ["id": .variable("id")]),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
WorkspaceQuotaQuery.Data.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// Get workspace by id
|
|
||||||
public var workspace: Workspace { __data["workspace"] }
|
|
||||||
|
|
||||||
/// Workspace
|
|
||||||
///
|
|
||||||
/// Parent Type: `WorkspaceType`
|
|
||||||
public struct Workspace: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("quota", Quota.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
WorkspaceQuotaQuery.Data.Workspace.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// quota of workspace
|
|
||||||
public var quota: Quota { __data["quota"] }
|
|
||||||
|
|
||||||
/// Workspace.Quota
|
|
||||||
///
|
|
||||||
/// Parent Type: `WorkspaceQuotaType`
|
|
||||||
public struct Quota: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceQuotaType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("name", String.self),
|
|
||||||
.field("blobLimit", AffineGraphQL.SafeInt.self),
|
|
||||||
.field("storageQuota", AffineGraphQL.SafeInt.self),
|
|
||||||
.field("usedStorageQuota", AffineGraphQL.SafeInt.self),
|
|
||||||
.field("historyPeriod", AffineGraphQL.SafeInt.self),
|
|
||||||
.field("memberLimit", Int.self),
|
|
||||||
.field("memberCount", Int.self),
|
|
||||||
.field("overcapacityMemberCount", Int.self),
|
|
||||||
.field("humanReadable", HumanReadable.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
WorkspaceQuotaQuery.Data.Workspace.Quota.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var name: String { __data["name"] }
|
|
||||||
public var blobLimit: AffineGraphQL.SafeInt { __data["blobLimit"] }
|
|
||||||
public var storageQuota: AffineGraphQL.SafeInt { __data["storageQuota"] }
|
|
||||||
public var usedStorageQuota: AffineGraphQL.SafeInt { __data["usedStorageQuota"] }
|
|
||||||
public var historyPeriod: AffineGraphQL.SafeInt { __data["historyPeriod"] }
|
|
||||||
public var memberLimit: Int { __data["memberLimit"] }
|
|
||||||
public var memberCount: Int { __data["memberCount"] }
|
|
||||||
public var overcapacityMemberCount: Int { __data["overcapacityMemberCount"] }
|
|
||||||
public var humanReadable: HumanReadable { __data["humanReadable"] }
|
|
||||||
|
|
||||||
/// Workspace.Quota.HumanReadable
|
|
||||||
///
|
|
||||||
/// Parent Type: `WorkspaceQuotaHumanReadableType`
|
|
||||||
public struct HumanReadable: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.WorkspaceQuotaHumanReadableType }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("name", String.self),
|
|
||||||
.field("blobLimit", String.self),
|
|
||||||
.field("storageQuota", String.self),
|
|
||||||
.field("historyPeriod", String.self),
|
|
||||||
.field("memberLimit", String.self),
|
|
||||||
.field("memberCount", String.self),
|
|
||||||
.field("overcapacityMemberCount", String.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
WorkspaceQuotaQuery.Data.Workspace.Quota.HumanReadable.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
public var name: String { __data["name"] }
|
|
||||||
public var blobLimit: String { __data["blobLimit"] }
|
|
||||||
public var storageQuota: String { __data["storageQuota"] }
|
|
||||||
public var historyPeriod: String { __data["historyPeriod"] }
|
|
||||||
public var memberLimit: String { __data["memberLimit"] }
|
|
||||||
public var memberCount: String { __data["memberCount"] }
|
|
||||||
public var overcapacityMemberCount: String { __data["overcapacityMemberCount"] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ import { AuthService } from '@affine/core/modules/cloud/services/auth';
|
|||||||
import { FetchService } from '@affine/core/modules/cloud/services/fetch';
|
import { FetchService } from '@affine/core/modules/cloud/services/fetch';
|
||||||
import { AuthStore } from '@affine/core/modules/cloud/stores/auth';
|
import { AuthStore } from '@affine/core/modules/cloud/stores/auth';
|
||||||
import { GlobalDialogService } from '@affine/core/modules/dialogs/services/dialog';
|
import { GlobalDialogService } from '@affine/core/modules/dialogs/services/dialog';
|
||||||
|
import { NbstoreService } from '@affine/core/modules/storage';
|
||||||
import { UrlService } from '@affine/core/modules/url/services/url';
|
import { UrlService } from '@affine/core/modules/url/services/url';
|
||||||
import { Framework } from '@toeverything/infra';
|
import { Framework } from '@toeverything/infra';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
@@ -37,12 +38,16 @@ describe('AuthService oauthPreflight', () => {
|
|||||||
} as any);
|
} as any);
|
||||||
framework.service(UrlService, { getClientScheme: () => null } as any);
|
framework.service(UrlService, { getClientScheme: () => null } as any);
|
||||||
framework.service(GlobalDialogService, { open: vi.fn() } as any);
|
framework.service(GlobalDialogService, { open: vi.fn() } as any);
|
||||||
|
framework.service(NbstoreService, {
|
||||||
|
realtime: { subscribe: () => of() },
|
||||||
|
} as any);
|
||||||
|
|
||||||
framework.service(AuthService, [
|
framework.service(AuthService, [
|
||||||
FetchService,
|
FetchService,
|
||||||
AuthStore,
|
AuthStore,
|
||||||
UrlService,
|
UrlService,
|
||||||
GlobalDialogService,
|
GlobalDialogService,
|
||||||
|
NbstoreService,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const auth = framework.provider().get(AuthService);
|
const auth = framework.provider().get(AuthService);
|
||||||
|
|||||||
@@ -31,4 +31,22 @@ describe('CopilotClient action streams', () => {
|
|||||||
'/api/copilot/actions/session-1/stream?messageId=message-1&actionId=mindmap.generate&actionVersion=v1&runId=run-1&retry=true'
|
'/api/copilot/actions/session-1/stream?messageId=message-1&actionId=mindmap.generate&actionVersion=v1&runId=run-1&retry=true'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('loads embedding status through realtime request', async () => {
|
||||||
|
const gql = vi.fn();
|
||||||
|
const request = vi.fn().mockResolvedValue({ total: 4, embedded: 2 });
|
||||||
|
const client = new CopilotClient(gql, vi.fn(), { request });
|
||||||
|
|
||||||
|
await expect(client.getEmbeddingStatus('workspace-1')).resolves.toEqual({
|
||||||
|
total: 4,
|
||||||
|
embedded: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(gql).not.toHaveBeenCalled();
|
||||||
|
expect(request).toHaveBeenCalledWith(
|
||||||
|
'workspace.embedding.progress.get',
|
||||||
|
{ workspaceId: 'workspace-1' },
|
||||||
|
{ timeoutMs: 10000 }
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { showAILoginRequiredAtom } from '@affine/core/components/affine/auth/ai-login-required';
|
import { showAILoginRequiredAtom } from '@affine/core/components/affine/auth/ai-login-required';
|
||||||
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
|
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
|
||||||
|
import type { NbstoreService } from '@affine/core/modules/storage';
|
||||||
import { UserFriendlyError } from '@affine/error';
|
import { UserFriendlyError } from '@affine/error';
|
||||||
import {
|
import {
|
||||||
addContextBlobMutation,
|
addContextBlobMutation,
|
||||||
@@ -17,7 +18,6 @@ import {
|
|||||||
getCopilotRecentSessionsQuery,
|
getCopilotRecentSessionsQuery,
|
||||||
getCopilotSessionQuery,
|
getCopilotSessionQuery,
|
||||||
getCopilotSessionsQuery,
|
getCopilotSessionsQuery,
|
||||||
getWorkspaceEmbeddingStatusQuery,
|
|
||||||
type GraphQLQuery,
|
type GraphQLQuery,
|
||||||
listContextObjectQuery,
|
listContextObjectQuery,
|
||||||
listContextQuery,
|
listContextQuery,
|
||||||
@@ -98,7 +98,8 @@ export class CopilotClient {
|
|||||||
readonly eventSource: (
|
readonly eventSource: (
|
||||||
url: string,
|
url: string,
|
||||||
eventSourceInitDict?: EventSourceInit
|
eventSourceInitDict?: EventSourceInit
|
||||||
) => EventSource
|
) => EventSource,
|
||||||
|
readonly realtime?: Pick<NbstoreService['realtime'], 'request'>
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createSession(
|
async createSession(
|
||||||
@@ -546,11 +547,15 @@ export class CopilotClient {
|
|||||||
return queryString.toString();
|
return queryString.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
getEmbeddingStatus(workspaceId: string) {
|
async getEmbeddingStatus(workspaceId: string) {
|
||||||
return this.gql({
|
if (!this.realtime) {
|
||||||
query: getWorkspaceEmbeddingStatusQuery,
|
throw new Error('Realtime client is required');
|
||||||
variables: { workspaceId },
|
}
|
||||||
}).then(res => res.queryWorkspaceEmbeddingStatus);
|
return await this.realtime.request(
|
||||||
|
'workspace.embedding.progress.get',
|
||||||
|
{ workspaceId },
|
||||||
|
{ timeoutMs: 10000 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
addContextBlob(options: OptionsField<typeof addContextBlobMutation>) {
|
addContextBlob(options: OptionsField<typeof addContextBlobMutation>) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { NbstoreService } from '@affine/core/modules/storage';
|
||||||
import {
|
import {
|
||||||
ContextCategories,
|
ContextCategories,
|
||||||
type CopilotChatHistoryFragment,
|
type CopilotChatHistoryFragment,
|
||||||
@@ -390,7 +391,8 @@ export function createAIRequestService(
|
|||||||
eventSource: (
|
eventSource: (
|
||||||
url: string,
|
url: string,
|
||||||
eventSourceInitDict?: EventSourceInit
|
eventSourceInitDict?: EventSourceInit
|
||||||
) => EventSource
|
) => EventSource,
|
||||||
|
realtime: Pick<NbstoreService['realtime'], 'request'>
|
||||||
) {
|
) {
|
||||||
return new AIRequestService(new CopilotClient(gql, eventSource));
|
return new AIRequestService(new CopilotClient(gql, eventSource, realtime));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { DocsService } from '@affine/core/modules/doc';
|
|||||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||||
import { useRegisterNavigationCommands } from '@affine/core/modules/navigation/view/use-register-navigation-commands';
|
import { useRegisterNavigationCommands } from '@affine/core/modules/navigation/view/use-register-navigation-commands';
|
||||||
import { QuickSearchContainer } from '@affine/core/modules/quicksearch';
|
import { QuickSearchContainer } from '@affine/core/modules/quicksearch';
|
||||||
|
import { NbstoreService } from '@affine/core/modules/storage';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import {
|
import {
|
||||||
getAFFiNEWorkspaceSchema,
|
getAFFiNEWorkspaceSchema,
|
||||||
@@ -140,12 +141,14 @@ export const WorkspaceSideEffects = () => {
|
|||||||
const graphqlService = useService(GraphQLService);
|
const graphqlService = useService(GraphQLService);
|
||||||
const eventSourceService = useService(EventSourceService);
|
const eventSourceService = useService(EventSourceService);
|
||||||
const authService = useService(AuthService);
|
const authService = useService(AuthService);
|
||||||
|
const nbstoreService = useService(NbstoreService);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const dispose = setupAIProvider(
|
const dispose = setupAIProvider(
|
||||||
createAIRequestService(
|
createAIRequestService(
|
||||||
graphqlService.gql,
|
graphqlService.gql,
|
||||||
eventSourceService.eventSource
|
eventSourceService.eventSource,
|
||||||
|
nbstoreService.realtime
|
||||||
),
|
),
|
||||||
globalDialogService,
|
globalDialogService,
|
||||||
authService
|
authService
|
||||||
@@ -155,6 +158,7 @@ export const WorkspaceSideEffects = () => {
|
|||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
eventSourceService,
|
eventSourceService,
|
||||||
|
nbstoreService,
|
||||||
workspaceDialogService,
|
workspaceDialogService,
|
||||||
graphqlService,
|
graphqlService,
|
||||||
globalDialogService,
|
globalDialogService,
|
||||||
|
|||||||
+3
-4
@@ -46,13 +46,12 @@ const McpServerSetting = () => {
|
|||||||
return accessTokens?.find(token => token.name === 'mcp');
|
return accessTokens?.find(token => token.name === 'mcp');
|
||||||
}, [accessTokens]);
|
}, [accessTokens]);
|
||||||
|
|
||||||
const displayedToken = revealedAccessToken ?? mcpAccessToken;
|
|
||||||
const hasMcpToken = Boolean(revealedAccessToken || mcpAccessToken);
|
const hasMcpToken = Boolean(revealedAccessToken || mcpAccessToken);
|
||||||
const hasCopyableToken = Boolean(revealedAccessToken);
|
const hasCopyableToken = Boolean(revealedAccessToken);
|
||||||
const isRedactedDisplay = hasMcpToken && !hasCopyableToken;
|
const isRedactedDisplay = hasMcpToken && !hasCopyableToken;
|
||||||
|
|
||||||
const code = useMemo(() => {
|
const code = useMemo(() => {
|
||||||
return displayedToken
|
return revealedAccessToken
|
||||||
? JSON.stringify(
|
? JSON.stringify(
|
||||||
{
|
{
|
||||||
mcpServers: {
|
mcpServers: {
|
||||||
@@ -61,7 +60,7 @@ const McpServerSetting = () => {
|
|||||||
url: `${serverService.server.baseUrl}/api/workspaces/${workspaceService.workspace.id}/mcp`,
|
url: `${serverService.server.baseUrl}/api/workspaces/${workspaceService.workspace.id}/mcp`,
|
||||||
note: `Read docs from AFFiNE workspace "${workspaceName}"`,
|
note: `Read docs from AFFiNE workspace "${workspaceName}"`,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${displayedToken.token}`,
|
Authorization: `Bearer ${revealedAccessToken.token}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -70,7 +69,7 @@ const McpServerSetting = () => {
|
|||||||
2
|
2
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
}, [displayedToken, workspaceName, workspaceService, serverService]);
|
}, [revealedAccessToken, workspaceName, workspaceService, serverService]);
|
||||||
|
|
||||||
const copyJsonDisabled = !code || mutating || isRedactedDisplay;
|
const copyJsonDisabled = !code || mutating || isRedactedDisplay;
|
||||||
const copyJsonTooltip = isRedactedDisplay
|
const copyJsonTooltip = isRedactedDisplay
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
import { PeekViewService } from '@affine/core/modules/peek-view';
|
import { PeekViewService } from '@affine/core/modules/peek-view';
|
||||||
|
import { NbstoreService } from '@affine/core/modules/storage';
|
||||||
import { AppThemeService } from '@affine/core/modules/theme';
|
import { AppThemeService } from '@affine/core/modules/theme';
|
||||||
import {
|
import {
|
||||||
ViewBody,
|
ViewBody,
|
||||||
@@ -55,14 +56,16 @@ import * as styles from './index.css';
|
|||||||
function useAIRequestService() {
|
function useAIRequestService() {
|
||||||
const graphqlService = useService(GraphQLService);
|
const graphqlService = useService(GraphQLService);
|
||||||
const eventSourceService = useService(EventSourceService);
|
const eventSourceService = useService(EventSourceService);
|
||||||
|
const nbstoreService = useService(NbstoreService);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() =>
|
() =>
|
||||||
createAIRequestService(
|
createAIRequestService(
|
||||||
graphqlService.gql,
|
graphqlService.gql,
|
||||||
eventSourceService.eventSource
|
eventSourceService.eventSource,
|
||||||
|
nbstoreService.realtime
|
||||||
),
|
),
|
||||||
[graphqlService, eventSourceService]
|
[graphqlService, eventSourceService, nbstoreService]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
|||||||
import { useSignalValue } from '@affine/core/modules/doc-info/utils';
|
import { useSignalValue } from '@affine/core/modules/doc-info/utils';
|
||||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
import { PeekViewService } from '@affine/core/modules/peek-view';
|
import { PeekViewService } from '@affine/core/modules/peek-view';
|
||||||
|
import { NbstoreService } from '@affine/core/modules/storage';
|
||||||
import { AppThemeService } from '@affine/core/modules/theme';
|
import { AppThemeService } from '@affine/core/modules/theme';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -75,6 +76,7 @@ export const EditorChatPanel = ({
|
|||||||
const framework = useFramework();
|
const framework = useFramework();
|
||||||
const graphqlService = useService(GraphQLService);
|
const graphqlService = useService(GraphQLService);
|
||||||
const eventSourceService = useService(EventSourceService);
|
const eventSourceService = useService(EventSourceService);
|
||||||
|
const nbstoreService = useService(NbstoreService);
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
@@ -108,9 +110,10 @@ export const EditorChatPanel = ({
|
|||||||
() =>
|
() =>
|
||||||
createAIRequestService(
|
createAIRequestService(
|
||||||
graphqlService.gql,
|
graphqlService.gql,
|
||||||
eventSourceService.eventSource
|
eventSourceService.eventSource,
|
||||||
|
nbstoreService.realtime
|
||||||
),
|
),
|
||||||
[eventSourceService.eventSource, graphqlService.gql]
|
[eventSourceService.eventSource, graphqlService.gql, nbstoreService]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [pendingSessionId] = useState(() => {
|
const [pendingSessionId] = useState(() => {
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { map, tap } from 'rxjs';
|
import { map, tap } from 'rxjs';
|
||||||
|
|
||||||
|
import { mapRealtimeEnum } from '../realtime/enum';
|
||||||
import type { AuthService } from '../services/auth';
|
import type { AuthService } from '../services/auth';
|
||||||
import type { UserFeatureStore } from '../stores/user-feature';
|
|
||||||
|
|
||||||
export class UserFeature extends Entity {
|
export class UserFeature extends Entity {
|
||||||
// undefined means no user, null means loading
|
// undefined means no user, null means loading
|
||||||
@@ -32,10 +32,7 @@ export class UserFeature extends Entity {
|
|||||||
isRevalidating$ = new LiveData(false);
|
isRevalidating$ = new LiveData(false);
|
||||||
error$ = new LiveData<any | null>(null);
|
error$ = new LiveData<any | null>(null);
|
||||||
|
|
||||||
constructor(
|
constructor(private readonly authService: AuthService) {
|
||||||
private readonly authService: AuthService,
|
|
||||||
private readonly store: UserFeatureStore
|
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,23 +41,20 @@ export class UserFeature extends Entity {
|
|||||||
accountId: this.authService.session.account$.value?.id,
|
accountId: this.authService.session.account$.value?.id,
|
||||||
})),
|
})),
|
||||||
exhaustMapSwitchUntilChanged(
|
exhaustMapSwitchUntilChanged(
|
||||||
(a, b) => a.accountId === b.accountId,
|
() => false,
|
||||||
({ accountId }) => {
|
({ accountId }) => {
|
||||||
return fromPromise(async signal => {
|
return fromPromise(async () => {
|
||||||
if (!accountId) {
|
if (!accountId) {
|
||||||
return; // no feature if no user
|
return; // no feature if no user
|
||||||
}
|
}
|
||||||
|
|
||||||
const { userId, features } = await this.store.getUserFeatures(signal);
|
const account = this.authService.session.account$.value;
|
||||||
if (userId !== accountId) {
|
if (account?.id !== accountId) return;
|
||||||
// The user has changed, ignore the result
|
|
||||||
this.authService.session.revalidate();
|
|
||||||
await this.authService.session.waitForRevalidation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
userId: userId,
|
userId: account.id,
|
||||||
features: features,
|
features: account.info?.features?.map(feature =>
|
||||||
|
mapRealtimeEnum(FeatureType, feature, 'user feature')
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}).pipe(
|
}).pipe(
|
||||||
smartRetry(),
|
smartRetry(),
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { GetCurrentUserProfileQuery } from '@affine/graphql';
|
|
||||||
import type { UserQuotaStateSnapshot } from '@affine/realtime';
|
import type { UserQuotaStateSnapshot } from '@affine/realtime';
|
||||||
import { Framework } from '@toeverything/infra';
|
import { Framework } from '@toeverything/infra';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
@@ -8,8 +7,6 @@ import { AuthService } from '../services/auth';
|
|||||||
import { UserQuotaStore } from '../stores/user-quota';
|
import { UserQuotaStore } from '../stores/user-quota';
|
||||||
import { UserQuota } from './user-quota';
|
import { UserQuota } from './user-quota';
|
||||||
|
|
||||||
type Quota = NonNullable<GetCurrentUserProfileQuery['currentUser']>['quota'];
|
|
||||||
|
|
||||||
const authService = {
|
const authService = {
|
||||||
session: {
|
session: {
|
||||||
['account$']: {
|
['account$']: {
|
||||||
@@ -41,24 +38,6 @@ function createQuotaState(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createQuota(overrides: Partial<Quota> = {}): Quota {
|
|
||||||
return {
|
|
||||||
name: 'Legacy',
|
|
||||||
blobLimit: 1024,
|
|
||||||
storageQuota: 2048,
|
|
||||||
historyPeriod: 30 * 24 * 60 * 60,
|
|
||||||
memberLimit: 8,
|
|
||||||
humanReadable: {
|
|
||||||
name: 'Legacy',
|
|
||||||
blobLimit: '1 KB',
|
|
||||||
storageQuota: '2 KB',
|
|
||||||
historyPeriod: '30 days',
|
|
||||||
memberLimit: '8',
|
|
||||||
},
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createStore(
|
function createStore(
|
||||||
overrides: Partial<UserQuotaStore>,
|
overrides: Partial<UserQuotaStore>,
|
||||||
eventSubject = new Subject<{ type: 'ready' } | { changed: true }>()
|
eventSubject = new Subject<{ type: 'ready' } | { changed: true }>()
|
||||||
@@ -66,7 +45,6 @@ function createStore(
|
|||||||
return {
|
return {
|
||||||
fetchUserQuotaState: vi.fn(),
|
fetchUserQuotaState: vi.fn(),
|
||||||
subscribeUserQuotaState: vi.fn(() => eventSubject),
|
subscribeUserQuotaState: vi.fn(() => eventSubject),
|
||||||
fetchUserQuota: vi.fn(),
|
|
||||||
...overrides,
|
...overrides,
|
||||||
} as unknown as UserQuotaStore;
|
} as unknown as UserQuotaStore;
|
||||||
}
|
}
|
||||||
@@ -103,24 +81,20 @@ describe('UserQuota', () => {
|
|||||||
await vi.waitFor(() => expect(quota.used$.value).toBe(768));
|
await vi.waitFor(() => expect(quota.used$.value).toBe(768));
|
||||||
|
|
||||||
expect(store.fetchUserQuotaState).toHaveBeenCalledTimes(2);
|
expect(store.fetchUserQuotaState).toHaveBeenCalledTimes(2);
|
||||||
expect(store.fetchUserQuota).not.toHaveBeenCalled();
|
|
||||||
quota.dispose();
|
quota.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('falls back to legacy GraphQL quota when realtime request fails', async () => {
|
test('surfaces realtime quota errors without GraphQL fallback', async () => {
|
||||||
|
const error = new Error('offline');
|
||||||
const store = createStore({
|
const store = createStore({
|
||||||
fetchUserQuotaState: vi.fn().mockRejectedValue(new Error('offline')),
|
fetchUserQuotaState: vi.fn().mockRejectedValue(error),
|
||||||
fetchUserQuota: vi.fn().mockResolvedValue({
|
|
||||||
quota: createQuota(),
|
|
||||||
used: 256,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
const quota = createEntity(store);
|
const quota = createEntity(store);
|
||||||
|
|
||||||
quota.revalidate();
|
quota.revalidate();
|
||||||
|
|
||||||
await vi.waitFor(() => expect(quota.quota$.value?.name).toBe('Legacy'));
|
await vi.waitFor(() => expect(quota.error$.value).toBe(error));
|
||||||
expect(quota.used$.value).toBe(256);
|
expect(quota.quota$.value).toBeNull();
|
||||||
quota.dispose();
|
quota.dispose();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { GetCurrentUserProfileQuery } from '@affine/graphql';
|
|
||||||
import type {
|
import type {
|
||||||
RealtimeTopicEventOf,
|
RealtimeTopicEventOf,
|
||||||
UserQuotaStateSnapshot,
|
UserQuotaStateSnapshot,
|
||||||
@@ -11,9 +10,20 @@ import { RealtimeLiveQuery } from '../realtime/live-query';
|
|||||||
import type { AuthService } from '../services/auth';
|
import type { AuthService } from '../services/auth';
|
||||||
import type { UserQuotaStore } from '../stores/user-quota';
|
import type { UserQuotaStore } from '../stores/user-quota';
|
||||||
|
|
||||||
type QuotaType = NonNullable<
|
type QuotaType = {
|
||||||
GetCurrentUserProfileQuery['currentUser']
|
name: string;
|
||||||
>['quota'];
|
blobLimit: number;
|
||||||
|
storageQuota: number;
|
||||||
|
historyPeriod: number;
|
||||||
|
memberLimit: number;
|
||||||
|
humanReadable: {
|
||||||
|
name: string;
|
||||||
|
blobLimit: string;
|
||||||
|
storageQuota: string;
|
||||||
|
historyPeriod: string;
|
||||||
|
memberLimit: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const DAY_SECONDS = 24 * 60 * 60;
|
const DAY_SECONDS = 24 * 60 * 60;
|
||||||
|
|
||||||
@@ -159,9 +169,6 @@ export class UserQuota extends Entity {
|
|||||||
quota: userQuotaFromState(state),
|
quota: userQuotaFromState(state),
|
||||||
used: state.usedStorageQuota,
|
used: state.usedStorageQuota,
|
||||||
};
|
};
|
||||||
} catch {
|
|
||||||
const { quota, used } = await this.store.fetchUserQuota(signal);
|
|
||||||
return { quota, used };
|
|
||||||
} finally {
|
} finally {
|
||||||
this.isRevalidating$.setValue(false);
|
this.isRevalidating$.setValue(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ import { ServerConfigStore } from './stores/server-config';
|
|||||||
import { ServerListStore } from './stores/server-list';
|
import { ServerListStore } from './stores/server-list';
|
||||||
import { SubscriptionStore } from './stores/subscription';
|
import { SubscriptionStore } from './stores/subscription';
|
||||||
import { UserCopilotQuotaStore } from './stores/user-copilot-quota';
|
import { UserCopilotQuotaStore } from './stores/user-copilot-quota';
|
||||||
import { UserFeatureStore } from './stores/user-feature';
|
|
||||||
import { UserQuotaStore } from './stores/user-quota';
|
import { UserQuotaStore } from './stores/user-quota';
|
||||||
import { UserSettingsStore } from './stores/user-settings';
|
import { UserSettingsStore } from './stores/user-settings';
|
||||||
import { DocCreatedByService } from './services/doc-created-by';
|
import { DocCreatedByService } from './services/doc-created-by';
|
||||||
@@ -146,6 +145,7 @@ export function configureCloudModule(framework: Framework) {
|
|||||||
AuthStore,
|
AuthStore,
|
||||||
UrlService,
|
UrlService,
|
||||||
GlobalDialogService,
|
GlobalDialogService,
|
||||||
|
NbstoreService,
|
||||||
])
|
])
|
||||||
.store(AuthStore, [
|
.store(AuthStore, [
|
||||||
FetchService,
|
FetchService,
|
||||||
@@ -153,6 +153,7 @@ export function configureCloudModule(framework: Framework) {
|
|||||||
GlobalState,
|
GlobalState,
|
||||||
ServerService,
|
ServerService,
|
||||||
AuthProvider,
|
AuthProvider,
|
||||||
|
NbstoreService,
|
||||||
])
|
])
|
||||||
.entity(AuthSession, [AuthStore])
|
.entity(AuthSession, [AuthStore])
|
||||||
.service(SubscriptionService, [SubscriptionStore])
|
.service(SubscriptionService, [SubscriptionStore])
|
||||||
@@ -165,7 +166,7 @@ export function configureCloudModule(framework: Framework) {
|
|||||||
.entity(Subscription, [AuthService, ServerService, SubscriptionStore])
|
.entity(Subscription, [AuthService, ServerService, SubscriptionStore])
|
||||||
.entity(SubscriptionPrices, [ServerService, SubscriptionStore])
|
.entity(SubscriptionPrices, [ServerService, SubscriptionStore])
|
||||||
.service(UserQuotaService)
|
.service(UserQuotaService)
|
||||||
.store(UserQuotaStore, [GraphQLService, NbstoreService])
|
.store(UserQuotaStore, [NbstoreService])
|
||||||
.entity(UserQuota, [AuthService, UserQuotaStore])
|
.entity(UserQuota, [AuthService, UserQuotaStore])
|
||||||
.service(UserCopilotQuotaService)
|
.service(UserCopilotQuotaService)
|
||||||
.store(UserCopilotQuotaStore, [GraphQLService])
|
.store(UserCopilotQuotaStore, [GraphQLService])
|
||||||
@@ -175,8 +176,7 @@ export function configureCloudModule(framework: Framework) {
|
|||||||
ServerService,
|
ServerService,
|
||||||
])
|
])
|
||||||
.service(UserFeatureService)
|
.service(UserFeatureService)
|
||||||
.entity(UserFeature, [AuthService, UserFeatureStore])
|
.entity(UserFeature, [AuthService])
|
||||||
.store(UserFeatureStore, [GraphQLService])
|
|
||||||
.service(InvoicesService)
|
.service(InvoicesService)
|
||||||
.store(InvoicesStore, [GraphQLService])
|
.store(InvoicesStore, [GraphQLService])
|
||||||
.entity(Invoices, [InvoicesStore])
|
.entity(Invoices, [InvoicesStore])
|
||||||
@@ -188,9 +188,9 @@ export function configureCloudModule(framework: Framework) {
|
|||||||
.service(PublicUserService, [PublicUserStore])
|
.service(PublicUserService, [PublicUserStore])
|
||||||
.store(PublicUserStore, [GraphQLService])
|
.store(PublicUserStore, [GraphQLService])
|
||||||
.service(UserSettingsService, [UserSettingsStore])
|
.service(UserSettingsService, [UserSettingsStore])
|
||||||
.store(UserSettingsStore, [GraphQLService])
|
.store(UserSettingsStore, [GraphQLService, NbstoreService])
|
||||||
.service(AccessTokenService, [AccessTokenStore])
|
.service(AccessTokenService, [AccessTokenStore])
|
||||||
.store(AccessTokenStore, [GraphQLService]);
|
.store(AccessTokenStore, [GraphQLService, NbstoreService]);
|
||||||
|
|
||||||
framework
|
framework
|
||||||
.scope(WorkspaceScope)
|
.scope(WorkspaceScope)
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export function mapRealtimeEnum<T extends Record<string, string>>(
|
||||||
|
enumType: T,
|
||||||
|
value: string,
|
||||||
|
label: string
|
||||||
|
): T[keyof T] {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(enumType, value)) {
|
||||||
|
return enumType[value as keyof T];
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown ${label}: ${value}`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Framework } from '@toeverything/infra';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { AccessTokenStore } from '../stores/access-token';
|
||||||
|
import { AccessTokenService } from './access-token';
|
||||||
|
|
||||||
|
function createStore() {
|
||||||
|
return {
|
||||||
|
subscribeUserAccessTokens: vi.fn(() => new Subject()),
|
||||||
|
listUserAccessTokens: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'token-1',
|
||||||
|
name: 'MCP',
|
||||||
|
createdAt: '2026-01-01T00:00:00.000Z',
|
||||||
|
expiresAt: null,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
generateUserAccessToken: vi.fn().mockResolvedValue({
|
||||||
|
id: 'token-1',
|
||||||
|
name: 'MCP',
|
||||||
|
createdAt: '2026-01-01T00:00:00.000Z',
|
||||||
|
expiresAt: null,
|
||||||
|
token: 'secret-token',
|
||||||
|
}),
|
||||||
|
} as unknown as AccessTokenStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AccessTokenService', () => {
|
||||||
|
test('does not store generated plaintext token in the long-lived list', async () => {
|
||||||
|
const framework = new Framework();
|
||||||
|
framework
|
||||||
|
.store(AccessTokenStore, createStore())
|
||||||
|
.service(AccessTokenService, [AccessTokenStore]);
|
||||||
|
const service = framework.provider().get(AccessTokenService);
|
||||||
|
|
||||||
|
const accessToken = await service.generateUserAccessToken('MCP');
|
||||||
|
|
||||||
|
expect(accessToken.token).toBe('secret-token');
|
||||||
|
expect(service.accessTokens$.value).toEqual([
|
||||||
|
{
|
||||||
|
id: 'token-1',
|
||||||
|
name: 'MCP',
|
||||||
|
createdAt: '2026-01-01T00:00:00.000Z',
|
||||||
|
expiresAt: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(JSON.stringify(service.accessTokens$.value)).not.toContain(
|
||||||
|
'secret-token'
|
||||||
|
);
|
||||||
|
|
||||||
|
service.dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,40 +1,48 @@
|
|||||||
import {
|
import { LiveData, OnEvent, Service } from '@toeverything/infra';
|
||||||
effect,
|
|
||||||
exhaustMapWithTrailing,
|
|
||||||
fromPromise,
|
|
||||||
LiveData,
|
|
||||||
onComplete,
|
|
||||||
OnEvent,
|
|
||||||
onStart,
|
|
||||||
Service,
|
|
||||||
smartRetry,
|
|
||||||
} from '@toeverything/infra';
|
|
||||||
import { catchError, EMPTY, tap } from 'rxjs';
|
|
||||||
|
|
||||||
import { AccountChanged } from '../events/account-changed';
|
import { AccountChanged } from '../events/account-changed';
|
||||||
import type { AccessToken, AccessTokenStore } from '../stores/access-token';
|
import { RealtimeLiveQuery } from '../realtime/live-query';
|
||||||
|
import type {
|
||||||
|
AccessToken,
|
||||||
|
AccessTokenStore,
|
||||||
|
ListedAccessToken,
|
||||||
|
} from '../stores/access-token';
|
||||||
|
|
||||||
@OnEvent(AccountChanged, e => e.onAccountChanged)
|
@OnEvent(AccountChanged, e => e.onAccountChanged)
|
||||||
export class AccessTokenService extends Service {
|
export class AccessTokenService extends Service {
|
||||||
constructor(private readonly accessTokenStore: AccessTokenStore) {
|
constructor(private readonly accessTokenStore: AccessTokenStore) {
|
||||||
super();
|
super();
|
||||||
|
this.liveQuery.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
accessTokens$ = new LiveData<AccessToken[] | null>(null);
|
accessTokens$ = new LiveData<ListedAccessToken[] | null>(null);
|
||||||
isRevalidating$ = new LiveData(false);
|
isRevalidating$ = new LiveData(false);
|
||||||
error$ = new LiveData<any>(null);
|
error$ = new LiveData<any>(null);
|
||||||
|
private readonly liveQuery = new RealtimeLiveQuery({
|
||||||
|
request: signal => this.requestAccessTokens(signal),
|
||||||
|
subscribe: () => this.accessTokenStore.subscribeUserAccessTokens(),
|
||||||
|
applySnapshot: accessTokens => {
|
||||||
|
this.error$.value = null;
|
||||||
|
this.accessTokens$.value = accessTokens;
|
||||||
|
},
|
||||||
|
applyEvent: () => 'revalidate' as const,
|
||||||
|
onError: error => {
|
||||||
|
this.error$.value = error;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
async generateUserAccessToken(name: string): Promise<AccessToken> {
|
async generateUserAccessToken(name: string): Promise<AccessToken> {
|
||||||
const accessToken =
|
const accessToken =
|
||||||
await this.accessTokenStore.generateUserAccessToken(name);
|
await this.accessTokenStore.generateUserAccessToken(name);
|
||||||
|
const { token: _token, ...listedAccessToken } = accessToken;
|
||||||
this.accessTokens$.value = [
|
this.accessTokens$.value = [
|
||||||
...(this.accessTokens$.value || []),
|
...(this.accessTokens$.value || []),
|
||||||
accessToken as AccessToken,
|
listedAccessToken,
|
||||||
];
|
];
|
||||||
|
|
||||||
await this.waitForRevalidation();
|
await this.waitForRevalidation();
|
||||||
|
|
||||||
return accessToken as AccessToken;
|
return accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
async revokeUserAccessToken(id: string) {
|
async revokeUserAccessToken(id: string) {
|
||||||
@@ -44,28 +52,9 @@ export class AccessTokenService extends Service {
|
|||||||
await this.waitForRevalidation();
|
await this.waitForRevalidation();
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidate = effect(
|
revalidate = () => {
|
||||||
exhaustMapWithTrailing(() => {
|
this.liveQuery.revalidate();
|
||||||
return fromPromise(() => {
|
};
|
||||||
return this.accessTokenStore.listUserAccessTokens();
|
|
||||||
}).pipe(
|
|
||||||
smartRetry(),
|
|
||||||
tap(accessTokens => {
|
|
||||||
this.accessTokens$.value = accessTokens;
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
this.error$.value = error;
|
|
||||||
return EMPTY;
|
|
||||||
}),
|
|
||||||
onStart(() => {
|
|
||||||
this.isRevalidating$.value = true;
|
|
||||||
}),
|
|
||||||
onComplete(() => {
|
|
||||||
this.isRevalidating$.value = false;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
private onAccountChanged() {
|
private onAccountChanged() {
|
||||||
this.accessTokens$.value = null;
|
this.accessTokens$.value = null;
|
||||||
@@ -79,4 +68,18 @@ export class AccessTokenService extends Service {
|
|||||||
signal
|
signal
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override dispose(): void {
|
||||||
|
super.dispose();
|
||||||
|
this.liveQuery.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requestAccessTokens(signal: AbortSignal) {
|
||||||
|
this.isRevalidating$.value = true;
|
||||||
|
try {
|
||||||
|
return await this.accessTokenStore.listUserAccessTokens(signal);
|
||||||
|
} finally {
|
||||||
|
this.isRevalidating$.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import type { OAuthProviderType } from '@affine/graphql';
|
|||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import { OnEvent, Service } from '@toeverything/infra';
|
import { OnEvent, Service } from '@toeverything/infra';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { distinctUntilChanged, map, skip } from 'rxjs';
|
import { distinctUntilChanged, map, skip, type Subscription } from 'rxjs';
|
||||||
|
|
||||||
import type { GlobalDialogService } from '../../dialogs';
|
import type { GlobalDialogService } from '../../dialogs';
|
||||||
import { ApplicationFocused } from '../../lifecycle';
|
import { ApplicationFocused } from '../../lifecycle';
|
||||||
|
import type { NbstoreService } from '../../storage';
|
||||||
import type { UrlService } from '../../url';
|
import type { UrlService } from '../../url';
|
||||||
import { AuthSession } from '../entities/session';
|
import { AuthSession } from '../entities/session';
|
||||||
import { AccountChanged } from '../events/account-changed';
|
import { AccountChanged } from '../events/account-changed';
|
||||||
@@ -20,12 +21,14 @@ import type { FetchService } from './fetch';
|
|||||||
@OnEvent(ServerStarted, e => e.onServerStarted)
|
@OnEvent(ServerStarted, e => e.onServerStarted)
|
||||||
export class AuthService extends Service {
|
export class AuthService extends Service {
|
||||||
session = this.framework.createEntity(AuthSession);
|
session = this.framework.createEntity(AuthSession);
|
||||||
|
private profileSubscription?: Subscription;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly fetchService: FetchService,
|
private readonly fetchService: FetchService,
|
||||||
private readonly store: AuthStore,
|
private readonly store: AuthStore,
|
||||||
private readonly urlService: UrlService,
|
private readonly urlService: UrlService,
|
||||||
private readonly dialogService: GlobalDialogService
|
private readonly dialogService: GlobalDialogService,
|
||||||
|
private readonly nbstoreService: NbstoreService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -41,21 +44,53 @@ export class AuthService extends Service {
|
|||||||
.subscribe(({ account }) => {
|
.subscribe(({ account }) => {
|
||||||
if (account === null) {
|
if (account === null) {
|
||||||
this.eventBus.emit(AccountLoggedOut, account);
|
this.eventBus.emit(AccountLoggedOut, account);
|
||||||
|
this.profileSubscription?.unsubscribe();
|
||||||
|
this.profileSubscription = undefined;
|
||||||
} else {
|
} else {
|
||||||
this.eventBus.emit(AccountLoggedIn, account);
|
this.eventBus.emit(AccountLoggedIn, account);
|
||||||
|
this.subscribeProfile();
|
||||||
}
|
}
|
||||||
this.eventBus.emit(AccountChanged, account);
|
this.eventBus.emit(AccountChanged, account);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.subscribeProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
private onServerStarted() {
|
private onServerStarted() {
|
||||||
this.session.revalidate();
|
this.session.revalidate();
|
||||||
|
this.subscribeProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
private onApplicationFocused() {
|
private onApplicationFocused() {
|
||||||
this.session.revalidate();
|
this.session.revalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private subscribeProfile() {
|
||||||
|
this.profileSubscription?.unsubscribe();
|
||||||
|
this.profileSubscription = undefined;
|
||||||
|
if (!this.session.account$.value) return;
|
||||||
|
this.profileSubscription = this.nbstoreService.realtime
|
||||||
|
.subscribe('user.profile.changed', {})
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
void (async () => {
|
||||||
|
this.session.revalidate();
|
||||||
|
await this.session.waitForRevalidation();
|
||||||
|
this.eventBus.emit(AccountChanged, this.session.account$.value);
|
||||||
|
})().catch(() => {});
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.profileSubscription = undefined;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override dispose() {
|
||||||
|
super.dispose();
|
||||||
|
this.profileSubscription?.unsubscribe();
|
||||||
|
this.profileSubscription = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
async sendEmailMagicLink(
|
async sendEmailMagicLink(
|
||||||
email: string,
|
email: string,
|
||||||
verifyToken?: string,
|
verifyToken?: string,
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
import {
|
import { LiveData, Service } from '@toeverything/infra';
|
||||||
effect,
|
|
||||||
exhaustMapWithTrailing,
|
|
||||||
fromPromise,
|
|
||||||
LiveData,
|
|
||||||
onComplete,
|
|
||||||
onStart,
|
|
||||||
Service,
|
|
||||||
smartRetry,
|
|
||||||
} from '@toeverything/infra';
|
|
||||||
import { catchError, EMPTY, tap } from 'rxjs';
|
|
||||||
|
|
||||||
|
import { RealtimeLiveQuery } from '../realtime/live-query';
|
||||||
import type {
|
import type {
|
||||||
UpdateUserSettingsInput,
|
UpdateUserSettingsInput,
|
||||||
UserSettings,
|
UserSettings,
|
||||||
@@ -21,34 +12,28 @@ export type { UserSettings };
|
|||||||
export class UserSettingsService extends Service {
|
export class UserSettingsService extends Service {
|
||||||
constructor(private readonly store: UserSettingsStore) {
|
constructor(private readonly store: UserSettingsStore) {
|
||||||
super();
|
super();
|
||||||
|
this.liveQuery.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
userSettings$ = new LiveData<UserSettings | undefined>(undefined);
|
userSettings$ = new LiveData<UserSettings | undefined>(undefined);
|
||||||
isLoading$ = new LiveData<boolean>(false);
|
isLoading$ = new LiveData<boolean>(false);
|
||||||
error$ = new LiveData<any | undefined>(undefined);
|
error$ = new LiveData<any | undefined>(undefined);
|
||||||
|
private readonly liveQuery = new RealtimeLiveQuery({
|
||||||
|
request: signal => this.requestUserSettings(signal),
|
||||||
|
subscribe: () => this.store.subscribeUserSettings(),
|
||||||
|
applySnapshot: settings => {
|
||||||
|
this.error$.value = undefined;
|
||||||
|
this.userSettings$.value = settings;
|
||||||
|
},
|
||||||
|
applyEvent: () => 'revalidate' as const,
|
||||||
|
onError: error => {
|
||||||
|
this.error$.value = error;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
revalidate = effect(
|
revalidate = () => {
|
||||||
exhaustMapWithTrailing(() => {
|
this.liveQuery.revalidate();
|
||||||
return fromPromise(() => {
|
};
|
||||||
return this.store.getUserSettings();
|
|
||||||
}).pipe(
|
|
||||||
smartRetry(),
|
|
||||||
tap(settings => {
|
|
||||||
this.userSettings$.value = settings;
|
|
||||||
}),
|
|
||||||
catchError(error => {
|
|
||||||
this.error$.value = error;
|
|
||||||
return EMPTY;
|
|
||||||
}),
|
|
||||||
onStart(() => {
|
|
||||||
this.isLoading$.value = true;
|
|
||||||
}),
|
|
||||||
onComplete(() => {
|
|
||||||
this.isLoading$.value = false;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
async updateUserSettings(settings: UpdateUserSettingsInput) {
|
async updateUserSettings(settings: UpdateUserSettingsInput) {
|
||||||
await this.store.updateUserSettings(settings);
|
await this.store.updateUserSettings(settings);
|
||||||
@@ -58,4 +43,18 @@ export class UserSettingsService extends Service {
|
|||||||
};
|
};
|
||||||
this.revalidate();
|
this.revalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override dispose(): void {
|
||||||
|
super.dispose();
|
||||||
|
this.liveQuery.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requestUserSettings(signal: AbortSignal) {
|
||||||
|
this.isLoading$.value = true;
|
||||||
|
try {
|
||||||
|
return await this.store.getUserSettings(signal);
|
||||||
|
} finally {
|
||||||
|
this.isLoading$.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,40 @@
|
|||||||
import {
|
import {
|
||||||
generateUserAccessTokenMutation,
|
generateUserAccessTokenMutation,
|
||||||
type ListUserAccessTokensQuery,
|
|
||||||
listUserAccessTokensQuery,
|
|
||||||
revokeUserAccessTokenMutation,
|
revokeUserAccessTokenMutation,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
|
import type { AccessTokenSnapshot } from '@affine/realtime';
|
||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import type { NbstoreService } from '../../storage';
|
||||||
import type { GraphQLService } from '../services/graphql';
|
import type { GraphQLService } from '../services/graphql';
|
||||||
|
|
||||||
export type AccessToken = NonNullable<
|
export type AccessToken = AccessTokenSnapshot & { token: string };
|
||||||
ListUserAccessTokensQuery['currentUser']
|
export type ListedAccessToken = AccessTokenSnapshot;
|
||||||
>['revealedAccessTokens'][number];
|
|
||||||
|
|
||||||
export class AccessTokenStore extends Store {
|
export class AccessTokenStore extends Store {
|
||||||
constructor(private readonly gqlService: GraphQLService) {
|
constructor(
|
||||||
|
private readonly gqlService: GraphQLService,
|
||||||
|
private readonly nbstoreService: NbstoreService
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
async listUserAccessTokens(signal?: AbortSignal): Promise<AccessToken[]> {
|
async listUserAccessTokens(
|
||||||
const data = await this.gqlService.gql({
|
signal?: AbortSignal
|
||||||
query: listUserAccessTokensQuery,
|
): Promise<ListedAccessToken[]> {
|
||||||
context: { signal },
|
const { tokens } = await this.nbstoreService.realtime.request(
|
||||||
});
|
'user.access-tokens.get',
|
||||||
|
{},
|
||||||
|
{ signal, timeoutMs: 10000 }
|
||||||
|
);
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
return data.currentUser?.revealedAccessTokens ?? [];
|
subscribeUserAccessTokens() {
|
||||||
|
return this.nbstoreService.realtime.subscribe(
|
||||||
|
'user.access-tokens.changed',
|
||||||
|
{}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateUserAccessToken(
|
async generateUserAccessToken(
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
deleteAccountMutation,
|
deleteAccountMutation,
|
||||||
removeAvatarMutation,
|
removeAvatarMutation,
|
||||||
|
ServerDeploymentType,
|
||||||
updateUserProfileMutation,
|
updateUserProfileMutation,
|
||||||
uploadAvatarMutation,
|
uploadAvatarMutation,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
import type { GlobalState } from '../../storage';
|
import type { GlobalState, NbstoreService } from '../../storage';
|
||||||
import type { AuthSessionInfo } from '../entities/session';
|
import type { AuthSessionInfo } from '../entities/session';
|
||||||
import type { AuthProvider } from '../provider/auth';
|
import type { AuthProvider } from '../provider/auth';
|
||||||
import type { FetchService } from '../services/fetch';
|
import type { FetchService } from '../services/fetch';
|
||||||
@@ -20,6 +21,7 @@ export interface AccountProfile {
|
|||||||
hasPassword: boolean;
|
hasPassword: boolean;
|
||||||
avatarUrl: string | null;
|
avatarUrl: string | null;
|
||||||
emailVerified: string | null;
|
emailVerified: string | null;
|
||||||
|
features?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthStore extends Store {
|
export class AuthStore extends Store {
|
||||||
@@ -28,7 +30,8 @@ export class AuthStore extends Store {
|
|||||||
private readonly gqlService: GraphQLService,
|
private readonly gqlService: GraphQLService,
|
||||||
private readonly globalState: GlobalState,
|
private readonly globalState: GlobalState,
|
||||||
private readonly serverService: ServerService,
|
private readonly serverService: ServerService,
|
||||||
private readonly authProvider: AuthProvider
|
private readonly authProvider: AuthProvider,
|
||||||
|
private readonly nbstoreService: NbstoreService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -58,20 +61,20 @@ export class AuthStore extends Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchSession() {
|
async fetchSession() {
|
||||||
const url = `/api/auth/session`;
|
const { user } = await this.nbstoreService.realtime.request(
|
||||||
const options: RequestInit = {
|
'user.profile.get',
|
||||||
headers: {
|
{},
|
||||||
'Content-Type': 'application/json',
|
{ timeoutMs: 10000 }
|
||||||
},
|
);
|
||||||
|
return {
|
||||||
|
user: user
|
||||||
|
? {
|
||||||
|
...user,
|
||||||
|
hasPassword: Boolean(user.hasPassword),
|
||||||
|
emailVerified: user.emailVerified ? 'true' : null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await this.fetchService.fetch(url, options);
|
|
||||||
const data = (await res.json()) as {
|
|
||||||
user?: AccountProfile | null;
|
|
||||||
};
|
|
||||||
if (!res.ok)
|
|
||||||
throw new Error('Get session fetch error: ' + JSON.stringify(data));
|
|
||||||
return data; // Return null if data empty
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async signInMagicLink(email: string, token: string) {
|
async signInMagicLink(email: string, token: string) {
|
||||||
@@ -102,6 +105,13 @@ export class AuthStore extends Store {
|
|||||||
|
|
||||||
async signOut() {
|
async signOut() {
|
||||||
await this.authProvider.signOut();
|
await this.authProvider.signOut();
|
||||||
|
await this.nbstoreService.realtime.configure({
|
||||||
|
endpoint: this.serverService.server.baseUrl,
|
||||||
|
authenticated: false,
|
||||||
|
isSelfHosted:
|
||||||
|
this.serverService.server.config$.value.type ===
|
||||||
|
ServerDeploymentType.Selfhosted,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadAvatar(file: File) {
|
async uploadAvatar(file: File) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getCurrentUserProfileQuery } from '@affine/graphql';
|
import { copilotQuotaQuery } from '@affine/graphql';
|
||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
import type { GraphQLService } from '../services/graphql';
|
import type { GraphQLService } from '../services/graphql';
|
||||||
@@ -10,7 +10,7 @@ export class UserCopilotQuotaStore extends Store {
|
|||||||
|
|
||||||
async fetchUserCopilotQuota(abortSignal?: AbortSignal) {
|
async fetchUserCopilotQuota(abortSignal?: AbortSignal) {
|
||||||
const data = await this.graphqlService.gql({
|
const data = await this.graphqlService.gql({
|
||||||
query: getCurrentUserProfileQuery,
|
query: copilotQuotaQuery,
|
||||||
context: {
|
context: {
|
||||||
signal: abortSignal,
|
signal: abortSignal,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { getCurrentUserProfileQuery } from '@affine/graphql';
|
|
||||||
import { Store } from '@toeverything/infra';
|
|
||||||
|
|
||||||
import type { GraphQLService } from '../services/graphql';
|
|
||||||
|
|
||||||
export class UserFeatureStore extends Store {
|
|
||||||
constructor(private readonly gqlService: GraphQLService) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserFeatures(signal: AbortSignal) {
|
|
||||||
const data = await this.gqlService.gql({
|
|
||||||
query: getCurrentUserProfileQuery,
|
|
||||||
context: {
|
|
||||||
signal,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
userId: data.currentUser?.id,
|
|
||||||
features: data.currentUser?.features,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,9 @@
|
|||||||
import { getCurrentUserProfileQuery } from '@affine/graphql';
|
|
||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
import type { NbstoreService } from '../../storage';
|
import type { NbstoreService } from '../../storage';
|
||||||
import type { GraphQLService } from '../services/graphql';
|
|
||||||
|
|
||||||
export class UserQuotaStore extends Store {
|
export class UserQuotaStore extends Store {
|
||||||
constructor(
|
constructor(private readonly nbstoreService: NbstoreService) {
|
||||||
private readonly graphqlService: GraphQLService,
|
|
||||||
private readonly nbstoreService: NbstoreService
|
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,23 +22,4 @@ export class UserQuotaStore extends Store {
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchUserQuota(abortSignal?: AbortSignal) {
|
|
||||||
const data = await this.graphqlService.gql({
|
|
||||||
query: getCurrentUserProfileQuery,
|
|
||||||
context: {
|
|
||||||
signal: abortSignal,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!data.currentUser) {
|
|
||||||
throw new Error('No logged in');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
userId: data.currentUser.id,
|
|
||||||
quota: data.currentUser.quota,
|
|
||||||
used: data.currentUser.quotaUsage.storageQuota,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,38 @@
|
|||||||
import {
|
import {
|
||||||
type GetCurrentUserProfileQuery,
|
|
||||||
getCurrentUserProfileQuery,
|
|
||||||
type UpdateUserSettingsInput,
|
type UpdateUserSettingsInput,
|
||||||
updateUserSettingsMutation,
|
updateUserSettingsMutation,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
|
import type { UserSettingsSnapshot } from '@affine/realtime';
|
||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import type { NbstoreService } from '../../storage';
|
||||||
import type { GraphQLService } from '../services/graphql';
|
import type { GraphQLService } from '../services/graphql';
|
||||||
|
|
||||||
export type UserSettings = NonNullable<
|
export type UserSettings = UserSettingsSnapshot;
|
||||||
GetCurrentUserProfileQuery['currentUser']
|
|
||||||
>['settings'];
|
|
||||||
|
|
||||||
export type { UpdateUserSettingsInput };
|
export type { UpdateUserSettingsInput };
|
||||||
|
|
||||||
export class UserSettingsStore extends Store {
|
export class UserSettingsStore extends Store {
|
||||||
constructor(private readonly gqlService: GraphQLService) {
|
constructor(
|
||||||
|
private readonly gqlService: GraphQLService,
|
||||||
|
private readonly nbstoreService: NbstoreService
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserSettings(): Promise<UserSettings | undefined> {
|
async getUserSettings(
|
||||||
const result = await this.gqlService.gql({
|
signal?: AbortSignal
|
||||||
query: getCurrentUserProfileQuery,
|
): Promise<UserSettings | undefined> {
|
||||||
});
|
const { settings } = await this.nbstoreService.realtime.request(
|
||||||
return result.currentUser?.settings;
|
'user.settings.get',
|
||||||
|
{},
|
||||||
|
{ signal, timeoutMs: 10000 }
|
||||||
|
);
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeUserSettings() {
|
||||||
|
return this.nbstoreService.realtime.subscribe('user.settings.changed', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUserSettings(settings: UpdateUserSettingsInput) {
|
async updateUserSettings(settings: UpdateUserSettingsInput) {
|
||||||
|
|||||||
@@ -150,7 +150,8 @@ export class DocCommentStore extends Entity<{
|
|||||||
return {
|
return {
|
||||||
changes: commentChanges.changes.map(change => ({
|
changes: commentChanges.changes.map(change => ({
|
||||||
id: change.id,
|
id: change.id,
|
||||||
action: change.action,
|
action:
|
||||||
|
change.action as DocCommentChangeListResult['changes'][number]['action'],
|
||||||
comment: normalizeComment(change.item as GQLCommentType),
|
comment: normalizeComment(change.item as GQLCommentType),
|
||||||
commentId: change.commentId ?? undefined,
|
commentId: change.commentId ?? undefined,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import type { GetMembersByWorkspaceIdQuery } from '@affine/graphql';
|
import type { WorkspaceMemberStatus } from '@affine/graphql';
|
||||||
import {
|
import type {
|
||||||
catchErrorInto,
|
RealtimeTopicEventOf,
|
||||||
effect,
|
WorkspaceMemberSnapshot,
|
||||||
Entity,
|
} from '@affine/realtime';
|
||||||
fromPromise,
|
import { Entity, LiveData } from '@toeverything/infra';
|
||||||
LiveData,
|
|
||||||
onComplete,
|
|
||||||
onStart,
|
|
||||||
smartRetry,
|
|
||||||
} from '@toeverything/infra';
|
|
||||||
import { map, switchMap, tap } from 'rxjs';
|
|
||||||
|
|
||||||
|
import { RealtimeLiveQuery } from '../../cloud/realtime/live-query';
|
||||||
import type { WorkspaceService } from '../../workspace';
|
import type { WorkspaceService } from '../../workspace';
|
||||||
import type { WorkspaceMembersStore } from '../stores/members';
|
import type { WorkspaceMembersStore } from '../stores/members';
|
||||||
|
|
||||||
export type Member =
|
export type Member = Omit<
|
||||||
GetMembersByWorkspaceIdQuery['workspace']['members'][number];
|
WorkspaceMemberSnapshot,
|
||||||
|
'permission' | 'role' | 'status'
|
||||||
|
> & {
|
||||||
|
permission: string;
|
||||||
|
role: string;
|
||||||
|
status: WorkspaceMemberStatus;
|
||||||
|
};
|
||||||
|
|
||||||
export class WorkspaceMembers extends Entity {
|
export class WorkspaceMembers extends Entity {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -23,6 +24,7 @@ export class WorkspaceMembers extends Entity {
|
|||||||
private readonly workspaceService: WorkspaceService
|
private readonly workspaceService: WorkspaceService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
this.liveQuery.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
pageNum$ = new LiveData(0);
|
pageNum$ = new LiveData(0);
|
||||||
@@ -34,37 +36,48 @@ export class WorkspaceMembers extends Entity {
|
|||||||
|
|
||||||
readonly PAGE_SIZE = 8;
|
readonly PAGE_SIZE = 8;
|
||||||
|
|
||||||
readonly revalidate = effect(
|
private readonly liveQuery = new RealtimeLiveQuery<
|
||||||
map(() => this.pageNum$.value),
|
{ members: Member[]; memberCount: number },
|
||||||
switchMap(pageNum => {
|
RealtimeTopicEventOf<'workspace.members.changed'>
|
||||||
return fromPromise(async signal => {
|
>({
|
||||||
return this.store.fetchMembers(
|
request: signal => this.requestMembers(signal),
|
||||||
this.workspaceService.workspace.id,
|
subscribe: () =>
|
||||||
pageNum * this.PAGE_SIZE,
|
this.store.subscribeMembers(this.workspaceService.workspace.id),
|
||||||
this.PAGE_SIZE,
|
applySnapshot: data => {
|
||||||
signal
|
this.error$.next(null);
|
||||||
);
|
this.memberCount$.setValue(data.memberCount);
|
||||||
}).pipe(
|
this.pageMembers$.setValue(data.members);
|
||||||
tap(data => {
|
},
|
||||||
this.memberCount$.setValue(data.memberCount);
|
applyEvent: () => 'revalidate',
|
||||||
this.pageMembers$.setValue(data.members);
|
onError: error => this.error$.setValue(error),
|
||||||
}),
|
});
|
||||||
smartRetry(),
|
|
||||||
catchErrorInto(this.error$),
|
revalidate = () => {
|
||||||
onStart(() => {
|
this.liveQuery.revalidate();
|
||||||
this.pageMembers$.setValue(undefined);
|
};
|
||||||
this.isLoading$.setValue(true);
|
|
||||||
}),
|
|
||||||
onComplete(() => this.isLoading$.setValue(false))
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setPageNum(pageNum: number) {
|
setPageNum(pageNum: number) {
|
||||||
this.pageNum$.setValue(pageNum);
|
this.pageNum$.setValue(pageNum);
|
||||||
|
this.revalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
override dispose(): void {
|
override dispose(): void {
|
||||||
this.revalidate.unsubscribe();
|
this.liveQuery.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requestMembers(signal: AbortSignal) {
|
||||||
|
this.pageMembers$.setValue(undefined);
|
||||||
|
this.isLoading$.setValue(true);
|
||||||
|
try {
|
||||||
|
const pageNum = this.pageNum$.value;
|
||||||
|
return await this.store.fetchMembers(
|
||||||
|
this.workspaceService.workspace.id,
|
||||||
|
pageNum * this.PAGE_SIZE,
|
||||||
|
this.PAGE_SIZE,
|
||||||
|
signal
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.isLoading$.setValue(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
onComplete,
|
onComplete,
|
||||||
onStart,
|
onStart,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
|
import type { Subscription } from 'rxjs';
|
||||||
import { tap } from 'rxjs';
|
import { tap } from 'rxjs';
|
||||||
|
|
||||||
import type { WorkspaceService } from '../../workspace';
|
import type { WorkspaceService } from '../../workspace';
|
||||||
@@ -25,12 +26,24 @@ export class WorkspacePermission extends Entity {
|
|||||||
);
|
);
|
||||||
isTeam$ = this.cache$.map(cache => cache?.isTeam ?? null);
|
isTeam$ = this.cache$.map(cache => cache?.isTeam ?? null);
|
||||||
isRevalidating$ = new LiveData(false);
|
isRevalidating$ = new LiveData(false);
|
||||||
|
private readonly subscription?: Subscription;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceService: WorkspaceService,
|
private readonly workspaceService: WorkspaceService,
|
||||||
private readonly store: WorkspacePermissionStore
|
private readonly store: WorkspacePermissionStore
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
if (
|
||||||
|
this.workspaceService.workspace.flavour !== 'local' &&
|
||||||
|
!this.workspaceService.workspace.openOptions.isSharedMode
|
||||||
|
) {
|
||||||
|
this.subscription = this.store
|
||||||
|
.subscribeWorkspaceAccess(this.workspaceService.workspace.id)
|
||||||
|
.subscribe({
|
||||||
|
next: () => this.revalidate(),
|
||||||
|
error: () => {},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidate = effect(
|
revalidate = effect(
|
||||||
@@ -82,5 +95,6 @@ export class WorkspacePermission extends Entity {
|
|||||||
|
|
||||||
override dispose(): void {
|
override dispose(): void {
|
||||||
this.revalidate.unsubscribe();
|
this.revalidate.unsubscribe();
|
||||||
|
this.subscription?.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { type Framework } from '@toeverything/infra';
|
|||||||
|
|
||||||
import { WorkspaceServerService } from '../cloud/services/workspace-server';
|
import { WorkspaceServerService } from '../cloud/services/workspace-server';
|
||||||
import { DocScope, DocService } from '../doc';
|
import { DocScope, DocService } from '../doc';
|
||||||
|
import { NbstoreService } from '../storage';
|
||||||
import {
|
import {
|
||||||
WorkspaceLocalState,
|
WorkspaceLocalState,
|
||||||
WorkspaceScope,
|
WorkspaceScope,
|
||||||
@@ -46,19 +47,24 @@ export function configurePermissionsModule(framework: Framework) {
|
|||||||
.store(WorkspacePermissionStore, [
|
.store(WorkspacePermissionStore, [
|
||||||
WorkspaceServerService,
|
WorkspaceServerService,
|
||||||
WorkspaceLocalState,
|
WorkspaceLocalState,
|
||||||
|
NbstoreService,
|
||||||
])
|
])
|
||||||
.entity(WorkspacePermission, [WorkspaceService, WorkspacePermissionStore])
|
.entity(WorkspacePermission, [WorkspaceService, WorkspacePermissionStore])
|
||||||
.service(WorkspaceMembersService, [WorkspaceMembersStore, WorkspaceService])
|
.service(WorkspaceMembersService, [WorkspaceMembersStore, WorkspaceService])
|
||||||
.store(WorkspaceMembersStore, [WorkspaceServerService])
|
.store(WorkspaceMembersStore, [WorkspaceServerService, NbstoreService])
|
||||||
.entity(WorkspaceMembers, [WorkspaceMembersStore, WorkspaceService])
|
.entity(WorkspaceMembers, [WorkspaceMembersStore, WorkspaceService])
|
||||||
.service(MemberSearchService, [MemberSearchStore, WorkspaceService])
|
.service(MemberSearchService, [MemberSearchStore, WorkspaceService])
|
||||||
.store(MemberSearchStore, [WorkspaceServerService])
|
.store(MemberSearchStore, [NbstoreService])
|
||||||
.service(GuardService, [
|
.service(GuardService, [
|
||||||
GuardStore,
|
GuardStore,
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
WorkspacePermissionService,
|
WorkspacePermissionService,
|
||||||
])
|
])
|
||||||
.store(GuardStore, [WorkspaceService, WorkspaceServerService]);
|
.store(GuardStore, [
|
||||||
|
WorkspaceService,
|
||||||
|
WorkspaceServerService,
|
||||||
|
NbstoreService,
|
||||||
|
]);
|
||||||
|
|
||||||
framework
|
framework
|
||||||
.scope(WorkspaceScope)
|
.scope(WorkspaceScope)
|
||||||
@@ -68,5 +74,5 @@ export function configurePermissionsModule(framework: Framework) {
|
|||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
DocService,
|
DocService,
|
||||||
])
|
])
|
||||||
.store(DocGrantedUsersStore, [WorkspaceServerService]);
|
.store(DocGrantedUsersStore, [WorkspaceServerService, NbstoreService]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { DocRole, type GetPageGrantedUsersListQuery } from '@affine/graphql';
|
import { DocRole } from '@affine/graphql';
|
||||||
|
import type { DocGrantedUserSnapshot } from '@affine/realtime';
|
||||||
import {
|
import {
|
||||||
catchErrorInto,
|
catchErrorInto,
|
||||||
effect,
|
effect,
|
||||||
@@ -9,14 +10,16 @@ import {
|
|||||||
Service,
|
Service,
|
||||||
smartRetry,
|
smartRetry,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
|
import type { Subscription } from 'rxjs';
|
||||||
import { EMPTY, exhaustMap, tap } from 'rxjs';
|
import { EMPTY, exhaustMap, tap } from 'rxjs';
|
||||||
|
|
||||||
import type { DocService } from '../../doc';
|
import type { DocService } from '../../doc';
|
||||||
import type { WorkspaceService } from '../../workspace';
|
import type { WorkspaceService } from '../../workspace';
|
||||||
import type { DocGrantedUsersStore } from '../stores/doc-granted-users';
|
import type { DocGrantedUsersStore } from '../stores/doc-granted-users';
|
||||||
|
|
||||||
export type GrantedUser =
|
export type GrantedUser = Omit<DocGrantedUserSnapshot, 'role'> & {
|
||||||
GetPageGrantedUsersListQuery['workspace']['doc']['grantedUsersList']['edges'][number]['node'];
|
role: DocRole;
|
||||||
|
};
|
||||||
|
|
||||||
export class DocGrantedUsersService extends Service {
|
export class DocGrantedUsersService extends Service {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -25,8 +28,22 @@ export class DocGrantedUsersService extends Service {
|
|||||||
private readonly docService: DocService
|
private readonly docService: DocService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
this.subscription = this.store
|
||||||
|
.subscribeDocGrants(
|
||||||
|
this.workspaceService.workspace.id,
|
||||||
|
this.docService.doc.id
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.reset();
|
||||||
|
this.loadMore();
|
||||||
|
},
|
||||||
|
error: error => this.error$.setValue(error),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly subscription: Subscription;
|
||||||
|
|
||||||
readonly PAGE_SIZE = 8;
|
readonly PAGE_SIZE = 8;
|
||||||
|
|
||||||
nextCursor$ = new LiveData<string | undefined>(undefined);
|
nextCursor$ = new LiveData<string | undefined>(undefined);
|
||||||
@@ -149,5 +166,6 @@ export class DocGrantedUsersService extends Service {
|
|||||||
|
|
||||||
override dispose(): void {
|
override dispose(): void {
|
||||||
this.loadMore.unsubscribe();
|
this.loadMore.unsubscribe();
|
||||||
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Service,
|
Service,
|
||||||
smartRetry,
|
smartRetry,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
|
import type { Subscription } from 'rxjs';
|
||||||
import { EMPTY, exhaustMap, tap } from 'rxjs';
|
import { EMPTY, exhaustMap, tap } from 'rxjs';
|
||||||
|
|
||||||
import type { WorkspaceService } from '../../workspace';
|
import type { WorkspaceService } from '../../workspace';
|
||||||
@@ -20,8 +21,20 @@ export class MemberSearchService extends Service {
|
|||||||
private readonly workspaceService: WorkspaceService
|
private readonly workspaceService: WorkspaceService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
this.subscription = this.store
|
||||||
|
.subscribeMembers(this.workspaceService.workspace.id)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
if (this.searchText$.value) {
|
||||||
|
this.search(this.searchText$.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: error => this.error$.setValue(error),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly subscription: Subscription;
|
||||||
|
|
||||||
readonly PAGE_SIZE = 8;
|
readonly PAGE_SIZE = 8;
|
||||||
readonly searchText$ = new LiveData<string>('');
|
readonly searchText$ = new LiveData<string>('');
|
||||||
readonly isLoading$ = new LiveData(false);
|
readonly isLoading$ = new LiveData(false);
|
||||||
@@ -70,4 +83,9 @@ export class MemberSearchService extends Service {
|
|||||||
this.searchText$.setValue(searchText ?? '');
|
this.searchText$.setValue(searchText ?? '');
|
||||||
this.loadMore();
|
this.loadMore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override dispose(): void {
|
||||||
|
this.loadMore.unsubscribe();
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { WorkspaceServerService } from '@affine/core/modules/cloud';
|
import type { WorkspaceServerService } from '@affine/core/modules/cloud';
|
||||||
import {
|
import {
|
||||||
type DocRole,
|
|
||||||
getPageGrantedUsersListQuery,
|
|
||||||
type GrantDocUserRolesInput,
|
type GrantDocUserRolesInput,
|
||||||
grantDocUserRolesMutation,
|
grantDocUserRolesMutation,
|
||||||
type PaginationInput,
|
type PaginationInput,
|
||||||
@@ -12,8 +10,14 @@ import {
|
|||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import type { NbstoreService } from '../../storage';
|
||||||
|
import { mapDocGrantedUserSnapshot } from './realtime-mappers';
|
||||||
|
|
||||||
export class DocGrantedUsersStore extends Store {
|
export class DocGrantedUsersStore extends Store {
|
||||||
constructor(private readonly workspaceServerService: WorkspaceServerService) {
|
constructor(
|
||||||
|
private readonly workspaceServerService: WorkspaceServerService,
|
||||||
|
private readonly nbstoreService: NbstoreService
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,20 +27,33 @@ export class DocGrantedUsersStore extends Store {
|
|||||||
pagination: PaginationInput,
|
pagination: PaginationInput,
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
) {
|
) {
|
||||||
if (!this.workspaceServerService.server) {
|
return await this.nbstoreService.realtime
|
||||||
throw new Error('No Server');
|
.request(
|
||||||
}
|
'doc.grants.get',
|
||||||
const res = await this.workspaceServerService.server.gql({
|
{
|
||||||
query: getPageGrantedUsersListQuery,
|
workspaceId,
|
||||||
variables: {
|
docId,
|
||||||
workspaceId,
|
pagination: {
|
||||||
docId,
|
first: pagination.first ?? 10,
|
||||||
pagination,
|
offset: pagination.offset ?? 0,
|
||||||
},
|
after: pagination.after ?? undefined,
|
||||||
context: { signal },
|
},
|
||||||
});
|
},
|
||||||
|
{ signal, timeoutMs: 10000 }
|
||||||
|
)
|
||||||
|
.then(data => ({
|
||||||
|
...data,
|
||||||
|
edges: data.edges.map(edge => ({
|
||||||
|
node: mapDocGrantedUserSnapshot(edge.node),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
return res.workspace.doc.grantedUsersList;
|
subscribeDocGrants(workspaceId: string, docId: string) {
|
||||||
|
return this.nbstoreService.realtime.subscribe('doc.grants.changed', {
|
||||||
|
workspaceId,
|
||||||
|
docId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async grantDocUserRoles(input: GrantDocUserRolesInput) {
|
async grantDocUserRoles(input: GrantDocUserRolesInput) {
|
||||||
@@ -75,7 +92,7 @@ export class DocGrantedUsersStore extends Store {
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
docId: string,
|
docId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
role: DocRole
|
role: GrantDocUserRolesInput['role']
|
||||||
) {
|
) {
|
||||||
if (!this.workspaceServerService.server) {
|
if (!this.workspaceServerService.server) {
|
||||||
throw new Error('No Server');
|
throw new Error('No Server');
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
type GetDocRolePermissionsQuery,
|
type GetDocRolePermissionsQuery,
|
||||||
getDocRolePermissionsQuery,
|
getDocRolePermissionsQuery,
|
||||||
type GetWorkspaceInfoQuery,
|
|
||||||
getWorkspaceInfoQuery,
|
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
import type { WorkspaceServerService } from '../../cloud';
|
import type { WorkspaceServerService } from '../../cloud';
|
||||||
|
import type { NbstoreService } from '../../storage';
|
||||||
import type { WorkspaceService } from '../../workspace';
|
import type { WorkspaceService } from '../../workspace';
|
||||||
|
|
||||||
export type WorkspacePermissionActions = keyof Omit<
|
export type WorkspacePermissionActions = string;
|
||||||
GetWorkspaceInfoQuery['workspace']['permissions'],
|
|
||||||
'__typename'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type DocPermissionActions = keyof Omit<
|
export type DocPermissionActions = keyof Omit<
|
||||||
GetDocRolePermissionsQuery['workspace']['doc']['permissions'],
|
GetDocRolePermissionsQuery['workspace']['doc']['permissions'],
|
||||||
@@ -22,7 +18,8 @@ export type DocPermissionActions = keyof Omit<
|
|||||||
export class GuardStore extends Store {
|
export class GuardStore extends Store {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceService: WorkspaceService,
|
private readonly workspaceService: WorkspaceService,
|
||||||
private readonly workspaceServerService: WorkspaceServerService
|
private readonly workspaceServerService: WorkspaceServerService,
|
||||||
|
private readonly nbstoreService: NbstoreService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -30,16 +27,12 @@ export class GuardStore extends Store {
|
|||||||
async getWorkspacePermissions(): Promise<
|
async getWorkspacePermissions(): Promise<
|
||||||
Record<WorkspacePermissionActions, boolean>
|
Record<WorkspacePermissionActions, boolean>
|
||||||
> {
|
> {
|
||||||
if (!this.workspaceServerService.server) {
|
const { access } = await this.nbstoreService.realtime.request(
|
||||||
throw new Error('No server');
|
'workspace.access.get',
|
||||||
}
|
{ workspaceId: this.workspaceService.workspace.id },
|
||||||
const data = await this.workspaceServerService.server.gql({
|
{ timeoutMs: 10000 }
|
||||||
query: getWorkspaceInfoQuery,
|
);
|
||||||
variables: {
|
return access.permissions as Record<WorkspacePermissionActions, boolean>;
|
||||||
workspaceId: this.workspaceService.workspace.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return data.workspace.permissions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDocPermissions(
|
async getDocPermissions(
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { getMembersByWorkspaceIdQuery } from '@affine/graphql';
|
|
||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
import type { WorkspaceServerService } from '../../cloud';
|
import type { NbstoreService } from '../../storage';
|
||||||
|
import { mapWorkspaceMemberSnapshot } from './realtime-mappers';
|
||||||
|
|
||||||
export class MemberSearchStore extends Store {
|
export class MemberSearchStore extends Store {
|
||||||
constructor(private readonly workspaceServerService: WorkspaceServerService) {
|
constructor(private readonly nbstoreService: NbstoreService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,22 +15,21 @@ export class MemberSearchStore extends Store {
|
|||||||
take?: number,
|
take?: number,
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
) {
|
) {
|
||||||
if (!this.workspaceServerService.server) {
|
return await this.nbstoreService.realtime
|
||||||
throw new Error('No Server');
|
.request(
|
||||||
}
|
'workspace.members.get',
|
||||||
const data = await this.workspaceServerService.server.gql({
|
{ workspaceId, skip, take, query },
|
||||||
query: getMembersByWorkspaceIdQuery,
|
{ signal, timeoutMs: 10000 }
|
||||||
variables: {
|
)
|
||||||
workspaceId,
|
.then(data => ({
|
||||||
skip,
|
...data,
|
||||||
take,
|
members: data.members.map(mapWorkspaceMemberSnapshot),
|
||||||
query,
|
}));
|
||||||
},
|
}
|
||||||
context: {
|
|
||||||
signal,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return data.workspace;
|
subscribeMembers(workspaceId: string) {
|
||||||
|
return this.nbstoreService.realtime.subscribe('workspace.members.changed', {
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
approveWorkspaceTeamMemberMutation,
|
approveWorkspaceTeamMemberMutation,
|
||||||
createInviteLinkMutation,
|
createInviteLinkMutation,
|
||||||
getMembersByWorkspaceIdQuery,
|
|
||||||
grantWorkspaceTeamMemberMutation,
|
grantWorkspaceTeamMemberMutation,
|
||||||
inviteByEmailsMutation,
|
inviteByEmailsMutation,
|
||||||
type Permission,
|
type Permission,
|
||||||
@@ -12,9 +11,14 @@ import {
|
|||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
import type { WorkspaceServerService } from '../../cloud';
|
import type { WorkspaceServerService } from '../../cloud';
|
||||||
|
import type { NbstoreService } from '../../storage';
|
||||||
|
import { mapWorkspaceMemberSnapshot } from './realtime-mappers';
|
||||||
|
|
||||||
export class WorkspaceMembersStore extends Store {
|
export class WorkspaceMembersStore extends Store {
|
||||||
constructor(private readonly workspaceServerService: WorkspaceServerService) {
|
constructor(
|
||||||
|
private readonly workspaceServerService: WorkspaceServerService,
|
||||||
|
private readonly nbstoreService: NbstoreService
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,22 +28,22 @@ export class WorkspaceMembersStore extends Store {
|
|||||||
take: number,
|
take: number,
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
) {
|
) {
|
||||||
if (!this.workspaceServerService.server) {
|
return await this.nbstoreService.realtime
|
||||||
throw new Error('No Server');
|
.request(
|
||||||
}
|
'workspace.members.get',
|
||||||
const data = await this.workspaceServerService.server.gql({
|
{ workspaceId, skip, take },
|
||||||
query: getMembersByWorkspaceIdQuery,
|
{ signal, timeoutMs: 10000 }
|
||||||
variables: {
|
)
|
||||||
workspaceId,
|
.then(data => ({
|
||||||
skip,
|
...data,
|
||||||
take,
|
members: data.members.map(mapWorkspaceMemberSnapshot),
|
||||||
},
|
}));
|
||||||
context: {
|
}
|
||||||
signal,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return data.workspace;
|
subscribeMembers(workspaceId: string) {
|
||||||
|
return this.nbstoreService.realtime.subscribe('workspace.members.changed', {
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async inviteBatch(workspaceId: string, emails: string[]) {
|
async inviteBatch(workspaceId: string, emails: string[]) {
|
||||||
|
|||||||
@@ -1,30 +1,32 @@
|
|||||||
import type { WorkspaceServerService } from '@affine/core/modules/cloud';
|
import type { WorkspaceServerService } from '@affine/core/modules/cloud';
|
||||||
import { getWorkspaceInfoQuery, leaveWorkspaceMutation } from '@affine/graphql';
|
import { leaveWorkspaceMutation } from '@affine/graphql';
|
||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import type { NbstoreService } from '../../storage';
|
||||||
import type { WorkspaceLocalState } from '../../workspace';
|
import type { WorkspaceLocalState } from '../../workspace';
|
||||||
|
|
||||||
export class WorkspacePermissionStore extends Store {
|
export class WorkspacePermissionStore extends Store {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceServerService: WorkspaceServerService,
|
private readonly workspaceServerService: WorkspaceServerService,
|
||||||
private readonly workspaceLocalState: WorkspaceLocalState
|
private readonly workspaceLocalState: WorkspaceLocalState,
|
||||||
|
private readonly nbstoreService: NbstoreService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchWorkspaceInfo(workspaceId: string, signal?: AbortSignal) {
|
async fetchWorkspaceInfo(workspaceId: string, signal?: AbortSignal) {
|
||||||
if (!this.workspaceServerService.server) {
|
const { access } = await this.nbstoreService.realtime.request(
|
||||||
throw new Error('No Server');
|
'workspace.access.get',
|
||||||
}
|
{ workspaceId },
|
||||||
const info = await this.workspaceServerService.server.gql({
|
{ signal, timeoutMs: 10000 }
|
||||||
query: getWorkspaceInfoQuery,
|
);
|
||||||
variables: {
|
return { workspace: access };
|
||||||
workspaceId,
|
}
|
||||||
},
|
|
||||||
context: { signal },
|
|
||||||
});
|
|
||||||
|
|
||||||
return info;
|
subscribeWorkspaceAccess(workspaceId: string) {
|
||||||
|
return this.nbstoreService.realtime.subscribe('workspace.access.changed', {
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { DocRole, Permission, WorkspaceMemberStatus } from '@affine/graphql';
|
||||||
|
import type {
|
||||||
|
DocGrantedUserSnapshot,
|
||||||
|
WorkspaceMemberSnapshot,
|
||||||
|
} from '@affine/realtime';
|
||||||
|
|
||||||
|
import { mapRealtimeEnum } from '../../cloud/realtime/enum';
|
||||||
|
|
||||||
|
export function mapWorkspaceMemberSnapshot(member: WorkspaceMemberSnapshot) {
|
||||||
|
return {
|
||||||
|
...member,
|
||||||
|
permission: mapRealtimeEnum(Permission, member.permission, 'permission'),
|
||||||
|
role: mapRealtimeEnum(Permission, member.role, 'permission'),
|
||||||
|
status: mapRealtimeEnum(
|
||||||
|
WorkspaceMemberStatus,
|
||||||
|
member.status,
|
||||||
|
'workspace member status'
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapDocGrantedUserSnapshot(node: DocGrantedUserSnapshot) {
|
||||||
|
return { ...node, role: mapRealtimeEnum(DocRole, node.role, 'doc role') };
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import type { WorkspaceQuotaQuery } from '@affine/graphql';
|
|
||||||
import type { WorkspaceQuotaStateSnapshot } from '@affine/realtime';
|
import type { WorkspaceQuotaStateSnapshot } from '@affine/realtime';
|
||||||
import { Framework } from '@toeverything/infra';
|
import { Framework } from '@toeverything/infra';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
@@ -8,8 +7,6 @@ import { describe, expect, test, vi } from 'vitest';
|
|||||||
import { WorkspaceQuotaStore } from '../stores/quota';
|
import { WorkspaceQuotaStore } from '../stores/quota';
|
||||||
import { WorkspaceQuota } from './quota';
|
import { WorkspaceQuota } from './quota';
|
||||||
|
|
||||||
type Quota = WorkspaceQuotaQuery['workspace']['quota'];
|
|
||||||
|
|
||||||
const workspaceService = {
|
const workspaceService = {
|
||||||
workspace: { id: 'workspace-1' },
|
workspace: { id: 'workspace-1' },
|
||||||
} as unknown as WorkspaceService;
|
} as unknown as WorkspaceService;
|
||||||
@@ -43,29 +40,6 @@ function createQuotaState(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createQuota(overrides: Partial<Quota> = {}): Quota {
|
|
||||||
return {
|
|
||||||
name: 'Legacy',
|
|
||||||
blobLimit: 1024,
|
|
||||||
storageQuota: 2048,
|
|
||||||
usedStorageQuota: 256,
|
|
||||||
historyPeriod: 30 * 24 * 60 * 60 * 1000,
|
|
||||||
memberLimit: 8,
|
|
||||||
memberCount: 2,
|
|
||||||
overcapacityMemberCount: 0,
|
|
||||||
humanReadable: {
|
|
||||||
name: 'Legacy',
|
|
||||||
blobLimit: '1 KB',
|
|
||||||
storageQuota: '2 KB',
|
|
||||||
historyPeriod: '30 days',
|
|
||||||
memberLimit: '8',
|
|
||||||
memberCount: '2',
|
|
||||||
overcapacityMemberCount: '0',
|
|
||||||
},
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createStore(
|
function createStore(
|
||||||
overrides: Partial<WorkspaceQuotaStore>,
|
overrides: Partial<WorkspaceQuotaStore>,
|
||||||
eventSubject = new Subject<{ type: 'ready' } | { changed: true }>()
|
eventSubject = new Subject<{ type: 'ready' } | { changed: true }>()
|
||||||
@@ -73,7 +47,6 @@ function createStore(
|
|||||||
return {
|
return {
|
||||||
fetchWorkspaceQuotaState: vi.fn(),
|
fetchWorkspaceQuotaState: vi.fn(),
|
||||||
subscribeWorkspaceQuotaState: vi.fn(() => eventSubject),
|
subscribeWorkspaceQuotaState: vi.fn(() => eventSubject),
|
||||||
fetchWorkspaceQuota: vi.fn(),
|
|
||||||
...overrides,
|
...overrides,
|
||||||
} as unknown as WorkspaceQuotaStore;
|
} as unknown as WorkspaceQuotaStore;
|
||||||
}
|
}
|
||||||
@@ -110,21 +83,20 @@ describe('WorkspaceQuota', () => {
|
|||||||
await vi.waitFor(() => expect(quota.quota$.value?.memberCount).toBe(4));
|
await vi.waitFor(() => expect(quota.quota$.value?.memberCount).toBe(4));
|
||||||
|
|
||||||
expect(store.fetchWorkspaceQuotaState).toHaveBeenCalledTimes(2);
|
expect(store.fetchWorkspaceQuotaState).toHaveBeenCalledTimes(2);
|
||||||
expect(store.fetchWorkspaceQuota).not.toHaveBeenCalled();
|
|
||||||
quota.dispose();
|
quota.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('falls back to legacy GraphQL quota when realtime request fails', async () => {
|
test('surfaces realtime quota errors without GraphQL fallback', async () => {
|
||||||
|
const error = new Error('offline');
|
||||||
const store = createStore({
|
const store = createStore({
|
||||||
fetchWorkspaceQuotaState: vi.fn().mockRejectedValue(new Error('offline')),
|
fetchWorkspaceQuotaState: vi.fn().mockRejectedValue(error),
|
||||||
fetchWorkspaceQuota: vi.fn().mockResolvedValue(createQuota()),
|
|
||||||
});
|
});
|
||||||
const quota = createEntity(store);
|
const quota = createEntity(store);
|
||||||
|
|
||||||
quota.revalidate();
|
quota.revalidate();
|
||||||
|
|
||||||
await vi.waitFor(() => expect(quota.quota$.value?.name).toBe('Legacy'));
|
await vi.waitFor(() => expect(quota.error$.value).toBe(error));
|
||||||
expect(quota.error$.value).toBeNull();
|
expect(quota.quota$.value).toBeNull();
|
||||||
quota.dispose();
|
quota.dispose();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
import type { WorkspaceQuotaQuery } from '@affine/graphql';
|
|
||||||
import type {
|
import type {
|
||||||
RealtimeTopicEventOf,
|
RealtimeTopicEventOf,
|
||||||
WorkspaceQuotaStateSnapshot,
|
WorkspaceQuotaStateSnapshot,
|
||||||
@@ -12,7 +11,25 @@ import { RealtimeLiveQuery } from '../../cloud/realtime/live-query';
|
|||||||
import type { WorkspaceService } from '../../workspace';
|
import type { WorkspaceService } from '../../workspace';
|
||||||
import type { WorkspaceQuotaStore } from '../stores/quota';
|
import type { WorkspaceQuotaStore } from '../stores/quota';
|
||||||
|
|
||||||
type QuotaType = WorkspaceQuotaQuery['workspace']['quota'];
|
type QuotaType = {
|
||||||
|
name: string;
|
||||||
|
blobLimit: number;
|
||||||
|
storageQuota: number;
|
||||||
|
usedStorageQuota: number;
|
||||||
|
historyPeriod: number;
|
||||||
|
memberLimit: number;
|
||||||
|
memberCount: number;
|
||||||
|
overcapacityMemberCount: number;
|
||||||
|
humanReadable: {
|
||||||
|
name: string;
|
||||||
|
blobLimit: string;
|
||||||
|
storageQuota: string;
|
||||||
|
historyPeriod: string;
|
||||||
|
memberLimit: string;
|
||||||
|
memberCount: string;
|
||||||
|
overcapacityMemberCount: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const logger = new DebugLogger('affine:workspace-permission');
|
const logger = new DebugLogger('affine:workspace-permission');
|
||||||
const DAY_SECONDS = 24 * 60 * 60;
|
const DAY_SECONDS = 24 * 60 * 60;
|
||||||
@@ -172,11 +189,6 @@ export class WorkspaceQuota extends Entity {
|
|||||||
signal
|
signal
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch {
|
|
||||||
return await this.store.fetchWorkspaceQuota(
|
|
||||||
this.workspaceService.workspace.id,
|
|
||||||
signal
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
this.isRevalidating$.setValue(false);
|
this.isRevalidating$.setValue(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ export { QuotaCheck } from './views/quota-check';
|
|||||||
|
|
||||||
import { type Framework } from '@toeverything/infra';
|
import { type Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
import { WorkspaceServerService } from '../cloud';
|
|
||||||
import { NbstoreService } from '../storage';
|
import { NbstoreService } from '../storage';
|
||||||
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
||||||
import { WorkspaceQuota } from './entities/quota';
|
import { WorkspaceQuota } from './entities/quota';
|
||||||
@@ -14,6 +13,6 @@ export function configureQuotaModule(framework: Framework) {
|
|||||||
framework
|
framework
|
||||||
.scope(WorkspaceScope)
|
.scope(WorkspaceScope)
|
||||||
.service(WorkspaceQuotaService)
|
.service(WorkspaceQuotaService)
|
||||||
.store(WorkspaceQuotaStore, [WorkspaceServerService, NbstoreService])
|
.store(WorkspaceQuotaStore, [NbstoreService])
|
||||||
.entity(WorkspaceQuota, [WorkspaceService, WorkspaceQuotaStore]);
|
.entity(WorkspaceQuota, [WorkspaceService, WorkspaceQuotaStore]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import type { WorkspaceServerService } from '@affine/core/modules/cloud';
|
|
||||||
import { workspaceQuotaQuery } from '@affine/graphql';
|
|
||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
import type { NbstoreService } from '../../storage';
|
import type { NbstoreService } from '../../storage';
|
||||||
|
|
||||||
export class WorkspaceQuotaStore extends Store {
|
export class WorkspaceQuotaStore extends Store {
|
||||||
constructor(
|
constructor(private readonly nbstoreService: NbstoreService) {
|
||||||
private readonly workspaceServerService: WorkspaceServerService,
|
|
||||||
private readonly nbstoreService: NbstoreService
|
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,20 +22,4 @@ export class WorkspaceQuotaStore extends Store {
|
|||||||
{ workspaceId }
|
{ workspaceId }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchWorkspaceQuota(workspaceId: string, signal?: AbortSignal) {
|
|
||||||
if (!this.workspaceServerService.server) {
|
|
||||||
throw new Error('No Server');
|
|
||||||
}
|
|
||||||
const data = await this.workspaceServerService.server.gql({
|
|
||||||
query: workspaceQuotaQuery,
|
|
||||||
variables: {
|
|
||||||
id: workspaceId,
|
|
||||||
},
|
|
||||||
context: {
|
|
||||||
signal,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return data.workspace.quota;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user