feat: add copilot feature type (#5465)

This commit is contained in:
DarkSky
2024-01-04 10:36:34 +00:00
parent df09dac389
commit f5b74ca8a9
13 changed files with 186 additions and 133 deletions

View File

@@ -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<F extends FeatureType> = InstanceType<
(typeof FeatureConfigMap)[F]
>;
const FeatureCache = new Map<number, FeatureConfigType<FeatureType>>();
export async function getFeature(prisma: PrismaService, featureId: number) {
const cachedQuota = FeatureCache.get(featureId);

View File

@@ -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<F extends FeatureType>(
feature: F
): Promise<FeatureConfigType<F> | 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<F>;
}
return undefined;
}

View File

@@ -0,0 +1,4 @@
export enum FeatureType {
Copilot = 'copilot',
EarlyAccess = 'early_access',
}

View File

@@ -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({}),
});

View File

@@ -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(),
}),
});

View File

@@ -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<typeof commonFeatureSchema>;
/// ======== 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<typeof FeatureSchema>;
export { FeatureType };

View File

@@ -27,4 +27,5 @@ import {
exports: [PermissionService],
})
export class WorkspaceModule {}
export { InvitationType, WorkspaceType } from './resolvers';
export type { InvitationType, WorkspaceType } from './types';

View File

@@ -34,6 +34,9 @@ export class PermissionService {
accepted: true,
type: Permission.Owner,
},
select: {
workspaceId: true,
},
})
.then(data => data.map(({ workspaceId }) => workspaceId));
}

View File

@@ -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()

View File

@@ -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<SnapshotHistory> {

View File

@@ -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',

View File

@@ -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<Workspace> {
@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

View File

@@ -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<Workspace> {
@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;
}