diff --git a/packages/backend/server/src/modules/features/feature.ts b/packages/backend/server/src/modules/features/feature.ts index f0bd2a763c..b04654f8d6 100644 --- a/packages/backend/server/src/modules/features/feature.ts +++ b/packages/backend/server/src/modules/features/feature.ts @@ -19,7 +19,20 @@ class FeatureConfig { } } +export class CopilotFeatureConfig extends FeatureConfig { + override config!: Feature & { feature: FeatureType.Copilot }; + constructor(data: any) { + super(data); + + if (this.config.feature !== FeatureType.Copilot) { + throw new Error('Invalid feature config: type is not Copilot'); + } + } +} + export class EarlyAccessFeatureConfig extends FeatureConfig { + override config!: Feature & { feature: FeatureType.EarlyAccess }; + constructor(data: any) { super(data); @@ -39,13 +52,15 @@ export class EarlyAccessFeatureConfig extends FeatureConfig { } const FeatureConfigMap = { + [FeatureType.Copilot]: CopilotFeatureConfig, [FeatureType.EarlyAccess]: EarlyAccessFeatureConfig, }; -const FeatureCache = new Map< - number, - InstanceType<(typeof FeatureConfigMap)[FeatureType]> ->(); +export type FeatureConfigType = InstanceType< + (typeof FeatureConfigMap)[F] +>; + +const FeatureCache = new Map>(); export async function getFeature(prisma: PrismaService, featureId: number) { const cachedQuota = FeatureCache.get(featureId); diff --git a/packages/backend/server/src/modules/features/service.ts b/packages/backend/server/src/modules/features/service.ts index beed9dc4fb..1dae214a08 100644 --- a/packages/backend/server/src/modules/features/service.ts +++ b/packages/backend/server/src/modules/features/service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '../../prisma'; import { UserType } from '../users/types'; -import { getFeature } from './feature'; +import { FeatureConfigType, getFeature } from './feature'; import { FeatureKind, FeatureType } from './types'; @Injectable() @@ -28,7 +28,9 @@ export class FeatureService { ); } - async getFeature(feature: FeatureType) { + async getFeature( + feature: F + ): Promise | undefined> { const data = await this.prisma.features.findFirst({ where: { feature, @@ -40,7 +42,7 @@ export class FeatureService { }, }); if (data) { - return getFeature(this.prisma, data.id); + return getFeature(this.prisma, data.id) as FeatureConfigType; } return undefined; } diff --git a/packages/backend/server/src/modules/features/types/common.ts b/packages/backend/server/src/modules/features/types/common.ts new file mode 100644 index 0000000000..448ff156af --- /dev/null +++ b/packages/backend/server/src/modules/features/types/common.ts @@ -0,0 +1,4 @@ +export enum FeatureType { + Copilot = 'copilot', + EarlyAccess = 'early_access', +} diff --git a/packages/backend/server/src/modules/features/types/copilot.ts b/packages/backend/server/src/modules/features/types/copilot.ts new file mode 100644 index 0000000000..0a58096354 --- /dev/null +++ b/packages/backend/server/src/modules/features/types/copilot.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +import { FeatureType } from './common'; + +export const featureCopilot = z.object({ + feature: z.literal(FeatureType.Copilot), + configs: z.object({}), +}); diff --git a/packages/backend/server/src/modules/features/types/early-access.ts b/packages/backend/server/src/modules/features/types/early-access.ts new file mode 100644 index 0000000000..a3fb3a19fb --- /dev/null +++ b/packages/backend/server/src/modules/features/types/early-access.ts @@ -0,0 +1,24 @@ +import { URL } from 'node:url'; + +import { z } from 'zod'; + +import { FeatureType } from './common'; + +function checkHostname(host: string) { + try { + return new URL(`https://${host}`).hostname === host; + } catch (_) { + return false; + } +} + +export const featureEarlyAccess = z.object({ + feature: z.literal(FeatureType.EarlyAccess), + configs: z.object({ + whitelist: z + .string() + .startsWith('@') + .refine(domain => checkHostname(domain.slice(1))) + .array(), + }), +}); diff --git a/packages/backend/server/src/modules/features/types.ts b/packages/backend/server/src/modules/features/types/index.ts similarity index 58% rename from packages/backend/server/src/modules/features/types.ts rename to packages/backend/server/src/modules/features/types/index.ts index 007463b07d..2f445b43ec 100644 --- a/packages/backend/server/src/modules/features/types.ts +++ b/packages/backend/server/src/modules/features/types/index.ts @@ -1,7 +1,9 @@ -import { URL } from 'node:url'; - import { z } from 'zod'; +import { FeatureType } from './common'; +import { featureCopilot } from './copilot'; +import { featureEarlyAccess } from './early-access'; + /// ======== common schema ======== export enum FeatureKind { @@ -20,30 +22,13 @@ export type CommonFeature = z.infer; /// ======== feature define ======== -export enum FeatureType { - EarlyAccess = 'early_access', -} - -function checkHostname(host: string) { - try { - return new URL(`https://${host}`).hostname === host; - } catch (_) { - return false; - } -} - -const featureEarlyAccess = z.object({ - feature: z.literal(FeatureType.EarlyAccess), - configs: z.object({ - whitelist: z - .string() - .startsWith('@') - .refine(domain => checkHostname(domain.slice(1))) - .array(), - }), -}); - export const Features: Feature[] = [ + { + feature: FeatureType.Copilot, + type: FeatureKind.Feature, + version: 1, + configs: {}, + }, { feature: FeatureType.EarlyAccess, type: FeatureKind.Feature, @@ -60,6 +45,8 @@ export const FeatureSchema = commonFeatureSchema .extend({ type: z.literal(FeatureKind.Feature), }) - .and(z.discriminatedUnion('feature', [featureEarlyAccess])); + .and(z.discriminatedUnion('feature', [featureCopilot, featureEarlyAccess])); export type Feature = z.infer; + +export { FeatureType }; diff --git a/packages/backend/server/src/modules/workspaces/index.ts b/packages/backend/server/src/modules/workspaces/index.ts index ecb09cb73a..6585c6e9d0 100644 --- a/packages/backend/server/src/modules/workspaces/index.ts +++ b/packages/backend/server/src/modules/workspaces/index.ts @@ -27,4 +27,5 @@ import { exports: [PermissionService], }) export class WorkspaceModule {} -export { InvitationType, WorkspaceType } from './resolvers'; + +export type { InvitationType, WorkspaceType } from './types'; diff --git a/packages/backend/server/src/modules/workspaces/permission.ts b/packages/backend/server/src/modules/workspaces/permission.ts index 524ab37e39..1248075b1c 100644 --- a/packages/backend/server/src/modules/workspaces/permission.ts +++ b/packages/backend/server/src/modules/workspaces/permission.ts @@ -34,6 +34,9 @@ export class PermissionService { accepted: true, type: Permission.Owner, }, + select: { + workspaceId: true, + }, }) .then(data => data.map(({ workspaceId }) => workspaceId)); } diff --git a/packages/backend/server/src/modules/workspaces/resolvers/blob.ts b/packages/backend/server/src/modules/workspaces/resolvers/blob.ts index 6d86c3b5a9..d2a03994ac 100644 --- a/packages/backend/server/src/modules/workspaces/resolvers/blob.ts +++ b/packages/backend/server/src/modules/workspaces/resolvers/blob.ts @@ -19,8 +19,7 @@ import { QuotaManagementService } from '../../quota'; import { WorkspaceBlobStorage } from '../../storage'; import { UserType } from '../../users'; import { PermissionService } from '../permission'; -import { Permission } from '../types'; -import { WorkspaceBlobSizes, WorkspaceType } from './workspace'; +import { Permission, WorkspaceBlobSizes, WorkspaceType } from '../types'; @UseGuards(CloudThrottlerGuard) @Auth() diff --git a/packages/backend/server/src/modules/workspaces/resolvers/history.ts b/packages/backend/server/src/modules/workspaces/resolvers/history.ts index 77a3415bd7..e7940f9df1 100644 --- a/packages/backend/server/src/modules/workspaces/resolvers/history.ts +++ b/packages/backend/server/src/modules/workspaces/resolvers/history.ts @@ -18,8 +18,7 @@ import { Auth, CurrentUser } from '../../auth'; import { DocHistoryManager } from '../../doc/history'; import { UserType } from '../../users'; import { PermissionService } from '../permission'; -import { Permission } from '../types'; -import { WorkspaceType } from './workspace'; +import { Permission, WorkspaceType } from '../types'; @ObjectType() class DocHistoryType implements Partial { diff --git a/packages/backend/server/src/modules/workspaces/resolvers/page.ts b/packages/backend/server/src/modules/workspaces/resolvers/page.ts index deb138c98c..c37bc4049c 100644 --- a/packages/backend/server/src/modules/workspaces/resolvers/page.ts +++ b/packages/backend/server/src/modules/workspaces/resolvers/page.ts @@ -17,8 +17,7 @@ import { DocID } from '../../../utils/doc'; import { Auth, CurrentUser } from '../../auth'; import { UserType } from '../../users'; import { PermissionService, PublicPageMode } from '../permission'; -import { Permission } from '../types'; -import { WorkspaceType } from './workspace'; +import { Permission, WorkspaceType } from '../types'; registerEnumType(PublicPageMode, { name: 'PublicPageMode', diff --git a/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts b/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts index 71039e9857..eb772a66b3 100644 --- a/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts +++ b/packages/backend/server/src/modules/workspaces/resolvers/workspace.ts @@ -7,23 +7,14 @@ import { } from '@nestjs/common'; import { Args, - Field, - Float, - ID, - InputType, Int, Mutation, - ObjectType, - OmitType, Parent, - PartialType, - PickType, Query, - registerEnumType, ResolveField, Resolver, } from '@nestjs/graphql'; -import type { User, Workspace } from '@prisma/client'; +import type { User } from '@prisma/client'; import { getStreamAsBuffer } from 'get-stream'; import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; import { applyUpdate, Doc } from 'yjs'; @@ -38,91 +29,15 @@ import { AuthService } from '../../auth/service'; import { WorkspaceBlobStorage } from '../../storage'; import { UsersService, UserType } from '../../users'; import { PermissionService } from '../permission'; -import { Permission } from '../types'; +import { + InvitationType, + InviteUserType, + Permission, + UpdateWorkspaceInput, + WorkspaceType, +} from '../types'; import { defaultWorkspaceAvatar } from '../utils'; -registerEnumType(Permission, { - name: 'Permission', - description: 'User permission in workspace', -}); - -@ObjectType() -export class InviteUserType extends OmitType( - PartialType(UserType), - ['id'], - ObjectType -) { - @Field(() => ID) - id!: string; - - @Field(() => Permission, { description: 'User permission in workspace' }) - permission!: Permission; - - @Field({ description: 'Invite id' }) - inviteId!: string; - - @Field({ description: 'User accepted' }) - accepted!: boolean; -} - -@ObjectType() -export class WorkspaceType implements Partial { - @Field(() => ID) - id!: string; - - @Field({ description: 'is Public workspace' }) - public!: boolean; - - @Field({ description: 'Workspace created date' }) - createdAt!: Date; - - @Field(() => [InviteUserType], { - description: 'Members of workspace', - }) - members!: InviteUserType[]; -} - -@ObjectType() -export class InvitationWorkspaceType { - @Field(() => ID) - id!: string; - - @Field({ description: 'Workspace name' }) - name!: string; - - @Field(() => String, { - // nullable: true, - description: 'Base64 encoded avatar', - }) - avatar!: string; -} - -@ObjectType() -export class WorkspaceBlobSizes { - @Field(() => Float) - size!: number; -} - -@ObjectType() -export class InvitationType { - @Field({ description: 'Workspace information' }) - workspace!: InvitationWorkspaceType; - @Field({ description: 'User information' }) - user!: UserType; - @Field({ description: 'Invitee information' }) - invitee!: UserType; -} - -@InputType() -export class UpdateWorkspaceInput extends PickType( - PartialType(WorkspaceType), - ['public'], - InputType -) { - @Field(() => ID) - id!: string; -} - /** * Workspace resolver * Public apis rate limit: 10 req/m diff --git a/packages/backend/server/src/modules/workspaces/types.ts b/packages/backend/server/src/modules/workspaces/types.ts index a86fcb6c2b..c3492f4230 100644 --- a/packages/backend/server/src/modules/workspaces/types.ts +++ b/packages/backend/server/src/modules/workspaces/types.ts @@ -1,6 +1,103 @@ +import { + Field, + Float, + ID, + InputType, + ObjectType, + OmitType, + PartialType, + PickType, + registerEnumType, +} from '@nestjs/graphql'; +import type { Workspace } from '@prisma/client'; + +import { UserType } from '../users/types'; + export enum Permission { Read = 0, Write = 1, Admin = 10, Owner = 99, } + +registerEnumType(Permission, { + name: 'Permission', + description: 'User permission in workspace', +}); + +@ObjectType() +export class InviteUserType extends OmitType( + PartialType(UserType), + ['id'], + ObjectType +) { + @Field(() => ID) + id!: string; + + @Field(() => Permission, { description: 'User permission in workspace' }) + permission!: Permission; + + @Field({ description: 'Invite id' }) + inviteId!: string; + + @Field({ description: 'User accepted' }) + accepted!: boolean; +} + +@ObjectType() +export class WorkspaceType implements Partial { + @Field(() => ID) + id!: string; + + @Field({ description: 'is Public workspace' }) + public!: boolean; + + @Field({ description: 'Workspace created date' }) + createdAt!: Date; + + @Field(() => [InviteUserType], { + description: 'Members of workspace', + }) + members!: InviteUserType[]; +} + +@ObjectType() +export class InvitationWorkspaceType { + @Field(() => ID) + id!: string; + + @Field({ description: 'Workspace name' }) + name!: string; + + @Field(() => String, { + // nullable: true, + description: 'Base64 encoded avatar', + }) + avatar!: string; +} + +@ObjectType() +export class WorkspaceBlobSizes { + @Field(() => Float) + size!: number; +} + +@ObjectType() +export class InvitationType { + @Field({ description: 'Workspace information' }) + workspace!: InvitationWorkspaceType; + @Field({ description: 'User information' }) + user!: UserType; + @Field({ description: 'Invitee information' }) + invitee!: UserType; +} + +@InputType() +export class UpdateWorkspaceInput extends PickType( + PartialType(WorkspaceType), + ['public'], + InputType +) { + @Field(() => ID) + id!: string; +}