mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 20:38:52 +00:00
feat(server): port resolvers to node server (#2026)
Co-authored-by: Himself65 <himself65@outlook.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
/// <reference types="./global.d.ts" />
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ConfigModule } from './config';
|
||||
|
||||
@@ -69,6 +69,10 @@ export function parseEnvValue(value: string | undefined, type?: EnvConfigType) {
|
||||
*/
|
||||
export interface AFFiNEConfig {
|
||||
ENV_MAP: Record<string, ConfigPaths | [ConfigPaths, EnvConfigType?]>;
|
||||
/**
|
||||
* Application sign key secret
|
||||
*/
|
||||
readonly secret: string;
|
||||
/**
|
||||
* System version
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@ import pkg from '../../package.json' assert { type: 'json' };
|
||||
import type { AFFiNEConfig } from './def';
|
||||
|
||||
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
|
||||
secret: 'secret',
|
||||
version: pkg.version,
|
||||
ENV_MAP: {},
|
||||
env: process.env.NODE_ENV ?? 'development',
|
||||
|
||||
5
apps/server/src/global.d.ts
vendored
Normal file
5
apps/server/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare namespace Express {
|
||||
interface Request {
|
||||
user?: import('@prisma/client').User | null;
|
||||
}
|
||||
}
|
||||
82
apps/server/src/modules/auth/guard.ts
Normal file
82
apps/server/src/modules/auth/guard.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
CanActivate,
|
||||
createParamDecorator,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { PrismaService } from '../../prisma';
|
||||
import { getRequestResponseFromContext } from '../../utils/nestjs';
|
||||
import { AuthService } from './service';
|
||||
|
||||
export function getUserFromContext(context: ExecutionContext) {
|
||||
const req = getRequestResponseFromContext(context).req;
|
||||
return req.user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to fetch current user from the request context.
|
||||
*
|
||||
* > The user may be undefined if authorization token is not provided.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```typescript
|
||||
* // Graphql Query
|
||||
* \@Query(() => UserType)
|
||||
* user(@CurrentUser() user?: User) {
|
||||
* return user;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ```typescript
|
||||
* // HTTP Controller
|
||||
* \@Get('/user)
|
||||
* user(@CurrentUser() user?: User) {
|
||||
* return user;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(_: unknown, context: ExecutionContext) => {
|
||||
return getUserFromContext(context);
|
||||
}
|
||||
);
|
||||
|
||||
@Injectable()
|
||||
class AuthGuard implements CanActivate {
|
||||
constructor(private auth: AuthService, private prisma: PrismaService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const { req } = getRequestResponseFromContext(context);
|
||||
const token = req.headers.authorization;
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const claims = this.auth.verify(token);
|
||||
req.user = await this.prisma.user.findUnique({ where: { id: claims.id } });
|
||||
return !!req.user;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This guard is used to protect routes/queries/mutations that require a user to be logged in.
|
||||
*
|
||||
* The `@CurrentUser()` parameter decorator used in a `Auth` guarded queries would always give us the user because the `Auth` guard will
|
||||
* fast throw if user is not logged in.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```typescript
|
||||
* \@Auth()
|
||||
* \@Query(() => UserType)
|
||||
* user(@CurrentUser() user: User) {
|
||||
* return user;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const Auth = () => {
|
||||
return UseGuards(AuthGuard);
|
||||
};
|
||||
12
apps/server/src/modules/auth/index.ts
Normal file
12
apps/server/src/modules/auth/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
|
||||
import { AuthResolver } from './resolver';
|
||||
import { AuthService } from './service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [AuthService, AuthResolver],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
export * from './guard';
|
||||
54
apps/server/src/modules/auth/resolver.ts
Normal file
54
apps/server/src/modules/auth/resolver.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { ForbiddenException } from '@nestjs/common';
|
||||
import {
|
||||
Args,
|
||||
Context,
|
||||
Field,
|
||||
Mutation,
|
||||
ObjectType,
|
||||
Parent,
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import { Request } from 'express';
|
||||
|
||||
import { UserType } from '../users/resolver';
|
||||
import { CurrentUser } from './guard';
|
||||
import { AuthService } from './service';
|
||||
|
||||
@ObjectType()
|
||||
export class TokenType {
|
||||
@Field()
|
||||
token!: string;
|
||||
|
||||
@Field()
|
||||
refresh!: string;
|
||||
}
|
||||
|
||||
@Resolver(() => UserType)
|
||||
export class AuthResolver {
|
||||
constructor(private auth: AuthService) {}
|
||||
|
||||
@ResolveField(() => TokenType)
|
||||
token(@CurrentUser() currentUser: UserType, @Parent() user: UserType) {
|
||||
if (user !== currentUser) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return {
|
||||
token: this.auth.sign(user),
|
||||
// TODO: impl
|
||||
refresh: '',
|
||||
};
|
||||
}
|
||||
|
||||
@Mutation(() => UserType)
|
||||
async signIn(
|
||||
@Context() ctx: { req: Request },
|
||||
@Args('email') email: string,
|
||||
@Args('password') password: string
|
||||
) {
|
||||
const user = await this.auth.signIn(email, password);
|
||||
ctx.req.user = user;
|
||||
return user;
|
||||
}
|
||||
}
|
||||
45
apps/server/src/modules/auth/service.ts
Normal file
45
apps/server/src/modules/auth/service.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { User } from '@prisma/client';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import { Config } from '../../config';
|
||||
import { PrismaService } from '../../prisma';
|
||||
|
||||
type UserClaim = Pick<User, 'id' | 'name' | 'email'>;
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(private config: Config, private prisma: PrismaService) {}
|
||||
|
||||
sign(user: UserClaim) {
|
||||
return jwt.sign(user, this.config.secret);
|
||||
}
|
||||
|
||||
verify(token: string) {
|
||||
try {
|
||||
const claims = jwt.verify(token, this.config.secret) as UserClaim;
|
||||
return claims;
|
||||
} catch (e) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
}
|
||||
|
||||
async signIn(email: string, password: string) {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
email,
|
||||
password,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new BadRequestException('Invalid email or password');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AuthModule } from './auth';
|
||||
import { UsersModule } from './users';
|
||||
import { WorkspaceModule } from './workspaces';
|
||||
|
||||
export const BusinessModules = [WorkspaceModule, UsersModule];
|
||||
export const BusinessModules = [AuthModule, WorkspaceModule, UsersModule];
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import { Args, Field, ID, ObjectType, Query, Resolver } from '@nestjs/graphql';
|
||||
import type { users } from '@prisma/client';
|
||||
import type { User } from '@prisma/client';
|
||||
|
||||
import { PrismaService } from '../../prisma/service';
|
||||
|
||||
@ObjectType()
|
||||
export class User implements users {
|
||||
export class UserType implements Partial<User> {
|
||||
@Field(() => ID)
|
||||
id!: string;
|
||||
|
||||
@Field({ description: 'User name' })
|
||||
name!: string;
|
||||
|
||||
@Field({ description: 'User email' })
|
||||
email!: string;
|
||||
@Field({ description: 'User password', nullable: true })
|
||||
password!: string;
|
||||
|
||||
@Field({ description: 'User avatar url', nullable: true })
|
||||
avatar_url!: string;
|
||||
@Field({ description: 'User token nonce', nullable: true })
|
||||
token_nonce!: number;
|
||||
avatarUrl!: string;
|
||||
|
||||
@Field({ description: 'User created date', nullable: true })
|
||||
created_at!: Date;
|
||||
createdAt!: Date;
|
||||
}
|
||||
|
||||
@Resolver(() => User)
|
||||
@Resolver(() => UserType)
|
||||
export class UserResolver {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
@Query(() => User, {
|
||||
@Query(() => UserType, {
|
||||
name: 'user',
|
||||
description: 'Get user by email',
|
||||
})
|
||||
async user(@Args('email') email: string) {
|
||||
return this.prisma.users.findUnique({
|
||||
return this.prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { PermissionService } from './permission';
|
||||
import { WorkspaceResolver } from './resolver';
|
||||
|
||||
@Module({
|
||||
providers: [WorkspaceResolver],
|
||||
providers: [WorkspaceResolver, PermissionService],
|
||||
exports: [PermissionService],
|
||||
})
|
||||
export class WorkspaceModule {}
|
||||
export { WorkspaceType } from './resolver';
|
||||
|
||||
134
apps/server/src/modules/workspaces/permission.ts
Normal file
134
apps/server/src/modules/workspaces/permission.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import { PrismaService } from '../../prisma';
|
||||
import { Permission } from './types';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async get(ws: string, user: string) {
|
||||
const data = await this.prisma.userWorkspacePermission.findFirst({
|
||||
where: {
|
||||
workspaceId: ws,
|
||||
userId: user,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
return data?.type as Permission;
|
||||
}
|
||||
|
||||
async check(
|
||||
ws: string,
|
||||
user: string,
|
||||
permission: Permission = Permission.Read
|
||||
) {
|
||||
if (!(await this.tryCheck(ws, user, permission))) {
|
||||
throw new ForbiddenException('Permission denied');
|
||||
}
|
||||
}
|
||||
|
||||
async tryCheck(
|
||||
ws: string,
|
||||
user: string,
|
||||
permission: Permission = Permission.Read
|
||||
) {
|
||||
const data = await this.prisma.userWorkspacePermission.count({
|
||||
where: {
|
||||
workspaceId: ws,
|
||||
userId: user,
|
||||
accepted: true,
|
||||
type: {
|
||||
gte: permission,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (data > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the permission is read, we should check if the workspace is public
|
||||
if (permission === Permission.Read) {
|
||||
const data = await this.prisma.workspace.count({
|
||||
where: { id: ws, public: true },
|
||||
});
|
||||
|
||||
return data > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async grant(
|
||||
ws: string,
|
||||
user: string,
|
||||
permission: Permission = Permission.Read
|
||||
) {
|
||||
const data = await this.prisma.userWorkspacePermission.findFirst({
|
||||
where: {
|
||||
workspaceId: ws,
|
||||
userId: user,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (data) {
|
||||
const [p] = await this.prisma.$transaction(
|
||||
[
|
||||
this.prisma.userWorkspacePermission.update({
|
||||
where: {
|
||||
id: data.id,
|
||||
},
|
||||
data: {
|
||||
type: permission,
|
||||
},
|
||||
}),
|
||||
|
||||
// If the new permission is owner, we need to revoke old owner
|
||||
permission === Permission.Owner
|
||||
? this.prisma.userWorkspacePermission.updateMany({
|
||||
where: {
|
||||
workspaceId: ws,
|
||||
type: Permission.Owner,
|
||||
userId: {
|
||||
not: user,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
type: Permission.Admin,
|
||||
},
|
||||
})
|
||||
: null,
|
||||
].filter(Boolean) as Prisma.PrismaPromise<any>[]
|
||||
);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
return this.prisma.userWorkspacePermission.create({
|
||||
data: {
|
||||
workspaceId: ws,
|
||||
userId: user,
|
||||
type: permission,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async revoke(ws: string, user: string) {
|
||||
const result = await this.prisma.userWorkspacePermission.deleteMany({
|
||||
where: {
|
||||
workspaceId: ws,
|
||||
userId: user,
|
||||
type: {
|
||||
// We shouldn't revoke owner permission, should auto deleted by workspace/user delete cascading
|
||||
not: Permission.Owner,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return result.count > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,85 +1,206 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { ForbiddenException, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
Args,
|
||||
Field,
|
||||
ID,
|
||||
InputType,
|
||||
Int,
|
||||
Mutation,
|
||||
ObjectType,
|
||||
Parent,
|
||||
PartialType,
|
||||
PickType,
|
||||
Query,
|
||||
registerEnumType,
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import type { workspaces } from '@prisma/client';
|
||||
import type { User, Workspace } from '@prisma/client';
|
||||
|
||||
import { PrismaService } from '../../prisma/service';
|
||||
import { PrismaService } from '../../prisma';
|
||||
import { Auth, CurrentUser } from '../auth';
|
||||
import { UserType } from '../users/resolver';
|
||||
import { PermissionService } from './permission';
|
||||
import { Permission } from './types';
|
||||
|
||||
export enum WorkspaceType {
|
||||
Private = 0,
|
||||
Normal = 1,
|
||||
}
|
||||
|
||||
registerEnumType(WorkspaceType, {
|
||||
name: 'WorkspaceType',
|
||||
description: 'Workspace type',
|
||||
valuesMap: {
|
||||
Normal: {
|
||||
description: 'Normal workspace',
|
||||
},
|
||||
Private: {
|
||||
description: 'Private workspace',
|
||||
},
|
||||
},
|
||||
registerEnumType(Permission, {
|
||||
name: 'Permission',
|
||||
description: 'User permission in workspace',
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
export class Workspace implements workspaces {
|
||||
export class WorkspaceType implements Partial<Workspace> {
|
||||
@Field(() => ID)
|
||||
id!: string;
|
||||
|
||||
@Field({ description: 'is Public workspace' })
|
||||
public!: boolean;
|
||||
@Field(() => WorkspaceType, { description: 'Workspace type' })
|
||||
type!: WorkspaceType;
|
||||
|
||||
@Field({ description: 'Workspace created date' })
|
||||
created_at!: Date;
|
||||
createdAt!: Date;
|
||||
}
|
||||
|
||||
@Resolver(() => Workspace)
|
||||
@InputType()
|
||||
export class UpdateWorkspaceInput extends PickType(
|
||||
PartialType(WorkspaceType),
|
||||
['public'],
|
||||
InputType
|
||||
) {
|
||||
@Field(() => ID)
|
||||
id!: string;
|
||||
}
|
||||
|
||||
@Auth()
|
||||
@Resolver(() => WorkspaceType)
|
||||
export class WorkspaceResolver {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly permissionProvider: PermissionService
|
||||
) {}
|
||||
|
||||
// debug only query should be removed
|
||||
@Query(() => [Workspace], {
|
||||
name: 'workspaces',
|
||||
description: 'Get all workspaces',
|
||||
@ResolveField(() => Permission, {
|
||||
description: 'Permission of current signed in user in workspace',
|
||||
complexity: 2,
|
||||
})
|
||||
async workspaces() {
|
||||
return this.prisma.workspaces.findMany();
|
||||
async permission(
|
||||
@CurrentUser() user: User,
|
||||
@Parent() workspace: WorkspaceType
|
||||
) {
|
||||
// may applied in workspaces query
|
||||
if ('permission' in workspace) {
|
||||
return workspace.permission;
|
||||
}
|
||||
|
||||
const permission = this.permissionProvider.get(workspace.id, user.id);
|
||||
|
||||
if (!permission) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return permission;
|
||||
}
|
||||
|
||||
@Query(() => Workspace, {
|
||||
name: 'workspace',
|
||||
description: 'Get workspace by id',
|
||||
@ResolveField(() => Int, {
|
||||
description: 'member count of workspace',
|
||||
complexity: 2,
|
||||
})
|
||||
async workspace(@Args('id') id: string) {
|
||||
return this.prisma.workspaces.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
// create workspace
|
||||
@Mutation(() => Workspace, {
|
||||
name: 'createWorkspace',
|
||||
description: 'Create workspace',
|
||||
})
|
||||
async createWorkspace() {
|
||||
return this.prisma.workspaces.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
type: WorkspaceType.Private,
|
||||
public: false,
|
||||
created_at: new Date(),
|
||||
memberCount(@Parent() workspace: WorkspaceType) {
|
||||
return this.prisma.userWorkspacePermission.count({
|
||||
where: {
|
||||
workspaceId: workspace.id,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ResolveField(() => UserType, {
|
||||
description: 'Owner of workspace',
|
||||
complexity: 2,
|
||||
})
|
||||
async owner(@Parent() workspace: WorkspaceType) {
|
||||
const data = await this.prisma.userWorkspacePermission.findFirstOrThrow({
|
||||
where: {
|
||||
workspaceId: workspace.id,
|
||||
type: Permission.Owner,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
return data.user;
|
||||
}
|
||||
|
||||
@Query(() => [WorkspaceType], {
|
||||
description: 'Get all accessible workspaces for current user',
|
||||
complexity: 2,
|
||||
})
|
||||
async workspaces(@CurrentUser() user: User) {
|
||||
const data = await this.prisma.userWorkspacePermission.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
accepted: true,
|
||||
},
|
||||
include: {
|
||||
workspace: true,
|
||||
},
|
||||
});
|
||||
|
||||
return data.map(({ workspace, type }) => {
|
||||
return {
|
||||
...workspace,
|
||||
permission: type,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@Query(() => WorkspaceType, {
|
||||
description: 'Get workspace by id',
|
||||
})
|
||||
async workspace(@CurrentUser() user: UserType, @Args('id') id: string) {
|
||||
await this.permissionProvider.check(id, user.id);
|
||||
const workspace = await this.prisma.workspace.findUnique({ where: { id } });
|
||||
|
||||
if (!workspace) {
|
||||
throw new NotFoundException("Workspace doesn't exist");
|
||||
}
|
||||
|
||||
return workspace;
|
||||
}
|
||||
|
||||
@Mutation(() => WorkspaceType, {
|
||||
description: 'Create a new workspace',
|
||||
})
|
||||
async createWorkspace(@CurrentUser() user: User) {
|
||||
return this.prisma.workspace.create({
|
||||
data: {
|
||||
public: false,
|
||||
users: {
|
||||
create: {
|
||||
type: Permission.Owner,
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => WorkspaceType, {
|
||||
description: 'Update workspace',
|
||||
})
|
||||
async updateWorkspace(
|
||||
@CurrentUser() user: User,
|
||||
@Args({ name: 'input', type: () => UpdateWorkspaceInput })
|
||||
{ id, ...updates }: UpdateWorkspaceInput
|
||||
) {
|
||||
await this.permissionProvider.check('id', user.id, Permission.Admin);
|
||||
|
||||
return this.prisma.workspace.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: updates,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async deleteWorkspace(@CurrentUser() user: User, @Args('id') id: string) {
|
||||
await this.permissionProvider.check(id, user.id, Permission.Owner);
|
||||
|
||||
await this.prisma.workspace.delete({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO:
|
||||
// delete all related data, like websocket connections, blobs, etc.
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
6
apps/server/src/modules/workspaces/types.ts
Normal file
6
apps/server/src/modules/workspaces/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export enum Permission {
|
||||
Read = 0,
|
||||
Write = 1,
|
||||
Admin = 10,
|
||||
Owner = 99,
|
||||
}
|
||||
@@ -8,3 +8,4 @@ import { PrismaService } from './service';
|
||||
exports: [PrismaService],
|
||||
})
|
||||
export class PrismaModule {}
|
||||
export { PrismaService } from './service';
|
||||
|
||||
@@ -34,22 +34,46 @@ describe('AppModule', () => {
|
||||
.post(gql)
|
||||
.send({
|
||||
query: `
|
||||
query {
|
||||
error
|
||||
}
|
||||
`,
|
||||
query {
|
||||
error
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
let token;
|
||||
await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
signIn(email: "alex.yang@example.org", password: "123456") {
|
||||
token {
|
||||
token
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200)
|
||||
.expect(res => {
|
||||
ok(
|
||||
typeof res.body.data.signIn.token.token === 'string',
|
||||
'res.body.data.signIn.token.token is not a string'
|
||||
);
|
||||
token = res.body.data.signIn.token.token;
|
||||
});
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.set({ Authorization: token })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
createWorkspace {
|
||||
id
|
||||
type
|
||||
public
|
||||
created_at
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
@@ -64,16 +88,12 @@ describe('AppModule', () => {
|
||||
typeof res.body.data.createWorkspace.id === 'string',
|
||||
'res.body.data.createWorkspace.id is not a string'
|
||||
);
|
||||
ok(
|
||||
typeof res.body.data.createWorkspace.type === 'string',
|
||||
'res.body.data.createWorkspace.type is not a string'
|
||||
);
|
||||
ok(
|
||||
typeof res.body.data.createWorkspace.public === 'boolean',
|
||||
'res.body.data.createWorkspace.public is not a boolean'
|
||||
);
|
||||
ok(
|
||||
typeof res.body.data.createWorkspace.created_at === 'string',
|
||||
typeof res.body.data.createWorkspace.createdAt === 'string',
|
||||
'res.body.data.createWorkspace.created_at is not a string'
|
||||
);
|
||||
});
|
||||
@@ -87,7 +107,7 @@ describe('AppModule', () => {
|
||||
query {
|
||||
user(email: "alex.yang@example.org") {
|
||||
email
|
||||
avatar_url
|
||||
avatarUrl
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
57
apps/server/src/utils/nestjs.ts
Normal file
57
apps/server/src/utils/nestjs.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { ArgumentsHost, ExecutionContext } from '@nestjs/common';
|
||||
import {
|
||||
GqlArgumentsHost,
|
||||
GqlContextType,
|
||||
GqlExecutionContext,
|
||||
} from '@nestjs/graphql';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
export function getRequestResponseFromContext(context: ExecutionContext) {
|
||||
switch (context.getType<GqlContextType>()) {
|
||||
case 'graphql': {
|
||||
const gqlContext = GqlExecutionContext.create(context).getContext<{
|
||||
req: Request;
|
||||
}>();
|
||||
return {
|
||||
req: gqlContext.req,
|
||||
res: gqlContext.req.res!,
|
||||
};
|
||||
}
|
||||
case 'http': {
|
||||
const http = context.switchToHttp();
|
||||
return {
|
||||
req: http.getRequest<Request>(),
|
||||
res: http.getResponse<Response>(),
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error('Unknown context type for getting request and response');
|
||||
}
|
||||
}
|
||||
|
||||
export function getRequestResponseFromHost(host: ArgumentsHost) {
|
||||
switch (host.getType<GqlContextType>()) {
|
||||
case 'graphql': {
|
||||
const gqlContext = GqlArgumentsHost.create(host).getContext<{
|
||||
req: Request;
|
||||
}>();
|
||||
return {
|
||||
req: gqlContext.req,
|
||||
res: gqlContext.req.res!,
|
||||
};
|
||||
}
|
||||
case 'http': {
|
||||
const http = host.switchToHttp();
|
||||
return {
|
||||
req: http.getRequest<Request>(),
|
||||
res: http.getResponse<Response>(),
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error('Unknown host type for getting request and response');
|
||||
}
|
||||
}
|
||||
|
||||
export function getRequestFromHost(host: ArgumentsHost) {
|
||||
return getRequestResponseFromHost(host).req;
|
||||
}
|
||||
Reference in New Issue
Block a user