feat: add workspace experimental features api (#5525)

This commit is contained in:
DarkSky
2024-01-06 11:04:49 +00:00
parent 9650a5a6a1
commit 443908da22
10 changed files with 259 additions and 59 deletions
@@ -19,7 +19,13 @@ import { Auth, CurrentUser, Public, Publicable } from '../auth/guard';
import { FeatureManagementService } from '../features';
import { QuotaService } from '../quota';
import { AvatarStorage } from '../storage';
import { DeleteAccount, RemoveAvatar, UserQuotaType, UserType } from './types';
import {
DeleteAccount,
RemoveAvatar,
UserOrLimitedUser,
UserQuotaType,
UserType,
} from './types';
import { UsersService } from './users';
/**
@@ -77,14 +83,17 @@ export class UserResolver {
ttl: 60,
},
})
@Query(() => UserType, {
@Query(() => UserOrLimitedUser, {
name: 'user',
description: 'Get user by email',
nullable: true,
})
@Public()
async user(@Args('email') email: string) {
if (!(await this.feature.canEarlyAccess(email))) {
async user(
@CurrentUser() currentUser?: UserType,
@Args('email') email?: string
) {
if (!email || !(await this.feature.canEarlyAccess(email))) {
return new GraphQLError(
`You don't have early access permission\nVisit https://community.affine.pro/c/insider-general/ for more information`,
{
@@ -95,13 +104,16 @@ export class UserResolver {
}
);
}
// TODO: need to limit a user can only get another user witch is in the same workspace
const user = await this.users.findUserByEmail(email);
if (user?.password) {
const userResponse: UserType = user;
userResponse.hasPassword = true;
}
return user;
if (currentUser) return user;
// only return limited info when not logged in
return {
email: user?.email,
hasPassword: !!user?.password,
};
}
@Throttle({ default: { limit: 10, ttl: 60 } })
@@ -1,4 +1,4 @@
import { Field, Float, ID, ObjectType } from '@nestjs/graphql';
import { createUnionType, Field, Float, ID, ObjectType } from '@nestjs/graphql';
import type { User } from '@prisma/client';
@ObjectType('UserQuotaHumanReadable')
@@ -67,6 +67,29 @@ export class UserType implements Partial<User> {
hasPassword?: boolean;
}
@ObjectType()
export class LimitedUserType implements Partial<User> {
@Field({ description: 'User email' })
email!: string;
@Field(() => Boolean, {
description: 'User password has been set',
nullable: true,
})
hasPassword?: boolean;
}
export const UserOrLimitedUser = createUnionType({
name: 'UserOrLimitedUser',
types: () => [UserType, LimitedUserType] as const,
resolveType(value) {
if (value.id) {
return UserType;
}
return LimitedUserType;
},
});
@ObjectType()
export class DeleteAccount {
@Field()
@@ -13,13 +13,17 @@ import { CloudThrottlerGuard, Throttle } from '../../throttler';
import { Auth, CurrentUser } from '../auth';
import { FeatureManagementService, FeatureType } from '../features';
import { UserType } from '../users';
import { PermissionService } from './permission';
import { WorkspaceType } from './types';
@UseGuards(CloudThrottlerGuard)
@Auth()
@Resolver(() => WorkspaceType)
export class WorkspaceManagementResolver {
constructor(private readonly feature: FeatureManagementService) {}
constructor(
private readonly feature: FeatureManagementService,
private readonly permission: PermissionService
) {}
@Throttle({
default: {
@@ -77,6 +81,51 @@ export class WorkspaceManagementResolver {
return this.feature.listFeatureWorkspaces(feature);
}
@Mutation(() => Boolean)
async setWorkspaceExperimentalFeature(
@CurrentUser() user: UserType,
@Args('workspaceId') workspaceId: string,
@Args('feature', { type: () => FeatureType }) feature: FeatureType,
@Args('enable') enable: boolean
): Promise<boolean> {
if (!(await this.feature.canEarlyAccess(user.email))) {
throw new ForbiddenException('You are not allowed to do this');
}
const owner = await this.permission.getWorkspaceOwner(workspaceId);
const availableFeatures = await this.availableFeatures(user);
if (owner.user.id !== user.id || !availableFeatures.includes(feature)) {
throw new ForbiddenException('You are not allowed to do this');
}
if (enable) {
return await this.feature
.addWorkspaceFeatures(
workspaceId,
feature,
undefined,
'add by experimental feature api'
)
.then(id => id > 0);
} else {
return await this.feature.removeWorkspaceFeature(workspaceId, feature);
}
}
@ResolveField(() => [FeatureType], {
description: 'Available features of workspace',
complexity: 2,
})
async availableFeatures(
@CurrentUser() user: UserType
): Promise<FeatureType[]> {
if (await this.feature.canEarlyAccess(user.email)) {
return [FeatureType.Copilot];
} else {
return [];
}
}
@ResolveField(() => [FeatureType], {
description: 'Enabled features of workspace',
complexity: 2,
+15 -1
View File
@@ -131,6 +131,9 @@ type WorkspaceType {
"""Owner of workspace"""
owner: UserType!
"""Available features of workspace"""
availableFeatures: [FeatureType!]!
"""Enabled features of workspace"""
features: [FeatureType!]!
@@ -299,11 +302,21 @@ type Query {
currentUser: UserType
"""Get user by email"""
user(email: String!): UserType
user(email: String!): UserOrLimitedUser
earlyAccessUsers: [UserType!]!
prices: [SubscriptionPrice!]!
}
union UserOrLimitedUser = UserType | LimitedUserType
type LimitedUserType {
"""User email"""
email: String!
"""User password has been set"""
hasPassword: Boolean
}
type Mutation {
signUp(name: String!, email: String!, password: String!): UserType!
signIn(email: String!, password: String!): UserType!
@@ -326,6 +339,7 @@ type Mutation {
leaveWorkspace(workspaceId: String!, workspaceName: String!, sendLeaveMail: Boolean): Boolean!
addWorkspaceFeature(workspaceId: String!, feature: FeatureType!): Int!
removeWorkspaceFeature(workspaceId: String!, feature: FeatureType!): Int!
setWorkspaceExperimentalFeature(workspaceId: String!, feature: FeatureType!, enable: Boolean!): Boolean!
sharePage(workspaceId: String!, pageId: String!): Boolean! @deprecated(reason: "renamed to publicPage")
publishPage(workspaceId: String!, pageId: String!, mode: PublicPageMode = Page): WorkspacePage!
revokePage(workspaceId: String!, pageId: String!): Boolean! @deprecated(reason: "use revokePublicPage")
+6 -2
View File
@@ -114,8 +114,12 @@ test('should find default user', async t => {
query: `
query {
user(email: "alex.yang@example.org") {
email
avatarUrl
... on UserType {
email
}
... on LimitedUserType {
email
}
}
}
`,
@@ -1,9 +1,16 @@
query getUser($email: String!) {
user(email: $email) {
id
name
avatarUrl
email
hasPassword
__typename
... on UserType {
id
name
avatarUrl
email
hasPassword
}
... on LimitedUserType {
email
hasPassword
}
}
}
+53 -18
View File
@@ -345,11 +345,31 @@ export const getUserQuery = {
query: `
query getUser($email: String!) {
user(email: $email) {
id
name
avatarUrl
email
hasPassword
__typename
... on UserType {
id
name
avatarUrl
email
hasPassword
}
... on LimitedUserType {
email
hasPassword
}
}
}`,
};
export const getWorkspaceFeaturesQuery = {
id: 'getWorkspaceFeaturesQuery' as const,
operationName: 'getWorkspaceFeatures',
definitionName: 'workspace',
containsFile: false,
query: `
query getWorkspaceFeatures($workspaceId: String!) {
workspace(id: $workspaceId) {
features
}
}`,
};
@@ -383,19 +403,6 @@ query getWorkspacePublicPages($workspaceId: String!) {
}`,
};
export const getWorkspaceFeaturesQuery = {
id: 'getWorkspaceFeaturesQuery' as const,
operationName: 'getWorkspaceFeatures',
definitionName: 'workspace',
containsFile: false,
query: `
query getWorkspaceFeatures($workspaceId: String!) {
workspace(id: $workspaceId) {
features
}
}`,
};
export const getWorkspaceQuery = {
id: 'getWorkspaceQuery' as const,
operationName: 'getWorkspace',
@@ -773,6 +780,34 @@ mutation uploadAvatar($avatar: Upload!) {
}`,
};
export const availableFeaturesQuery = {
id: 'availableFeaturesQuery' as const,
operationName: 'availableFeatures',
definitionName: 'workspace',
containsFile: false,
query: `
query availableFeatures($id: String!) {
workspace(id: $id) {
availableFeatures
}
}`,
};
export const setWorkspaceExperimentalFeatureMutation = {
id: 'setWorkspaceExperimentalFeatureMutation' as const,
operationName: 'setWorkspaceExperimentalFeature',
definitionName: 'setWorkspaceExperimentalFeature',
containsFile: false,
query: `
mutation setWorkspaceExperimentalFeature($workspaceId: String!, $feature: FeatureType!, $enable: Boolean!) {
setWorkspaceExperimentalFeature(
workspaceId: $workspaceId
feature: $feature
enable: $enable
)
}`,
};
export const addWorkspaceFeatureMutation = {
id: 'addWorkspaceFeatureMutation' as const,
operationName: 'addWorkspaceFeature',
@@ -0,0 +1,5 @@
query availableFeatures($id: String!) {
workspace(id: $id) {
availableFeatures
}
}
@@ -0,0 +1,11 @@
mutation setWorkspaceExperimentalFeature(
$workspaceId: String!
$feature: FeatureType!
$enable: Boolean!
) {
setWorkspaceExperimentalFeature(
workspaceId: $workspaceId
feature: $feature
enable: $enable
)
}
+62 -22
View File
@@ -363,14 +363,30 @@ export type GetUserQueryVariables = Exact<{
export type GetUserQuery = {
__typename?: 'Query';
user: {
__typename?: 'UserType';
id: string;
name: string;
avatarUrl: string | null;
email: string;
hasPassword: boolean | null;
} | null;
user:
| {
__typename: 'LimitedUserType';
email: string;
hasPassword: boolean | null;
}
| {
__typename: 'UserType';
id: string;
name: string;
avatarUrl: string | null;
email: string;
hasPassword: boolean | null;
}
| null;
};
export type GetWorkspaceFeaturesQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
}>;
export type GetWorkspaceFeaturesQuery = {
__typename?: 'Query';
workspace: { __typename?: 'WorkspaceType'; features: Array<FeatureType> };
};
export type GetWorkspacePublicByIdQueryVariables = Exact<{
@@ -398,15 +414,6 @@ export type GetWorkspacePublicPagesQuery = {
};
};
export type GetWorkspaceFeaturesQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
}>;
export type GetWorkspaceFeaturesQuery = {
__typename?: 'Query';
workspace: { __typename?: 'WorkspaceType'; features: Array<FeatureType> };
};
export type GetWorkspaceQueryVariables = Exact<{
id: Scalars['String']['input'];
}>;
@@ -739,6 +746,29 @@ export type UploadAvatarMutation = {
};
};
export type AvailableFeaturesQueryVariables = Exact<{
id: Scalars['String']['input'];
}>;
export type AvailableFeaturesQuery = {
__typename?: 'Query';
workspace: {
__typename?: 'WorkspaceType';
availableFeatures: Array<FeatureType>;
};
};
export type SetWorkspaceExperimentalFeatureMutationVariables = Exact<{
workspaceId: Scalars['String']['input'];
feature: FeatureType;
enable: Scalars['Boolean']['input'];
}>;
export type SetWorkspaceExperimentalFeatureMutation = {
__typename?: 'Mutation';
setWorkspaceExperimentalFeature: boolean;
};
export type AddWorkspaceFeatureMutationVariables = Exact<{
workspaceId: Scalars['String']['input'];
feature: FeatureType;
@@ -857,6 +887,11 @@ export type Queries =
variables: GetUserQueryVariables;
response: GetUserQuery;
}
| {
name: 'getWorkspaceFeaturesQuery';
variables: GetWorkspaceFeaturesQueryVariables;
response: GetWorkspaceFeaturesQuery;
}
| {
name: 'getWorkspacePublicByIdQuery';
variables: GetWorkspacePublicByIdQueryVariables;
@@ -867,11 +902,6 @@ export type Queries =
variables: GetWorkspacePublicPagesQueryVariables;
response: GetWorkspacePublicPagesQuery;
}
| {
name: 'getWorkspaceFeaturesQuery';
variables: GetWorkspaceFeaturesQueryVariables;
response: GetWorkspaceFeaturesQuery;
}
| {
name: 'getWorkspaceQuery';
variables: GetWorkspaceQueryVariables;
@@ -917,6 +947,11 @@ export type Queries =
variables: SubscriptionQueryVariables;
response: SubscriptionQuery;
}
| {
name: 'availableFeaturesQuery';
variables: AvailableFeaturesQueryVariables;
response: AvailableFeaturesQuery;
}
| {
name: 'listWorkspaceFeaturesQuery';
variables: ListWorkspaceFeaturesQueryVariables;
@@ -1064,6 +1099,11 @@ export type Mutations =
variables: UploadAvatarMutationVariables;
response: UploadAvatarMutation;
}
| {
name: 'setWorkspaceExperimentalFeatureMutation';
variables: SetWorkspaceExperimentalFeatureMutationVariables;
response: SetWorkspaceExperimentalFeatureMutation;
}
| {
name: 'addWorkspaceFeatureMutation';
variables: AddWorkspaceFeatureMutationVariables;