Removing Prisma and Grapql-nestjs-prisma resolvers (#2574)

* Some cleaning

* Fix seeds

* Fix all sign in, sign up flow and apiKey optimistic rendering

* Fix
This commit is contained in:
Charles Bochet
2023-11-19 18:25:47 +01:00
committed by GitHub
parent 18dac1a2b6
commit f5e1d7825a
616 changed files with 2220 additions and 23073 deletions

View File

@ -1,15 +0,0 @@
import { Module } from '@nestjs/common';
import { AbilityModule } from 'src/ability/ability.module';
import { PrismaModule } from 'src/database/prisma.module';
import { ActivityResolver } from './resolvers/activity.resolver';
import { ActivityService } from './services/activity.service';
import { ActivityTargetService } from './services/activity-target.service';
@Module({
imports: [AbilityModule, PrismaModule],
providers: [ActivityResolver, ActivityService, ActivityTargetService],
exports: [ActivityService, ActivityTargetService],
})
export class ActivityModule {}

View File

@ -1,32 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ActivityService } from 'src/core/activity/services/activity.service';
import { AbilityFactory } from 'src/ability/ability.factory';
import { ActivityResolver } from './activity.resolver';
describe('ActivityResolver', () => {
let resolver: ActivityResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ActivityResolver,
{
provide: ActivityService,
useValue: {},
},
{
provide: AbilityFactory,
useValue: {},
},
],
}).compile();
resolver = module.get<ActivityResolver>(ActivityResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -1,156 +0,0 @@
import { Resolver, Args, Mutation, Query } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { accessibleBy } from '@casl/prisma';
import { Prisma } from '@prisma/client';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import {
PrismaSelector,
PrismaSelect,
} from 'src/decorators/prisma-select.decorator';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
CreateActivityAbilityHandler,
DeleteActivityAbilityHandler,
ReadActivityAbilityHandler,
UpdateActivityAbilityHandler,
} from 'src/ability/handlers/activity.ability-handler';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output';
import { Activity } from 'src/core/@generated/activity/activity.model';
import { ActivityService } from 'src/core/activity/services/activity.service';
import { CreateOneActivityArgs } from 'src/core/@generated/activity/create-one-activity.args';
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { UpdateOneActivityArgs } from 'src/core/@generated/activity/update-one-activity.args';
import { FindManyActivityArgs } from 'src/core/@generated/activity/find-many-activity.args';
import { DeleteManyActivityArgs } from 'src/core/@generated/activity/delete-many-activity.args';
@UseGuards(JwtAuthGuard)
@Resolver(() => Activity)
export class ActivityResolver {
constructor(private readonly activityService: ActivityService) {}
@Mutation(() => Activity, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(CreateActivityAbilityHandler)
async createOneActivity(
@Args() args: CreateOneActivityArgs,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'Activity' })
prismaSelect: PrismaSelect<'Activity'>,
): Promise<Partial<Activity>> {
const createdActivity = await this.activityService.create({
data: {
...args.data,
...{ workspace: { connect: { id: workspace.id } } },
activityTargets: args.data?.activityTargets?.createMany
? {
createMany: {
data: args.data.activityTargets.createMany.data.map(
(target) => ({ ...target, workspaceId: workspace.id }),
),
},
}
: undefined,
},
select: prismaSelect.value,
} as Prisma.ActivityCreateArgs);
return createdActivity;
}
@Mutation(() => Activity, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(UpdateActivityAbilityHandler)
async updateOneActivity(
@Args() args: UpdateOneActivityArgs,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'Activity' })
prismaSelect: PrismaSelect<'Activity'>,
): Promise<Partial<Activity>> {
// TODO: Do a proper check with recursion testing on args in a more generic place
for (const key in args.data) {
if (args.data[key]) {
for (const subKey in args.data[key]) {
if (JSON.stringify(args.data[key][subKey]) === '{}') {
delete args.data[key][subKey];
}
}
}
if (JSON.stringify(args.data[key]) === '{}') {
delete args.data[key];
}
}
const updatedActivity = await this.activityService.update({
where: args.where,
data: {
...args.data,
activityTargets: args.data?.activityTargets
? {
createMany: args.data.activityTargets.createMany
? {
data: args.data.activityTargets.createMany.data.map(
(target) => ({
...target,
workspaceId: workspace.id,
}),
),
}
: undefined,
deleteMany: args.data.activityTargets.deleteMany ?? undefined,
}
: undefined,
},
select: prismaSelect.value,
} as Prisma.ActivityUpdateArgs);
return updatedActivity;
}
@Query(() => [Activity])
@UseGuards(AbilityGuard)
@CheckAbilities(ReadActivityAbilityHandler)
async findManyActivities(
@Args() args: FindManyActivityArgs,
@UserAbility() ability: AppAbility,
@PrismaSelector({ modelName: 'Activity' })
prismaSelect: PrismaSelect<'Activity'>,
): Promise<Partial<Activity>[]> {
const result = await this.activityService.findMany({
where: {
...args.where,
AND: [accessibleBy(ability).Activity],
},
orderBy: args.orderBy,
cursor: args.cursor,
take: args.take,
skip: args.skip,
distinct: args.distinct,
select: prismaSelect.value,
});
return result;
}
@Mutation(() => AffectedRows, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(DeleteActivityAbilityHandler)
async deleteManyActivities(
@Args() args: DeleteManyActivityArgs,
): Promise<AffectedRows> {
return this.activityService.deleteMany({
where: args.where,
});
}
}

View File

@ -1,28 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { ActivityTargetService } from './activity-target.service';
describe('ActivityTargetService', () => {
let service: ActivityTargetService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ActivityTargetService,
{
provide: PrismaService,
useValue: prismaMock,
},
],
}).compile();
service = module.get<ActivityTargetService>(ActivityTargetService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,40 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class ActivityTargetService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.activityTarget.findFirst;
findFirstOrThrow = this.prismaService.client.activityTarget.findFirstOrThrow;
findUnique = this.prismaService.client.activityTarget.findUnique;
findUniqueOrThrow =
this.prismaService.client.activityTarget.findUniqueOrThrow;
findMany = this.prismaService.client.activityTarget.findMany;
// Create
create = this.prismaService.client.activityTarget.create;
createMany = this.prismaService.client.activityTarget.createMany;
// Update
update = this.prismaService.client.activityTarget.update;
upsert = this.prismaService.client.activityTarget.upsert;
updateMany = this.prismaService.client.activityTarget.updateMany;
// Delete
delete = this.prismaService.client.activityTarget.delete;
deleteMany = this.prismaService.client.activityTarget.deleteMany;
// Aggregate
aggregate = this.prismaService.client.activityTarget.aggregate;
// Count
count = this.prismaService.client.activityTarget.count;
// GroupBy
groupBy = this.prismaService.client.activityTarget.groupBy;
}

View File

@ -1,28 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { ActivityService } from './activity.service';
describe('ActivityService', () => {
let service: ActivityService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ActivityService,
{
provide: PrismaService,
useValue: prismaMock,
},
],
}).compile();
service = module.get<ActivityService>(ActivityService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,39 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class ActivityService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.activity.findFirst;
findFirstOrThrow = this.prismaService.client.activity.findFirstOrThrow;
findUnique = this.prismaService.client.activity.findUnique;
findUniqueOrThrow = this.prismaService.client.activity.findUniqueOrThrow;
findMany = this.prismaService.client.activity.findMany;
// Create
create = this.prismaService.client.activity.create;
createMany = this.prismaService.client.activity.createMany;
// Update
update = this.prismaService.client.activity.update;
upsert = this.prismaService.client.activity.upsert;
updateMany = this.prismaService.client.activity.updateMany;
// Delete
delete = this.prismaService.client.activity.delete;
deleteMany = this.prismaService.client.activity.deleteMany;
// Aggregate
aggregate = this.prismaService.client.activity.aggregate;
// Count
count = this.prismaService.client.activity.count;
// GroupBy
groupBy = this.prismaService.client.activity.groupBy;
}

View File

@ -1,11 +1,11 @@
import { Resolver, Mutation, Args } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { User, Workspace } from '@prisma/client';
import { OptionalJwtAuthGuard } from 'src/guards/optional-jwt.auth.guard';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { AuthUser } from 'src/decorators/auth-user.decorator';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { User } from 'src/core/user/user.entity';
import { AnalyticsService } from './analytics.service';
import { Analytics } from './analytics.entity';

View File

@ -1,10 +1,11 @@
import { Injectable } from '@nestjs/common';
import { User, Workspace } from '@prisma/client';
import axios, { AxiosInstance } from 'axios';
import { anonymize } from 'src/utils/anonymize';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { User } from 'src/core/user/user.entity';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { CreateAnalyticsInput } from './dto/create-analytics.input';

View File

@ -1,6 +1,6 @@
import { ArgsType, Field } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import graphqlTypeJson from 'graphql-type-json';
import { IsNotEmpty, IsString, IsObject } from 'class-validator';
@ArgsType()
@ -10,7 +10,7 @@ export class CreateAnalyticsInput {
@IsString()
type: string;
@Field(() => GraphQLJSON, { description: 'Event data in JSON format' })
@Field(() => graphqlTypeJson, { description: 'Event data in JSON format' })
@IsObject()
data: JSON;
}

View File

@ -1,15 +0,0 @@
import { Module } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { TokenService } from 'src/core/auth/services/token.service';
import { AbilityModule } from 'src/ability/ability.module';
import { PrismaModule } from 'src/database/prisma.module';
import { ApiKeyResolver } from './api-key.resolver';
import { ApiKeyService } from './api-key.service';
@Module({
imports: [AbilityModule, PrismaModule],
providers: [ApiKeyResolver, ApiKeyService, TokenService, JwtService],
})
export class ApiKeyModule {}

View File

@ -1,28 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { JwtService } from '@nestjs/jwt';
import { AbilityFactory } from 'src/ability/ability.factory';
import { TokenService } from 'src/core/auth/services/token.service';
import { ApiKeyResolver } from './api-key.resolver';
import { ApiKeyService } from './api-key.service';
describe('ApiKeyResolver', () => {
let resolver: ApiKeyResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ApiKeyResolver,
{ provide: ApiKeyService, useValue: {} },
{ provide: TokenService, useValue: {} },
{ provide: JwtService, useValue: {} },
{ provide: AbilityFactory, useValue: {} },
],
}).compile();
resolver = module.get<ApiKeyResolver>(ApiKeyResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -1,97 +0,0 @@
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { NotFoundException, UseGuards } from '@nestjs/common';
import { accessibleBy } from '@casl/prisma';
import { AbilityGuard } from 'src/guards/ability.guard';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { CreateOneApiKeyArgs } from 'src/core/@generated/api-key/create-one-api-key.args';
import { ApiKey } from 'src/core/@generated/api-key/api-key.model';
import { FindManyApiKeyArgs } from 'src/core/@generated/api-key/find-many-api-key.args';
import { DeleteOneApiKeyArgs } from 'src/core/@generated/api-key/delete-one-api-key.args';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
CreateApiKeyAbilityHandler,
UpdateApiKeyAbilityHandler,
ReadApiKeyAbilityHandler,
} from 'src/ability/handlers/api-key.ability-handler';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
import { ApiKeyToken } from 'src/core/auth/dto/token.entity';
import { ApiKeyService } from './api-key.service';
@UseGuards(JwtAuthGuard)
@Resolver(() => ApiKey)
export class ApiKeyResolver {
constructor(private readonly apiKeyService: ApiKeyService) {}
@Mutation(() => ApiKeyToken)
@UseGuards(AbilityGuard)
@CheckAbilities(CreateApiKeyAbilityHandler)
async createOneApiKey(
@Args() args: CreateOneApiKeyArgs,
@AuthWorkspace() { id: workspaceId }: Workspace,
): Promise<ApiKeyToken> {
return await this.apiKeyService.generateApiKeyToken(
workspaceId,
args.data.name,
args.data.expiresAt,
);
}
@Mutation(() => ApiKeyToken)
@UseGuards(AbilityGuard)
@CheckAbilities(CreateApiKeyAbilityHandler)
async generateApiKeyV2Token(
@Args()
args: CreateOneApiKeyArgs,
@AuthWorkspace() { id: workspaceId }: Workspace,
): Promise<Pick<ApiKeyToken, 'token'> | undefined> {
return await this.apiKeyService.generateApiKeyV2Token(
workspaceId,
args.data.id,
args.data.expiresAt,
);
}
@Mutation(() => ApiKey)
@UseGuards(AbilityGuard)
@CheckAbilities(UpdateApiKeyAbilityHandler)
async revokeOneApiKey(
@Args() args: DeleteOneApiKeyArgs,
): Promise<Partial<ApiKey>> {
const apiKeyToDelete = await this.apiKeyService.findFirst({
where: { ...args.where },
});
if (!apiKeyToDelete) {
throw new NotFoundException();
}
return this.apiKeyService.update({
where: args.where,
data: {
revokedAt: new Date(),
},
});
}
@Query(() => [ApiKey])
@UseGuards(AbilityGuard)
@CheckAbilities(ReadApiKeyAbilityHandler)
async findManyApiKey(
@Args() args: FindManyApiKeyArgs,
@UserAbility() ability: AppAbility,
) {
const filterOptions = [
accessibleBy(ability).WorkspaceMember,
{ revokedAt: null },
];
if (args.where) filterOptions.push(args.where);
return this.apiKeyService.findMany({
...args,
where: { AND: filterOptions },
});
}
}

View File

@ -1,85 +0,0 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from 'src/database/prisma.service';
import { ApiKeyToken } from 'src/core/auth/dto/token.entity';
import { assert } from 'src/utils/assert';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
@Injectable()
export class ApiKeyService {
constructor(
private readonly prismaService: PrismaService,
private readonly environmentService: EnvironmentService,
private readonly jwtService: JwtService,
) {}
findFirst = this.prismaService.client.apiKey.findFirst;
findUniqueOrThrow = this.prismaService.client.apiKey.findUniqueOrThrow;
findMany = this.prismaService.client.apiKey.findMany;
create = this.prismaService.client.apiKey.create;
update = this.prismaService.client.apiKey.update;
delete = this.prismaService.client.apiKey.delete;
async generateApiKeyV2Token(
workspaceId: string,
apiKeyId?: string,
expiresAt?: Date | string,
): Promise<Pick<ApiKeyToken, 'token'> | undefined> {
if (!apiKeyId) {
return;
}
const jwtPayload = {
sub: workspaceId,
};
const secret = this.environmentService.getAccessTokenSecret();
let expiresIn: string | number;
if (expiresAt) {
expiresIn = Math.floor(
(new Date(expiresAt).getTime() - new Date().getTime()) / 1000,
);
} else {
expiresIn = this.environmentService.getApiTokenExpiresIn();
}
const token = this.jwtService.sign(jwtPayload, {
secret,
expiresIn,
jwtid: apiKeyId,
});
return { token };
}
async generateApiKeyToken(
workspaceId: string,
name: string,
expiresAt?: Date | string,
): Promise<ApiKeyToken> {
const secret = this.environmentService.getAccessTokenSecret();
let expiresIn: string | number;
const now = new Date().getTime();
if (expiresAt) {
expiresIn = Math.floor((new Date(expiresAt).getTime() - now) / 1000);
} else {
expiresIn = this.environmentService.getApiTokenExpiresIn();
}
assert(expiresIn, '', InternalServerErrorException);
const jwtPayload = {
sub: workspaceId,
};
const newApiKey = await this.prismaService.client.apiKey.create({
data: {
expiresAt: expiresAt,
name: name,
workspaceId: workspaceId,
},
});
return {
...newApiKey,
token: this.jwtService.sign(jwtPayload, {
secret,
expiresIn,
jwtid: newApiKey.id,
}),
};
}
}

View File

@ -1,15 +0,0 @@
import { Module } from '@nestjs/common';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { AbilityModule } from 'src/ability/ability.module';
import { PrismaModule } from 'src/database/prisma.module';
import { AttachmentResolver } from './resolvers/attachment.resolver';
import { AttachmentService } from './services/attachment.service';
@Module({
imports: [AbilityModule, PrismaModule],
providers: [AttachmentService, AttachmentResolver, FileUploadService],
exports: [AttachmentService],
})
export class AttachmentModule {}

View File

@ -1,37 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { AttachmentService } from 'src/core/attachment/services/attachment.service';
import { AbilityFactory } from 'src/ability/ability.factory';
import { AttachmentResolver } from './attachment.resolver';
describe('AttachmentResolver', () => {
let resolver: AttachmentResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AttachmentResolver,
{
provide: FileUploadService,
useValue: {},
},
{
provide: AttachmentService,
useValue: {},
},
{
provide: AbilityFactory,
useValue: {},
},
],
}).compile();
resolver = module.get<AttachmentResolver>(AttachmentResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -1,72 +0,0 @@
import { Resolver, Args, Mutation } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { User, Workspace } from '@prisma/client';
import { GraphQLUpload, FileUpload } from 'graphql-upload';
import { v4 as uuidV4 } from 'uuid';
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
import { AuthUser } from 'src/decorators/auth-user.decorator';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { AttachmentService } from 'src/core/attachment/services/attachment.service';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { Attachment } from 'src/core/@generated/attachment/attachment.model';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CreateAttachmentAbilityHandler } from 'src/ability/handlers/attachment.ability-handler';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
@UseGuards(JwtAuthGuard)
@Resolver(() => Attachment)
@Resolver()
export class AttachmentResolver {
constructor(
private readonly fileUploadService: FileUploadService,
private readonly attachmentService: AttachmentService,
) {}
@UseGuards(AbilityGuard)
@CheckAbilities(CreateAttachmentAbilityHandler)
@Mutation(() => String)
async uploadAttachment(
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
@Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename, mimetype }: FileUpload,
@Args('activityId') activityId?: string,
@Args('companyId') companyId?: string,
@Args('personId') personId?: string,
): Promise<string> {
const stream = createReadStream();
const buffer = await streamToBuffer(stream);
const { path } = await this.fileUploadService.uploadFile({
file: buffer,
filename,
mimeType: mimetype,
fileFolder: FileFolder.Attachment,
});
await this.attachmentService.create({
data: {
id: uuidV4(),
fullPath: path,
type: this.attachmentService.getFileTypeFromFileName(filename),
name: filename,
activityId,
companyId,
personId,
authorId: user.id,
workspaceId: workspace.id,
},
select: {
id: true,
fullPath: true,
},
});
return path;
}
}

View File

@ -1,28 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { AttachmentService } from './attachment.service';
describe('AttachmentService', () => {
let service: AttachmentService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AttachmentService,
{
provide: PrismaService,
useValue: prismaMock,
},
],
}).compile();
service = module.get<AttachmentService>(AttachmentService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,83 +0,0 @@
import { Injectable } from '@nestjs/common';
import { AttachmentType } from '@prisma/client';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class AttachmentService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.attachment.findFirst;
findFirstOrThrow = this.prismaService.client.attachment.findFirstOrThrow;
findUnique = this.prismaService.client.attachment.findUnique;
findUniqueOrThrow = this.prismaService.client.attachment.findUniqueOrThrow;
findMany = this.prismaService.client.attachment.findMany;
// Create
create = this.prismaService.client.attachment.create;
createMany = this.prismaService.client.attachment.createMany;
// Update
update = this.prismaService.client.attachment.update;
upsert = this.prismaService.client.attachment.upsert;
updateMany = this.prismaService.client.attachment.updateMany;
// Delete
delete = this.prismaService.client.attachment.delete;
deleteMany = this.prismaService.client.attachment.deleteMany;
// Aggregate
aggregate = this.prismaService.client.attachment.aggregate;
// Count
count = this.prismaService.client.attachment.count;
// GroupBy
groupBy = this.prismaService.client.attachment.groupBy;
getFileTypeFromFileName(fileName: string): AttachmentType {
const extension = fileName.split('.').pop()?.toLowerCase();
switch (extension) {
case 'mp4':
case 'avi':
case 'mov':
return AttachmentType.Video;
case 'mp3':
case 'wav':
case 'ogg':
return AttachmentType.Audio;
case 'jpg':
case 'jpeg':
case 'png':
case 'gif':
return AttachmentType.Image;
case 'txt':
case 'doc':
case 'docx':
case 'pdf':
return AttachmentType.TextDocument;
case 'xls':
case 'xlsx':
case 'csv':
return AttachmentType.Spreadsheet;
case 'zip':
case 'rar':
case 'tar':
case '7z':
return AttachmentType.Archive;
default:
return AttachmentType.Other;
}
}
}

View File

@ -1,11 +1,21 @@
/* eslint-disable no-restricted-imports */
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { PrismaService } from 'src/database/prisma.service';
import { UserModule } from 'src/core/user/user.module';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { WorkspaceModule } from 'src/core/workspace/workspace.module';
import { FileModule } from 'src/core/file/file.module';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { User } from 'src/core/user/user.entity';
import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity';
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { UserModule } from 'src/core/user/user.module';
import { WorkspaceManagerModule } from 'src/workspace/workspace-manager/workspace-manager.module';
import config from '../../../ormconfig';
import { AuthResolver } from './auth.resolver';
@ -28,15 +38,22 @@ const jwtModule = JwtModule.registerAsync({
});
@Module({
imports: [jwtModule, UserModule, WorkspaceModule, FileModule],
controllers: [GoogleAuthController, VerifyAuthController],
providers: [
AuthService,
TokenService,
JwtAuthStrategy,
PrismaService,
AuthResolver,
imports: [
jwtModule,
FileModule,
DataSourceModule,
UserModule,
WorkspaceManagerModule,
TypeOrmModule.forRoot(config),
NestjsQueryGraphQLModule.forFeature({
imports: [
TypeOrmModule.forFeature([Workspace, User, RefreshToken]),
TypeORMModule,
],
}),
],
controllers: [GoogleAuthController, VerifyAuthController],
providers: [AuthService, TokenService, JwtAuthStrategy, AuthResolver],
exports: [jwtModule],
})
export class AuthModule {}

View File

@ -4,21 +4,19 @@ import {
ForbiddenException,
UseGuards,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Prisma } from '@prisma/client';
import { Repository } from 'typeorm';
import {
PrismaSelect,
PrismaSelector,
} from 'src/decorators/prisma-select.decorator';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { AuthUser } from 'src/decorators/auth-user.decorator';
import { assert } from 'src/utils/assert';
import { User } from 'src/core/@generated/user/user.model';
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { WorkspaceService } from 'src/core/workspace/services/workspace.service';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { User } from 'src/core/user/user.entity';
import { ApiKeyTokenInput } from 'src/core/auth/dto/api-key-token.input';
import { AuthTokens } from './dto/token.entity';
import { ApiKeyToken, AuthTokens } from './dto/token.entity';
import { TokenService } from './services/token.service';
import { RefreshTokenInput } from './dto/refresh-token.input';
import { Verify } from './dto/verify.entity';
@ -36,7 +34,8 @@ import { ImpersonateInput } from './dto/impersonate.input';
@Resolver()
export class AuthResolver {
constructor(
private workspaceService: WorkspaceService,
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
private authService: AuthService,
private tokenService: TokenService,
) {}
@ -64,10 +63,8 @@ export class AuthResolver {
async findWorkspaceFromInviteHash(
@Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput,
) {
return await this.workspaceService.findFirst({
where: {
inviteHash: workspaceInviteHashValidInput.inviteHash,
},
return await this.workspaceRepository.findOneBy({
inviteHash: workspaceInviteHashValidInput.inviteHash,
});
}
@ -88,21 +85,12 @@ export class AuthResolver {
}
@Mutation(() => Verify)
async verify(
@Args() verifyInput: VerifyInput,
@PrismaSelector({
modelName: 'User',
defaultFields: { User: { id: true } },
})
prismaSelect: PrismaSelect<'User'>,
): Promise<Verify> {
async verify(@Args() verifyInput: VerifyInput): Promise<Verify> {
const email = await this.tokenService.verifyLoginToken(
verifyInput.loginToken,
);
const select = prismaSelect.valueOf('user') as Prisma.UserSelect & {
id: true;
};
const result = await this.authService.verify(email, select);
const result = await this.authService.verify(email);
return result;
}
@ -125,22 +113,24 @@ export class AuthResolver {
async impersonate(
@Args() impersonateInput: ImpersonateInput,
@AuthUser() user: User,
@PrismaSelector({
modelName: 'User',
defaultFields: {
User: {
id: true,
},
},
})
prismaSelect: PrismaSelect<'User'>,
): Promise<Verify> {
// Check if user can impersonate
assert(user.canImpersonate, 'User cannot impersonate', ForbiddenException);
const select = prismaSelect.valueOf('user') as Prisma.UserSelect & {
id: true;
};
return this.authService.impersonate(impersonateInput.userId, select);
return this.authService.impersonate(impersonateInput.userId);
}
@UseGuards(JwtAuthGuard)
@Mutation(() => ApiKeyToken)
async generateApiKeyToken(
@Args() args: ApiKeyTokenInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
): Promise<ApiKeyToken | undefined> {
console.log('toto');
return await this.tokenService.generateApiKeyToken(
workspaceId,
args.apiKeyId,
args.expiresAt,
);
}
}

View File

@ -1,29 +1,29 @@
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Response } from 'express';
import FileType from 'file-type';
import { v4 as uuidV4 } from 'uuid';
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
import { Repository } from 'typeorm';
import { GoogleRequest } from 'src/core/auth/strategies/google.auth.strategy';
import { UserService } from 'src/core/user/user.service';
import { TokenService } from 'src/core/auth/services/token.service';
import { GoogleProviderEnabledGuard } from 'src/core/auth/guards/google-provider-enabled.guard';
import { GoogleOauthGuard } from 'src/core/auth/guards/google-oauth.guard';
import { WorkspaceService } from 'src/core/workspace/services/workspace.service';
import { User } from 'src/core/user/user.entity';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { AuthService } from 'src/core/auth/services/auth.service';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { getImageBufferFromUrl } from 'src/utils/image';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
@Controller('auth/google')
export class GoogleAuthController {
constructor(
private readonly tokenService: TokenService,
private readonly userService: UserService,
private readonly workspaceService: WorkspaceService,
private readonly environmentService: EnvironmentService,
private readonly fileUploadService: FileUploadService,
private readonly typeORMService: TypeORMService,
private readonly authService: AuthService,
@InjectRepository(Workspace)
@InjectRepository(User, 'metadata')
private readonly userRepository: Repository<User>,
) {}
@Get()
@ -39,65 +39,29 @@ export class GoogleAuthController {
const { firstName, lastName, email, picture, workspaceInviteHash } =
req.user;
let workspaceId: string | undefined = undefined;
if (workspaceInviteHash) {
const workspace = await this.workspaceService.findFirst({
where: {
inviteHash: workspaceInviteHash,
},
});
const mainDataSource = await this.typeORMService.getMainDataSource();
if (!workspace) {
return res.redirect(
`${this.environmentService.getFrontAuthCallbackUrl()}`,
);
}
const existingUser = await mainDataSource
.getRepository(User)
.findOneBy({ email: email });
workspaceId = workspace.id;
if (existingUser) {
const loginToken = await this.tokenService.generateLoginToken(
existingUser.email,
);
return res.redirect(
this.tokenService.computeRedirectURI(loginToken.token),
);
}
let user = await this.userService.createUser(
{
data: {
email,
firstName: firstName ?? '',
lastName: lastName ?? '',
locale: 'en',
},
},
workspaceId,
);
if (!user.avatarUrl) {
let imagePath: string | undefined = undefined;
if (picture) {
// Get image buffer from url
const buffer = await getImageBufferFromUrl(picture);
// Extract mimetype and extension from buffer
const type = await FileType.fromBuffer(buffer);
// Upload image
const { paths } = await this.fileUploadService.uploadImage({
file: buffer,
filename: `${uuidV4()}.${type?.ext}`,
mimeType: type?.mime,
fileFolder: FileFolder.ProfilePicture,
});
imagePath = paths[0];
}
user = await this.userService.update({
where: {
id: user.id,
},
data: {
avatarUrl: imagePath,
},
});
}
const user = await this.authService.signUp({
email,
firstName,
lastName,
picture,
workspaceInviteHash,
});
const loginToken = await this.tokenService.generateLoginToken(user.email);

View File

@ -17,13 +17,7 @@ export class VerifyAuthController {
const email = await this.tokenService.verifyLoginToken(
verifyInput.loginToken,
);
const result = await this.authService.verify(email, {
id: true,
firstName: true,
lastName: true,
email: true,
emailVerified: true,
});
const result = await this.authService.verify(email);
return result;
}

View File

@ -0,0 +1,15 @@
import { ArgsType, Field } from '@nestjs/graphql';
import { IsNotEmpty, IsString } from 'class-validator';
@ArgsType()
export class ApiKeyTokenInput {
@Field(() => String)
@IsNotEmpty()
@IsString()
apiKeyId: string;
@Field(() => String)
@IsNotEmpty()
expiresAt: string;
}

View File

@ -1,7 +1,5 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { ApiKey } from 'src/core/@generated/api-key/api-key.model';
@ObjectType()
export class AuthToken {
@Field(() => String)
@ -12,7 +10,7 @@ export class AuthToken {
}
@ObjectType()
export class ApiKeyToken extends ApiKey {
export class ApiKeyToken {
@Field(() => String)
token: string;
}

View File

@ -1,6 +1,6 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { User } from 'src/core/@generated/user/user.model';
import { User } from 'src/core/user/user.entity';
import { AuthTokens } from './token.entity';

View File

@ -4,11 +4,15 @@ import {
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Prisma } from '@prisma/client';
import FileType from 'file-type';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
import { ChallengeInput } from 'src/core/auth/dto/challenge.input';
import { UserService } from 'src/core/user/user.service';
import { assert } from 'src/utils/assert';
import {
PASSWORD_REGEX,
@ -17,9 +21,13 @@ import {
} from 'src/core/auth/auth.util';
import { Verify } from 'src/core/auth/dto/verify.entity';
import { UserExists } from 'src/core/auth/dto/user-exists.entity';
import { WorkspaceService } from 'src/core/workspace/services/workspace.service';
import { WorkspaceInviteHashValid } from 'src/core/auth/dto/workspace-invite-hash-valid.entity';
import { SignUpInput } from 'src/core/auth/dto/sign-up.input';
import { User } from 'src/core/user/user.entity';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { UserService } from 'src/core/user/services/user.service';
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
import { getImageBufferFromUrl } from 'src/utils/image';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { TokenService } from './token.service';
@ -34,14 +42,17 @@ export class AuthService {
constructor(
private readonly tokenService: TokenService,
private readonly userService: UserService,
private readonly workspaceService: WorkspaceService,
private readonly workspaceManagerService: WorkspaceManagerService,
private readonly fileUploadService: FileUploadService,
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async challenge(challengeInput: ChallengeInput) {
const user = await this.userService.findUnique({
where: {
email: challengeInput.email,
},
const user = await this.userRepository.findOneBy({
email: challengeInput.email,
});
assert(user, "This user doesn't exist", NotFoundException);
@ -57,24 +68,40 @@ export class AuthService {
return user;
}
async signUp(signUpInput: SignUpInput) {
const existingUser = await this.userService.findUnique({
where: {
email: signUpInput.email,
},
async signUp({
email,
password,
workspaceInviteHash,
firstName,
lastName,
picture,
}: {
email: string;
password?: string;
firstName?: string | null;
lastName?: string | null;
workspaceInviteHash?: string | null;
picture?: string | null;
}) {
if (!firstName) firstName = '';
if (!lastName) lastName = '';
const existingUser = await this.userRepository.findOneBy({
email: email,
});
assert(!existingUser, 'This user already exists', ForbiddenException);
const isPasswordValid = PASSWORD_REGEX.test(signUpInput.password);
assert(isPasswordValid, 'Password too weak', BadRequestException);
if (password) {
const isPasswordValid = PASSWORD_REGEX.test(password);
assert(isPasswordValid, 'Password too weak', BadRequestException);
}
const passwordHash = await hashPassword(signUpInput.password);
const passwordHash = password ? await hashPassword(password) : undefined;
let workspace: Workspace | null;
if (signUpInput.workspaceInviteHash) {
const workspace = await this.workspaceService.findFirst({
where: {
inviteHash: signUpInput.workspaceInviteHash,
},
if (workspaceInviteHash) {
workspace = await this.workspaceRepository.findOneBy({
inviteHash: workspaceInviteHash,
});
assert(
@ -82,44 +109,59 @@ export class AuthService {
'This workspace inviteHash is invalid',
ForbiddenException,
);
return await this.userService.createUser(
{
data: {
email: signUpInput.email,
passwordHash,
},
} as Prisma.UserCreateArgs,
workspace.id,
);
} else {
const workspaceToCreate = this.workspaceRepository.create({
displayName: '',
domainName: '',
inviteHash: v4(),
});
workspace = await this.workspaceRepository.save(workspaceToCreate);
await this.workspaceManagerService.init(workspace.id);
}
return await this.userService.createUser({
data: {
email: signUpInput.email,
passwordHash,
locale: 'en',
},
} as Prisma.UserCreateArgs);
const userToCreate = this.userRepository.create({
email: email,
firstName: firstName,
lastName: lastName,
canImpersonate: false,
passwordHash,
defaultWorkspace: workspace,
});
const user = await this.userRepository.save(userToCreate);
let imagePath: string | undefined = undefined;
if (picture) {
const buffer = await getImageBufferFromUrl(picture);
const type = await FileType.fromBuffer(buffer);
const { paths } = await this.fileUploadService.uploadImage({
file: buffer,
filename: `${v4()}.${type?.ext}`,
mimeType: type?.mime,
fileFolder: FileFolder.ProfilePicture,
});
imagePath = paths[0];
}
await this.userService.createWorkspaceMember(user, imagePath);
return user;
}
async verify(
email: string,
select: Prisma.UserSelect & {
id: true;
},
): Promise<Verify> {
const user = await this.userService.findUnique({
async verify(email: string): Promise<Verify> {
const user = await this.userRepository.findOne({
where: {
email,
},
select,
relations: ['defaultWorkspace'],
});
assert(user, "This user doesn't exist", NotFoundException);
// passwordHash is hidden for security reasons
user.passwordHash = '';
user.workspaceMember = await this.userService.loadWorkspaceMember(user);
const accessToken = await this.tokenService.generateAccessToken(user.id);
const refreshToken = await this.tokenService.generateRefreshToken(user.id);
@ -134,10 +176,8 @@ export class AuthService {
}
async checkUserExists(email: string): Promise<UserExists> {
const user = await this.userService.findUnique({
where: {
email,
},
const user = await this.userRepository.findOneBy({
email,
});
return { exists: !!user };
@ -146,26 +186,16 @@ export class AuthService {
async checkWorkspaceInviteHashIsValid(
inviteHash: string,
): Promise<WorkspaceInviteHashValid> {
const workspace = await this.workspaceService.findFirst({
where: {
inviteHash,
},
const workspace = await this.workspaceRepository.findOneBy({
inviteHash,
});
return { isValid: !!workspace };
}
async impersonate(
userId: string,
select: Prisma.UserSelect & {
id: true;
},
) {
const user = await this.userService.findUnique({
where: {
id: userId,
},
select,
async impersonate(userId: string) {
const user = await this.userRepository.findOneBy({
id: userId,
});
assert(user, "This user doesn't exist", NotFoundException);

View File

@ -1,8 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { TokenService } from './token.service';
@ -22,10 +20,6 @@ describe('TokenService', () => {
provide: EnvironmentService,
useValue: {},
},
{
provide: PrismaService,
useValue: prismaMock,
},
],
}).compile();

View File

@ -7,23 +7,29 @@ import {
UnprocessableEntityException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { addMilliseconds } from 'date-fns';
import ms from 'ms';
import { TokenExpiredError } from 'jsonwebtoken';
import { Repository } from 'typeorm';
import { JwtPayload } from 'src/core/auth/strategies/jwt.auth.strategy';
import { PrismaService } from 'src/database/prisma.service';
import { assert } from 'src/utils/assert';
import { AuthToken } from 'src/core/auth/dto/token.entity';
import { ApiKeyToken, AuthToken } from 'src/core/auth/dto/token.entity';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { User } from 'src/core/user/user.entity';
import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity';
@Injectable()
export class TokenService {
constructor(
private readonly jwtService: JwtService,
private readonly environmentService: EnvironmentService,
private readonly prismaService: PrismaService,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
@InjectRepository(RefreshToken)
private readonly refreshTokenRepository: Repository<RefreshToken>,
) {}
async generateAccessToken(userId: string): Promise<AuthToken> {
@ -31,23 +37,26 @@ export class TokenService {
assert(expiresIn, '', InternalServerErrorException);
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
const user = await this.prismaService.client.user.findUnique({
const user = await this.userRepository.findOne({
where: { id: userId },
relations: ['defaultWorkspace'],
});
if (!user) {
throw new NotFoundException('User is not found');
}
if (!user.defaultWorkspaceId) {
if (!user.defaultWorkspace) {
throw new NotFoundException('User does not have a default workspace');
}
const jwtPayload: JwtPayload = {
sub: user.id,
workspaceId: user.defaultWorkspaceId,
workspaceId: user.defaultWorkspace.id,
};
console.log(jwtPayload);
return {
token: this.jwtService.sign(jwtPayload),
expiresAt,
@ -68,9 +77,13 @@ export class TokenService {
sub: userId,
};
const refreshToken = await this.prismaService.client.refreshToken.create({
data: refreshTokenPayload,
});
const refreshToken =
this.refreshTokenRepository.create(refreshTokenPayload);
console.log(refreshToken);
await this.refreshTokenRepository.save(refreshToken);
console.log('toto');
return {
token: this.jwtService.sign(jwtPayload, {
@ -101,6 +114,34 @@ export class TokenService {
};
}
async generateApiKeyToken(
workspaceId: string,
apiKeyId?: string,
expiresAt?: Date | string,
): Promise<Pick<ApiKeyToken, 'token'> | undefined> {
if (!apiKeyId) {
return;
}
const jwtPayload = {
sub: workspaceId,
};
const secret = this.environmentService.getAccessTokenSecret();
let expiresIn: string | number;
if (expiresAt) {
expiresIn = Math.floor(
(new Date(expiresAt).getTime() - new Date().getTime()) / 1000,
);
} else {
expiresIn = this.environmentService.getApiTokenExpiresIn();
}
const token = this.jwtService.sign(jwtPayload, {
secret,
expiresIn,
jwtid: apiKeyId,
});
return { token };
}
async verifyLoginToken(loginToken: string): Promise<string> {
const loginTokenSecret = this.environmentService.getLoginTokenSecret();
@ -120,19 +161,14 @@ export class TokenService {
UnprocessableEntityException,
);
const token = await this.prismaService.client.refreshToken.findUnique({
where: { id: jwtPayload.jti },
const token = await this.refreshTokenRepository.findOneBy({
id: jwtPayload.jti,
});
assert(token, "This refresh token doesn't exist", NotFoundException);
const user = await this.prismaService.client.user.findUnique({
where: {
id: jwtPayload.sub,
},
include: {
refreshTokens: true,
},
const user = await this.userRepository.findOneBy({
id: jwtPayload.sub,
});
assert(user, 'User not found', NotFoundException);
@ -143,16 +179,17 @@ export class TokenService {
token.revokedAt.getTime() <= Date.now() - ms(coolDown)
) {
// Revoke all user refresh tokens
await this.prismaService.client.refreshToken.updateMany({
where: {
id: {
in: user.refreshTokens.map(({ id }) => id),
},
},
data: {
revokedAt: new Date(),
},
});
await Promise.all(
user.refreshTokens.map(
async ({ id }) =>
await this.refreshTokenRepository.update(
{ id },
{
revokedAt: new Date(),
},
),
),
);
throw new ForbiddenException(
'Suspicious activity detected, this refresh token has been revoked. All tokens has been revoked.',
@ -172,14 +209,14 @@ export class TokenService {
} = await this.verifyRefreshToken(token);
// Revoke old refresh token
await this.prismaService.client.refreshToken.update({
where: {
await this.refreshTokenRepository.update(
{
id,
},
data: {
{
revokedAt: new Date(),
},
});
);
const accessToken = await this.generateAccessToken(user.id);
const refreshToken = await this.generateRefreshToken(user.id);

View File

@ -1,16 +1,13 @@
import { PassportStrategy } from '@nestjs/passport';
import {
ForbiddenException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { User, Workspace } from '@prisma/client';
import { Repository } from 'typeorm';
import { PrismaService } from 'src/database/prisma.service';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { assert } from 'src/utils/assert';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { User } from 'src/core/user/user.entity';
export type JwtPayload = { sub: string; workspaceId: string; jti?: string };
export type PassportUser = { user?: User; workspace: Workspace };
@ -19,7 +16,10 @@ export type PassportUser = { user?: User; workspace: Workspace };
export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
private readonly environmentService: EnvironmentService,
private readonly prismaService: PrismaService,
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
@ -29,26 +29,30 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
}
async validate(payload: JwtPayload): Promise<PassportUser> {
const workspace = await this.prismaService.client.workspace.findUnique({
where: { id: payload.workspaceId ?? payload.sub },
const workspace = await this.workspaceRepository.findOneBy({
id: payload.workspaceId ?? payload.sub,
});
if (!workspace) {
throw new UnauthorizedException();
}
if (payload.jti) {
// If apiKey has been deleted or revoked, we throw an error
const apiKey = await this.prismaService.client.apiKey.findUniqueOrThrow({
where: { id: payload.jti },
});
assert(!apiKey.revokedAt, 'This API Key is revoked', ForbiddenException);
// const apiKey = await this.prismaService.client.apiKey.findUniqueOrThrow({
// where: { id: payload.jti },
// });
// assert(!apiKey.revokedAt, 'This API Key is revoked', ForbiddenException);
}
const user = payload.workspaceId
? await this.prismaService.client.user.findUniqueOrThrow({
where: { id: payload.sub },
? await this.userRepository.findOneBy({
id: payload.sub,
})
: undefined;
if (!user) {
throw new UnauthorizedException();
}
return { user, workspace };
}
}

View File

@ -1,14 +0,0 @@
import { Module } from '@nestjs/common';
import { AbilityModule } from 'src/ability/ability.module';
import { PrismaModule } from 'src/database/prisma.module';
import { CommentService } from './comment.service';
import { CommentResolver } from './comment.resolver';
@Module({
imports: [AbilityModule, PrismaModule],
providers: [CommentService, CommentResolver],
exports: [CommentService],
})
export class CommentModule {}

View File

@ -1,32 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CommentService } from 'src/core/comment/comment.service';
import { AbilityFactory } from 'src/ability/ability.factory';
import { CommentResolver } from './comment.resolver';
describe('CommentResolver', () => {
let resolver: CommentResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CommentResolver,
{
provide: CommentService,
useValue: {},
},
{
provide: AbilityFactory,
useValue: {},
},
],
}).compile();
resolver = module.get<CommentResolver>(CommentResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -1,47 +0,0 @@
import { Resolver, Args, Mutation } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { CreateOneCommentArgs } from 'src/core/@generated/comment/create-one-comment.args';
import { Comment } from 'src/core/@generated/comment/comment.model';
import { CommentService } from 'src/core/comment/comment.service';
import {
PrismaSelector,
PrismaSelect,
} from 'src/decorators/prisma-select.decorator';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import { CreateCommentAbilityHandler } from 'src/ability/handlers/comment.ability-handler';
import { AuthUser } from 'src/decorators/auth-user.decorator';
import { User } from 'src/core/@generated/user/user.model';
@UseGuards(JwtAuthGuard)
@Resolver(() => Comment)
export class CommentResolver {
constructor(private readonly commentService: CommentService) {}
@Mutation(() => Comment, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(CreateCommentAbilityHandler)
async createOneComment(
@Args() args: CreateOneCommentArgs,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'Comment' })
prismaSelect: PrismaSelect<'Comment'>,
): Promise<Partial<Comment>> {
return this.commentService.create({
data: {
...args.data,
...{ workspace: { connect: { id: workspace.id } } },
},
select: prismaSelect.value,
} as Prisma.CommentCreateArgs);
}
}

View File

@ -1,28 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { CommentService } from './comment.service';
describe('CommentService', () => {
let service: CommentService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CommentService,
{
provide: PrismaService,
useValue: prismaMock,
},
],
}).compile();
service = module.get<CommentService>(CommentService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,39 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class CommentService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.comment.findFirst;
findFirstOrThrow = this.prismaService.client.comment.findFirstOrThrow;
findUnique = this.prismaService.client.comment.findUnique;
findUniqueOrThrow = this.prismaService.client.comment.findUniqueOrThrow;
findMany = this.prismaService.client.comment.findMany;
// Create
create = this.prismaService.client.comment.create;
createMany = this.prismaService.client.comment.createMany;
// Update
update = this.prismaService.client.comment.update;
upsert = this.prismaService.client.comment.upsert;
updateMany = this.prismaService.client.comment.updateMany;
// Delete
delete = this.prismaService.client.comment.delete;
deleteMany = this.prismaService.client.comment.deleteMany;
// Aggregate
aggregate = this.prismaService.client.comment.aggregate;
// Count
count = this.prismaService.client.comment.count;
// GroupBy
groupBy = this.prismaService.client.comment.groupBy;
}

View File

@ -1,37 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CommentService } from 'src/core/comment/comment.service';
import { ActivityService } from 'src/core/activity/services/activity.service';
import { CompanyRelationsResolver } from './company-relations.resolver';
import { CompanyService } from './company.service';
describe('CompanyRelationsResolver', () => {
let resolver: CompanyRelationsResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CompanyRelationsResolver,
{
provide: CompanyService,
useValue: {},
},
{
provide: ActivityService,
useValue: {},
},
{
provide: CommentService,
useValue: {},
},
],
}).compile();
resolver = module.get<CompanyRelationsResolver>(CompanyRelationsResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -1,76 +0,0 @@
import { Resolver, ResolveField, Root, Int } from '@nestjs/graphql';
import { Comment } from 'src/core/@generated/comment/comment.model';
import { Company } from 'src/core/@generated/company/company.model';
import { CommentService } from 'src/core/comment/comment.service';
import {
PrismaSelect,
PrismaSelector,
} from 'src/decorators/prisma-select.decorator';
import { ActivityService } from 'src/core/activity/services/activity.service';
import { Activity } from 'src/core/@generated/activity/activity.model';
@Resolver(() => Company)
export class CompanyRelationsResolver {
constructor(
private readonly activityService: ActivityService,
private readonly commentService: CommentService,
) {}
@ResolveField(() => [Activity], {
nullable: false,
})
async activities(
@Root() company: Company,
@PrismaSelector({ modelName: 'Activity' })
prismaSelect: PrismaSelect<'Activity'>,
): Promise<Partial<Activity>[]> {
return this.activityService.findMany({
where: {
activityTargets: {
some: {
companyId: company.id,
},
},
},
select: prismaSelect.value,
});
}
@ResolveField(() => [Comment], {
nullable: false,
})
async comments(
@Root() company: Company,
@PrismaSelector({ modelName: 'Comment' })
prismaSelect: PrismaSelect<'Comment'>,
): Promise<Partial<Comment>[]> {
return this.commentService.findMany({
where: {
activity: {
activityTargets: {
some: {
companyId: company.id,
},
},
},
},
select: prismaSelect.value,
});
}
@ResolveField(() => Int, {
nullable: false,
})
async _activityCount(@Root() company: Company): Promise<number> {
return this.activityService.count({
where: {
activityTargets: {
some: {
companyId: company.id,
},
},
},
});
}
}

View File

@ -1,17 +0,0 @@
import { Module } from '@nestjs/common';
import { CommentModule } from 'src/core/comment/comment.module';
import { ActivityModule } from 'src/core/activity/activity.module';
import { AbilityModule } from 'src/ability/ability.module';
import { PrismaModule } from 'src/database/prisma.module';
import { CompanyService } from './company.service';
import { CompanyResolver } from './company.resolver';
import { CompanyRelationsResolver } from './company-relations.resolver';
@Module({
imports: [CommentModule, ActivityModule, AbilityModule, PrismaModule],
providers: [CompanyService, CompanyResolver, CompanyRelationsResolver],
exports: [CompanyService],
})
export class CompanyModule {}

View File

@ -1,32 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AbilityFactory } from 'src/ability/ability.factory';
import { CompanyService } from './company.service';
import { CompanyResolver } from './company.resolver';
describe('CompanyResolver', () => {
let resolver: CompanyResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CompanyResolver,
{
provide: CompanyService,
useValue: {},
},
{
provide: AbilityFactory,
useValue: {},
},
],
}).compile();
resolver = module.get<CompanyResolver>(CompanyResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -1,145 +0,0 @@
import { Resolver, Query, Args, Mutation } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { Prisma, Workspace } from '@prisma/client';
import { accessibleBy } from '@casl/prisma';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { Company } from 'src/core/@generated/company/company.model';
import { FindManyCompanyArgs } from 'src/core/@generated/company/find-many-company.args';
import { UpdateOneCompanyArgs } from 'src/core/@generated/company/update-one-company.args';
import { CreateOneCompanyArgs } from 'src/core/@generated/company/create-one-company.args';
import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output';
import { DeleteManyCompanyArgs } from 'src/core/@generated/company/delete-many-company.args';
import {
PrismaSelect,
PrismaSelector,
} from 'src/decorators/prisma-select.decorator';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
CreateCompanyAbilityHandler,
DeleteCompanyAbilityHandler,
ReadOneCompanyAbilityHandler,
UpdateCompanyAbilityHandler,
} from 'src/ability/handlers/company.ability-handler';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
import { FindUniqueCompanyArgs } from 'src/core/@generated/company/find-unique-company.args';
import { CreateManyCompanyArgs } from 'src/core/@generated/company/create-many-company.args';
import { CompanyService } from './company.service';
@UseGuards(JwtAuthGuard)
@Resolver(() => Company)
export class CompanyResolver {
constructor(private readonly companyService: CompanyService) {}
@Query(() => [Company])
@UseGuards(AbilityGuard)
async findManyCompany(
@Args() args: FindManyCompanyArgs,
@UserAbility() ability: AppAbility,
@PrismaSelector({ modelName: 'Company' })
prismaSelect: PrismaSelect<'Company'>,
): Promise<Partial<Company>[]> {
return this.companyService.findMany({
where: args.where
? {
AND: [args.where, accessibleBy(ability).Company],
}
: accessibleBy(ability).Company,
orderBy: args.orderBy,
cursor: args.cursor,
take: args.take,
skip: args.skip,
distinct: args.distinct,
select: prismaSelect.value,
});
}
@Query(() => Company)
@UseGuards(AbilityGuard)
@CheckAbilities(ReadOneCompanyAbilityHandler)
async findUniqueCompany(
@Args() args: FindUniqueCompanyArgs,
@PrismaSelector({ modelName: 'Company' })
prismaSelect: PrismaSelect<'Company'>,
): Promise<Partial<Company>> {
const company = this.companyService.findUniqueOrThrow({
where: args.where,
select: prismaSelect.value,
});
return company;
}
@Mutation(() => Company, {
nullable: true,
})
@UseGuards(AbilityGuard)
@CheckAbilities(UpdateCompanyAbilityHandler)
async updateOneCompany(
@Args() args: UpdateOneCompanyArgs,
@PrismaSelector({ modelName: 'Company' })
prismaSelect: PrismaSelect<'Company'>,
): Promise<Partial<Company> | null> {
return this.companyService.update({
where: args.where,
data: args.data,
select: prismaSelect.value,
} as Prisma.CompanyUpdateArgs);
}
@Mutation(() => AffectedRows, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(DeleteCompanyAbilityHandler)
async deleteManyCompany(
@Args() args: DeleteManyCompanyArgs,
): Promise<AffectedRows> {
return this.companyService.deleteMany({
where: args.where,
});
}
@Mutation(() => Company, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(CreateCompanyAbilityHandler)
async createOneCompany(
@Args() args: CreateOneCompanyArgs,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'Company' })
prismaSelect: PrismaSelect<'Company'>,
): Promise<Partial<Company>> {
return this.companyService.create({
data: {
...args.data,
...{ workspace: { connect: { id: workspace.id } } },
},
select: prismaSelect.value,
} as Prisma.CompanyCreateArgs);
}
@Mutation(() => AffectedRows, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(CreateCompanyAbilityHandler)
async createManyCompany(
@Args() args: CreateManyCompanyArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<Prisma.BatchPayload> {
return this.companyService.createMany({
data: args.data.map((company) => ({
...company,
workspaceId: workspace.id,
})),
skipDuplicates: args.skipDuplicates,
});
}
}

View File

@ -1,28 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { CompanyService } from './company.service';
describe('CompanyService', () => {
let service: CompanyService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CompanyService,
{
provide: PrismaService,
useValue: prismaMock,
},
],
}).compile();
service = module.get<CompanyService>(CompanyService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,51 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/database/prisma.service';
import companiesSeed from 'src/core/company/seed-data/companies.json';
@Injectable()
export class CompanyService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.company.findFirst;
findFirstOrThrow = this.prismaService.client.company.findFirstOrThrow;
findUnique = this.prismaService.client.company.findUnique;
findUniqueOrThrow = this.prismaService.client.company.findUniqueOrThrow;
findMany = this.prismaService.client.company.findMany;
// Create
create = this.prismaService.client.company.create;
createMany = this.prismaService.client.company.createMany;
// Update
update = this.prismaService.client.company.update;
upsert = this.prismaService.client.company.upsert;
updateMany = this.prismaService.client.company.updateMany;
// Delete
delete = this.prismaService.client.company.delete;
deleteMany = this.prismaService.client.company.deleteMany;
// Aggregate
aggregate = this.prismaService.client.company.aggregate;
// Count
count = this.prismaService.client.company.count;
// GroupBy
groupBy = this.prismaService.client.company.groupBy;
async createDefaultCompanies({ workspaceId }: { workspaceId: string }) {
const companies = companiesSeed.map((company) => ({
...company,
workspaceId,
}));
await this.createMany({
data: companies,
});
return this.findMany({ where: { workspaceId } });
}
}

View File

@ -1,33 +0,0 @@
[
{
"name": "Airbnb",
"domainName": "airbnb.com",
"address": "San Francisco",
"employees": 5000
},
{
"name": "Qonto",
"domainName": "qonto.com",
"address": "San Francisco",
"employees": 800
},
{
"name": "Stripe",
"domainName": "stripe.com",
"address": "San Francisco",
"employees": 8000
},
{
"name": "Figma",
"domainName": "figma.com",
"address": "San Francisco",
"employees": 800
},
{
"name": "Notion",
"domainName": "notion.com",
"address": "San Francisco",
"employees": 400
}
]

View File

@ -1,62 +1,24 @@
import { Module } from '@nestjs/common';
import { WebHookModule } from 'src/core/web-hook/web-hook.module';
import { UserModule as UserV2Module } from 'src/coreV2/user/user.module';
import { RefreshTokenModule as RefreshTokenV2Module } from 'src/coreV2/refresh-token/refresh-token.module';
import { WorkspaceModule as WorkspaceV2Module } from 'src/coreV2/workspace/workspace.module';
import { WorkspaceModule } from 'src/core/workspace/workspace.module';
import { UserModule } from 'src/core/user/user.module';
import { RefreshTokenModule } from 'src/core/refresh-token/refresh-token.module';
import { AuthModule } from 'src/core/auth/auth.module';
import { UserModule } from './user/user.module';
import { CommentModule } from './comment/comment.module';
import { CompanyModule } from './company/company.module';
import { PersonModule } from './person/person.module';
import { PipelineModule } from './pipeline/pipeline.module';
import { AuthModule } from './auth/auth.module';
import { WorkspaceModule } from './workspace/workspace.module';
import { AnalyticsModule } from './analytics/analytics.module';
import { FileModule } from './file/file.module';
import { ClientConfigModule } from './client-config/client-config.module';
import { AttachmentModule } from './attachment/attachment.module';
import { ActivityModule } from './activity/activity.module';
import { FavoriteModule } from './favorite/favorite.module';
import { ApiKeyModule } from './api-key/api-key.module';
@Module({
imports: [
AuthModule,
UserModule,
CommentModule,
CompanyModule,
PersonModule,
PipelineModule,
WorkspaceModule,
UserModule,
RefreshTokenModule,
AnalyticsModule,
FileModule,
ClientConfigModule,
AttachmentModule,
ActivityModule,
FavoriteModule,
ApiKeyModule,
WebHookModule,
UserV2Module,
RefreshTokenV2Module,
WorkspaceV2Module,
],
exports: [
AuthModule,
UserModule,
CommentModule,
CompanyModule,
PersonModule,
PipelineModule,
WorkspaceModule,
AnalyticsModule,
AttachmentModule,
FavoriteModule,
ApiKeyModule,
WebHookModule,
UserV2Module,
RefreshTokenV2Module,
WorkspaceV2Module,
],
exports: [AuthModule, WorkspaceModule, UserModule, AnalyticsModule],
})
export class CoreModule {}

View File

@ -1,14 +0,0 @@
import { Module } from '@nestjs/common';
import { AbilityModule } from 'src/ability/ability.module';
import { PrismaModule } from 'src/database/prisma.module';
import { FavoriteResolver } from './resolvers/favorite.resolver';
import { FavoriteService } from './services/favorite.service';
@Module({
imports: [AbilityModule, PrismaModule],
providers: [FavoriteService, FavoriteResolver],
exports: [FavoriteService],
})
export class FavoriteModule {}

View File

@ -1,175 +0,0 @@
import {
Resolver,
Query,
Args,
Mutation,
InputType,
Field,
} from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { Prisma, Workspace } from '@prisma/client';
import {
PrismaSelect,
PrismaSelector,
} from 'src/decorators/prisma-select.decorator';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { Favorite } from 'src/core/@generated/favorite/favorite.model';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
CreateFavoriteAbilityHandler,
DeleteFavoriteAbilityHandler,
ReadFavoriteAbilityHandler,
UpdateFavoriteAbilityHandler,
} from 'src/ability/handlers/favorite.ability-handler';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { FavoriteService } from 'src/core/favorite/services/favorite.service';
import { FavoriteWhereInput } from 'src/core/@generated/favorite/favorite-where.input';
import { SortOrder } from 'src/core/@generated/prisma/sort-order.enum';
import { UpdateOneFavoriteArgs } from 'src/core/@generated/favorite/update-one-favorite.args';
@InputType()
class FavoriteMutationForPersonArgs {
@Field(() => String)
personId: string;
@Field(() => Number)
position: number;
}
@InputType()
class FavoriteMutationForCompanyArgs {
@Field(() => String)
companyId: string;
@Field(() => Number)
position: number;
}
@UseGuards(JwtAuthGuard)
@Resolver(() => Favorite)
export class FavoriteResolver {
constructor(private readonly favoriteService: FavoriteService) {}
@Query(() => [Favorite])
@UseGuards(AbilityGuard)
@CheckAbilities(ReadFavoriteAbilityHandler)
async findFavorites(
@AuthWorkspace() workspace: Workspace,
): Promise<Partial<Favorite>[]> {
const favorites = await this.favoriteService.findMany({
where: {
workspaceId: workspace.id,
},
orderBy: [{ position: SortOrder.asc }],
include: {
person: true,
company: {
include: {
accountOwner: true,
},
},
},
});
return favorites;
}
@Mutation(() => Favorite, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(CreateFavoriteAbilityHandler)
async createFavoriteForPerson(
@Args('data') args: FavoriteMutationForPersonArgs,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'Favorite' })
prismaSelect: PrismaSelect<'Favorite'>,
): Promise<Partial<Favorite>> {
//To avoid duplicates we first fetch all favorites assinged by workspace
const favorite = await this.favoriteService.findFirst({
where: { workspaceId: workspace.id, personId: args.personId },
});
if (favorite) return favorite;
return this.favoriteService.create({
data: {
person: {
connect: { id: args.personId },
},
workspaceId: workspace.id,
position: args.position,
},
select: prismaSelect.value,
});
}
@Mutation(() => Favorite, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(CreateFavoriteAbilityHandler)
async createFavoriteForCompany(
@Args('data') args: FavoriteMutationForCompanyArgs,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'Favorite' })
prismaSelect: PrismaSelect<'Favorite'>,
): Promise<Partial<Favorite>> {
//To avoid duplicates we first fetch all favorites assinged by workspace
const favorite = await this.favoriteService.findFirst({
where: { workspaceId: workspace.id, companyId: args.companyId },
});
if (favorite) return favorite;
return this.favoriteService.create({
data: {
company: {
connect: { id: args.companyId },
},
workspaceId: workspace.id,
position: args.position,
},
select: prismaSelect.value,
});
}
@Mutation(() => Favorite, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(UpdateFavoriteAbilityHandler)
async updateOneFavorites(
@Args() args: UpdateOneFavoriteArgs,
@PrismaSelector({ modelName: 'Favorite' })
prismaSelect: PrismaSelect<'Favorite'>,
): Promise<Partial<Favorite>> {
return this.favoriteService.update({
data: args.data,
where: args.where,
select: prismaSelect.value,
} as Prisma.FavoriteUpdateArgs);
}
@Mutation(() => Favorite, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(DeleteFavoriteAbilityHandler)
async deleteFavorite(
@Args('where') args: FavoriteWhereInput,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'Favorite' })
prismaSelect: PrismaSelect<'Favorite'>,
): Promise<Partial<Favorite>> {
const favorite = await this.favoriteService.findFirst({
where: { ...args, workspaceId: workspace.id },
});
return this.favoriteService.delete({
where: { id: favorite?.id },
select: prismaSelect.value,
});
}
}

View File

@ -1,39 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class FavoriteService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.favorite.findFirst;
findFirstOrThrow = this.prismaService.client.favorite.findFirstOrThrow;
findUnique = this.prismaService.client.favorite.findUnique;
findUniqueOrThrow = this.prismaService.client.favorite.findUniqueOrThrow;
findMany = this.prismaService.client.favorite.findMany;
// Create
create = this.prismaService.client.favorite.create;
createMany = this.prismaService.client.favorite.createMany;
// Update
update = this.prismaService.client.favorite.update;
upsert = this.prismaService.client.favorite.upsert;
updateMany = this.prismaService.client.favorite.updateMany;
// Delete
delete = this.prismaService.client.favorite.delete;
deleteMany = this.prismaService.client.favorite.deleteMany;
// Aggregate
aggregate = this.prismaService.client.favorite.aggregate;
// Count
count = this.prismaService.client.favorite.count;
// GroupBy
groupBy = this.prismaService.client.favorite.groupBy;
}

View File

@ -1,37 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CommentService } from 'src/core/comment/comment.service';
import { ActivityService } from 'src/core/activity/services/activity.service';
import { PersonRelationsResolver } from './person-relations.resolver';
import { PersonService } from './person.service';
describe('PersonRelationsResolver', () => {
let resolver: PersonRelationsResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PersonRelationsResolver,
{
provide: PersonService,
useValue: {},
},
{
provide: ActivityService,
useValue: {},
},
{
provide: CommentService,
useValue: {},
},
],
}).compile();
resolver = module.get<PersonRelationsResolver>(PersonRelationsResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -1,76 +0,0 @@
import { Resolver, Root, ResolveField, Int } from '@nestjs/graphql';
import { Comment } from 'src/core/@generated/comment/comment.model';
import { Person } from 'src/core/@generated/person/person.model';
import { CommentService } from 'src/core/comment/comment.service';
import {
PrismaSelect,
PrismaSelector,
} from 'src/decorators/prisma-select.decorator';
import { Activity } from 'src/core/@generated/activity/activity.model';
import { ActivityService } from 'src/core/activity/services/activity.service';
@Resolver(() => Person)
export class PersonRelationsResolver {
constructor(
private readonly activityService: ActivityService,
private readonly commentService: CommentService,
) {}
@ResolveField(() => [Activity], {
nullable: false,
})
async activities(
@Root() person: Person,
@PrismaSelector({ modelName: 'Activity' })
prismaSelect: PrismaSelect<'Activity'>,
): Promise<Partial<Activity>[]> {
return await this.activityService.findMany({
where: {
activityTargets: {
some: {
personId: person.id,
},
},
},
select: prismaSelect.value,
});
}
@ResolveField(() => [Comment], {
nullable: false,
})
async comments(
@Root() person: Person,
@PrismaSelector({ modelName: 'Comment' })
prismaSelect: PrismaSelect<'Comment'>,
): Promise<Partial<Comment>[]> {
return this.commentService.findMany({
where: {
activity: {
activityTargets: {
some: {
personId: person.id,
},
},
},
},
select: prismaSelect.value,
});
}
@ResolveField(() => Int, {
nullable: false,
})
async _activityCount(@Root() person: Person): Promise<number> {
return this.activityService.count({
where: {
activityTargets: {
some: {
personId: person.id,
},
},
},
});
}
}

View File

@ -1,24 +0,0 @@
import { Module } from '@nestjs/common';
import { CommentModule } from 'src/core/comment/comment.module';
import { ActivityModule } from 'src/core/activity/activity.module';
import { FileModule } from 'src/core/file/file.module';
import { AbilityModule } from 'src/ability/ability.module';
import { PrismaModule } from 'src/database/prisma.module';
import { PersonService } from './person.service';
import { PersonResolver } from './person.resolver';
import { PersonRelationsResolver } from './person-relations.resolver';
@Module({
imports: [
CommentModule,
ActivityModule,
FileModule,
AbilityModule,
PrismaModule,
],
providers: [PersonService, PersonResolver, PersonRelationsResolver],
exports: [PersonService],
})
export class PersonModule {}

View File

@ -1,37 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AbilityFactory } from 'src/ability/ability.factory';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { PersonService } from './person.service';
import { PersonResolver } from './person.resolver';
describe('PersonResolver', () => {
let resolver: PersonResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PersonResolver,
{
provide: PersonService,
useValue: {},
},
{
provide: AbilityFactory,
useValue: {},
},
{
provide: FileUploadService,
useValue: {},
},
],
}).compile();
resolver = module.get<PersonResolver>(PersonResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -1,214 +0,0 @@
import {
Resolver,
Query,
Args,
Mutation,
ResolveField,
Parent,
} from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { accessibleBy } from '@casl/prisma';
import { Prisma } from '@prisma/client';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { Person } from 'src/core/@generated/person/person.model';
import { FindManyPersonArgs } from 'src/core/@generated/person/find-many-person.args';
import { UpdateOnePersonArgs } from 'src/core/@generated/person/update-one-person.args';
import { CreateOnePersonArgs } from 'src/core/@generated/person/create-one-person.args';
import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output';
import { DeleteManyPersonArgs } from 'src/core/@generated/person/delete-many-person.args';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import {
PrismaSelect,
PrismaSelector,
} from 'src/decorators/prisma-select.decorator';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
CreatePersonAbilityHandler,
DeletePersonAbilityHandler,
ReadPersonAbilityHandler,
UpdatePersonAbilityHandler,
} from 'src/ability/handlers/person.ability-handler';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { CreateManyPersonArgs } from 'src/core/@generated/person/create-many-person.args';
import { PersonService } from './person.service';
@UseGuards(JwtAuthGuard)
@Resolver(() => Person)
export class PersonResolver {
constructor(
private readonly personService: PersonService,
private readonly fileUploadService: FileUploadService,
) {}
@Query(() => [Person], {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(ReadPersonAbilityHandler)
async findManyPerson(
@Args() args: FindManyPersonArgs,
@UserAbility() ability: AppAbility,
@PrismaSelector({ modelName: 'Person' })
prismaSelect: PrismaSelect<'Person'>,
): Promise<Partial<Person>[]> {
return this.personService.findMany({
where: args.where
? {
AND: [args.where, accessibleBy(ability).Person],
}
: accessibleBy(ability).Person,
orderBy: args.orderBy,
cursor: args.cursor,
take: args.take,
skip: args.skip,
distinct: args.distinct,
select: prismaSelect.value,
});
}
@Query(() => Person)
@UseGuards(AbilityGuard)
@CheckAbilities(ReadPersonAbilityHandler)
async findUniquePerson(
@Args('id') id: string,
@UserAbility() ability: AppAbility,
@PrismaSelector({ modelName: 'Person' })
prismaSelect: PrismaSelect<'Person'>,
): Promise<Partial<Person>> {
return this.personService.findUniqueOrThrow({
where: {
id: id,
},
select: prismaSelect.value,
});
}
@ResolveField(() => String, {
nullable: false,
})
displayName(@Parent() parent: Person): string {
return `${parent.firstName ?? ''} ${parent.lastName ?? ''}`;
}
@Mutation(() => Person, {
nullable: true,
})
@UseGuards(AbilityGuard)
@CheckAbilities(UpdatePersonAbilityHandler)
async updateOnePerson(
@Args() args: UpdateOnePersonArgs,
@PrismaSelector({ modelName: 'Person' })
prismaSelect: PrismaSelect<'Person'>,
): Promise<Partial<Person> | null> {
// TODO: Do a proper check with recursion testing on args in a more generic place
for (const key in args.data) {
if (args.data[key]) {
for (const subKey in args.data[key]) {
if (JSON.stringify(args.data[key][subKey]) === '{}') {
delete args.data[key][subKey];
}
}
}
if (JSON.stringify(args.data[key]) === '{}') {
delete args.data[key];
}
}
return this.personService.update({
where: args.where,
data: args.data,
select: prismaSelect.value,
} as Prisma.PersonUpdateArgs);
}
@Mutation(() => AffectedRows, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(DeletePersonAbilityHandler)
async deleteManyPerson(
@Args() args: DeleteManyPersonArgs,
): Promise<AffectedRows> {
return this.personService.deleteMany({
where: args.where,
});
}
@Mutation(() => Person, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(CreatePersonAbilityHandler)
async createOnePerson(
@Args() args: CreateOnePersonArgs,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'Person' })
prismaSelect: PrismaSelect<'Person'>,
): Promise<Partial<Person>> {
return this.personService.create({
data: {
...args.data,
...{ workspace: { connect: { id: workspace.id } } },
},
select: prismaSelect.value,
} as Prisma.PersonCreateArgs);
}
@Mutation(() => AffectedRows, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(CreatePersonAbilityHandler)
async createManyPerson(
@Args() args: CreateManyPersonArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<Prisma.BatchPayload> {
return this.personService.createMany({
data: args.data.map((person) => ({
...person,
workspaceId: workspace.id,
})),
skipDuplicates: args.skipDuplicates,
});
}
@Mutation(() => String)
@UseGuards(AbilityGuard)
@CheckAbilities(UpdatePersonAbilityHandler)
async uploadPersonPicture(
@Args('id') id: string,
@Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename, mimetype }: FileUpload,
): Promise<string> {
const stream = createReadStream();
const buffer = await streamToBuffer(stream);
const { paths } = await this.fileUploadService.uploadImage({
file: buffer,
filename,
mimeType: mimetype,
fileFolder: FileFolder.PersonPicture,
});
await this.personService.update({
where: { id },
data: {
avatarUrl: paths[0],
},
});
return paths[0];
}
}

View File

@ -1,28 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { PersonService } from './person.service';
describe('PersonService', () => {
let service: PersonService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PersonService,
{
provide: PrismaService,
useValue: prismaMock,
},
],
}).compile();
service = module.get<PersonService>(PersonService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,58 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Company } from '@prisma/client';
import { PrismaService } from 'src/database/prisma.service';
import peopleSeed from 'src/core/person/seed-data/people.json';
@Injectable()
export class PersonService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.person.findFirst;
findFirstOrThrow = this.prismaService.client.person.findFirstOrThrow;
findUnique = this.prismaService.client.person.findUnique;
findUniqueOrThrow = this.prismaService.client.person.findUniqueOrThrow;
findMany = this.prismaService.client.person.findMany;
// Create
create = this.prismaService.client.person.create;
createMany = this.prismaService.client.person.createMany;
// Update
update = this.prismaService.client.person.update;
upsert = this.prismaService.client.person.upsert;
updateMany = this.prismaService.client.person.updateMany;
// Delete
delete = this.prismaService.client.person.delete;
deleteMany = this.prismaService.client.person.deleteMany;
// Aggregate
aggregate = this.prismaService.client.person.aggregate;
// Count
count = this.prismaService.client.person.count;
// GroupBy
groupBy = this.prismaService.client.person.groupBy;
async createDefaultPeople({
workspaceId,
companies,
}: {
workspaceId: string;
companies: Company[];
}) {
const people = peopleSeed.map((person, i) => ({
...person,
companyId: companies[i].id || null,
workspaceId,
}));
return this.createMany({
data: people,
});
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,25 +0,0 @@
import { Module } from '@nestjs/common';
import { AbilityModule } from 'src/ability/ability.module';
import { PrismaModule } from 'src/database/prisma.module';
import { PipelineService } from './services/pipeline.service';
import { PipelineResolver } from './resolvers/pipeline.resolver';
import { PipelineStageResolver } from './resolvers/pipeline-stage.resolver';
import { PipelineProgressResolver } from './resolvers/pipeline-progress.resolver';
import { PipelineStageService } from './services/pipeline-stage.service';
import { PipelineProgressService } from './services/pipeline-progress.service';
@Module({
imports: [AbilityModule, PrismaModule],
providers: [
PipelineService,
PipelineStageService,
PipelineProgressService,
PipelineResolver,
PipelineStageResolver,
PipelineProgressResolver,
],
exports: [PipelineService, PipelineStageService, PipelineProgressService],
})
export class PipelineModule {}

View File

@ -1,32 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PipelineProgressService } from 'src/core/pipeline/services/pipeline-progress.service';
import { AbilityFactory } from 'src/ability/ability.factory';
import { PipelineProgressResolver } from './pipeline-progress.resolver';
describe('PipelineProgressResolver', () => {
let resolver: PipelineProgressResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PipelineProgressResolver,
{
provide: PipelineProgressService,
useValue: {},
},
{
provide: AbilityFactory,
useValue: {},
},
],
}).compile();
resolver = module.get<PipelineProgressResolver>(PipelineProgressResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -1,126 +0,0 @@
import { Resolver, Args, Query, Mutation } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { accessibleBy } from '@casl/prisma';
import { Prisma } from '@prisma/client';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { FindManyPipelineProgressArgs } from 'src/core/@generated/pipeline-progress/find-many-pipeline-progress.args';
import { PipelineProgress } from 'src/core/@generated/pipeline-progress/pipeline-progress.model';
import { UpdateOnePipelineProgressArgs } from 'src/core/@generated/pipeline-progress/update-one-pipeline-progress.args';
import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output';
import { DeleteManyPipelineProgressArgs } from 'src/core/@generated/pipeline-progress/delete-many-pipeline-progress.args';
import { CreateOnePipelineProgressArgs } from 'src/core/@generated/pipeline-progress/create-one-pipeline-progress.args';
import { PipelineProgressService } from 'src/core/pipeline/services/pipeline-progress.service';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
CreatePipelineProgressAbilityHandler,
ReadPipelineProgressAbilityHandler,
UpdatePipelineProgressAbilityHandler,
DeletePipelineProgressAbilityHandler,
} from 'src/ability/handlers/pipeline-progress.ability-handler';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
import {
PrismaSelector,
PrismaSelect,
} from 'src/decorators/prisma-select.decorator';
@UseGuards(JwtAuthGuard)
@Resolver(() => PipelineProgress)
export class PipelineProgressResolver {
constructor(
private readonly pipelineProgressService: PipelineProgressService,
) {}
@Query(() => [PipelineProgress])
@UseGuards(AbilityGuard)
@CheckAbilities(ReadPipelineProgressAbilityHandler)
async findManyPipelineProgress(
@Args() args: FindManyPipelineProgressArgs,
@UserAbility() ability: AppAbility,
@PrismaSelector({ modelName: 'PipelineProgress' })
prismaSelect: PrismaSelect<'PipelineProgress'>,
): Promise<Partial<PipelineProgress>[]> {
return this.pipelineProgressService.findMany({
where: args.where
? {
AND: [args.where, accessibleBy(ability).PipelineProgress],
}
: accessibleBy(ability).PipelineProgress,
orderBy: args.orderBy,
cursor: args.cursor,
take: args.take,
skip: args.skip,
distinct: args.distinct,
select: prismaSelect.value,
});
}
@Mutation(() => PipelineProgress, {
nullable: true,
})
@UseGuards(AbilityGuard)
@CheckAbilities(UpdatePipelineProgressAbilityHandler)
async updateOnePipelineProgress(
@Args() args: UpdateOnePipelineProgressArgs,
@PrismaSelector({ modelName: 'PipelineProgress' })
prismaSelect: PrismaSelect<'PipelineProgress'>,
): Promise<Partial<PipelineProgress> | null> {
// TODO: Do a proper check with recursion testing on args in a more generic place
for (const key in args.data) {
if (args.data[key]) {
for (const subKey in args.data[key]) {
if (JSON.stringify(args.data[key][subKey]) === '{}') {
delete args.data[key][subKey];
}
}
}
if (JSON.stringify(args.data[key]) === '{}') {
delete args.data[key];
}
}
return this.pipelineProgressService.update({
where: args.where,
data: args.data,
select: prismaSelect.value,
} as Prisma.PipelineProgressUpdateArgs);
}
@Mutation(() => AffectedRows, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(DeletePipelineProgressAbilityHandler)
async deleteManyPipelineProgress(
@Args() args: DeleteManyPipelineProgressArgs,
): Promise<AffectedRows> {
return this.pipelineProgressService.deleteMany({
where: args.where,
});
}
@Mutation(() => PipelineProgress, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(CreatePipelineProgressAbilityHandler)
async createOnePipelineProgress(
@Args() args: CreateOnePipelineProgressArgs,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'PipelineProgress' })
prismaSelect: PrismaSelect<'PipelineProgress'>,
): Promise<Partial<PipelineProgress>> {
return this.pipelineProgressService.create({
data: {
...args.data,
...{ workspace: { connect: { id: workspace.id } } },
},
select: prismaSelect.value,
} as Prisma.PipelineProgressCreateArgs);
}
}

View File

@ -1,32 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PipelineStageService } from 'src/core/pipeline/services/pipeline-stage.service';
import { AbilityFactory } from 'src/ability/ability.factory';
import { PipelineStageResolver } from './pipeline-stage.resolver';
describe('PipelineStageResolver', () => {
let resolver: PipelineStageResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PipelineStageResolver,
{
provide: PipelineStageService,
useValue: {},
},
{
provide: AbilityFactory,
useValue: {},
},
],
}).compile();
resolver = module.get<PipelineStageResolver>(PipelineStageResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -1,149 +0,0 @@
import { Resolver, Args, Query, Mutation } from '@nestjs/graphql';
import {
ForbiddenException,
NotFoundException,
UseGuards,
} from '@nestjs/common';
import { accessibleBy } from '@casl/prisma';
import { Prisma, Workspace } from '@prisma/client';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { PipelineStage } from 'src/core/@generated/pipeline-stage/pipeline-stage.model';
import { FindManyPipelineStageArgs } from 'src/core/@generated/pipeline-stage/find-many-pipeline-stage.args';
import { PipelineStageService } from 'src/core/pipeline/services/pipeline-stage.service';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
CreatePipelineStageAbilityHandler,
DeletePipelineStageAbilityHandler,
ReadPipelineStageAbilityHandler,
UpdatePipelineStageAbilityHandler,
} from 'src/ability/handlers/pipeline-stage.ability-handler';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
import {
PrismaSelector,
PrismaSelect,
} from 'src/decorators/prisma-select.decorator';
import { UpdateOnePipelineStageArgs } from 'src/core/@generated/pipeline-stage/update-one-pipeline-stage.args';
import { CreateOnePipelineStageArgs } from 'src/core/@generated/pipeline-stage/create-one-pipeline-stage.args';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { DeleteOnePipelineStageArgs } from 'src/core/@generated/pipeline-stage/delete-one-pipeline-stage.args';
@UseGuards(JwtAuthGuard)
@Resolver(() => PipelineStage)
export class PipelineStageResolver {
constructor(private readonly pipelineStageService: PipelineStageService) {}
@Mutation(() => PipelineStage, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(CreatePipelineStageAbilityHandler)
async createOnePipelineStage(
@Args() args: CreateOnePipelineStageArgs,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'PipelineStage' })
prismaSelect: PrismaSelect<'PipelineStage'>,
): Promise<Partial<PipelineStage>> {
return this.pipelineStageService.create({
data: {
...args.data,
workspace: { connect: { id: workspace.id } },
},
select: prismaSelect.value,
} as Prisma.PipelineStageCreateArgs);
}
@Query(() => [PipelineStage])
@UseGuards(AbilityGuard)
@CheckAbilities(ReadPipelineStageAbilityHandler)
async findManyPipelineStage(
@Args() args: FindManyPipelineStageArgs,
@UserAbility() ability: AppAbility,
@PrismaSelector({ modelName: 'PipelineStage' })
prismaSelect: PrismaSelect<'PipelineStage'>,
): Promise<Partial<PipelineStage>[]> {
return this.pipelineStageService.findMany({
where: args.where
? {
AND: [args.where, accessibleBy(ability).PipelineStage],
}
: accessibleBy(ability).PipelineStage,
orderBy: args.orderBy,
cursor: args.cursor,
take: args.take,
skip: args.skip,
distinct: args.distinct,
select: prismaSelect.value,
});
}
@Mutation(() => PipelineStage, {
nullable: true,
})
@UseGuards(AbilityGuard)
@CheckAbilities(UpdatePipelineStageAbilityHandler)
async updateOnePipelineStage(
@Args() args: UpdateOnePipelineStageArgs,
@PrismaSelector({ modelName: 'PipelineProgress' })
prismaSelect: PrismaSelect<'PipelineProgress'>,
): Promise<Partial<PipelineStage> | null> {
return this.pipelineStageService.update({
where: args.where,
data: args.data,
select: prismaSelect.value,
} as Prisma.PipelineProgressUpdateArgs);
}
@Mutation(() => PipelineStage, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(DeletePipelineStageAbilityHandler)
async deleteOnePipelineStage(
@Args() args: DeleteOnePipelineStageArgs,
): Promise<PipelineStage> {
const pipelineStageToDelete = await this.pipelineStageService.findUnique({
where: args.where,
});
if (!pipelineStageToDelete) {
throw new NotFoundException();
}
const { pipelineId } = pipelineStageToDelete;
const remainingPipelineStages = await this.pipelineStageService.findMany({
orderBy: { position: 'asc' },
where: {
pipelineId,
NOT: { id: pipelineStageToDelete.id },
},
});
if (!remainingPipelineStages.length) {
throw new ForbiddenException(
`Deleting last pipeline stage is not allowed`,
);
}
const deletedPipelineStage = await this.pipelineStageService.delete({
where: args.where,
});
await Promise.all(
remainingPipelineStages.map((pipelineStage, index) => {
if (pipelineStage.position === index) return;
return this.pipelineStageService.update({
data: { position: index },
where: { id: pipelineStage.id },
});
}),
);
return deletedPipelineStage;
}
}

View File

@ -1,32 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PipelineService } from 'src/core/pipeline/services/pipeline.service';
import { AbilityFactory } from 'src/ability/ability.factory';
import { PipelineResolver } from './pipeline.resolver';
describe('PipelineResolver', () => {
let resolver: PipelineResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PipelineResolver,
{
provide: PipelineService,
useValue: {},
},
{
provide: AbilityFactory,
useValue: {},
},
],
}).compile();
resolver = module.get<PipelineResolver>(PipelineResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -1,44 +0,0 @@
import { Resolver, Args, Query } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { accessibleBy } from '@casl/prisma';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { Pipeline } from 'src/core/@generated/pipeline/pipeline.model';
import { FindManyPipelineArgs } from 'src/core/@generated/pipeline/find-many-pipeline.args';
import { PipelineService } from 'src/core/pipeline/services/pipeline.service';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import { ReadPipelineAbilityHandler } from 'src/ability/handlers/pipeline.ability-handler';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
import {
PrismaSelector,
PrismaSelect,
} from 'src/decorators/prisma-select.decorator';
@UseGuards(JwtAuthGuard)
@Resolver(() => Pipeline)
export class PipelineResolver {
constructor(private readonly pipelineService: PipelineService) {}
@Query(() => [Pipeline])
@UseGuards(AbilityGuard)
@CheckAbilities(ReadPipelineAbilityHandler)
async findManyPipeline(
@Args() args: FindManyPipelineArgs,
@UserAbility() ability: AppAbility,
@PrismaSelector({ modelName: 'Pipeline' })
prismaSelect: PrismaSelect<'Pipeline'>,
): Promise<Partial<Pipeline>[]> {
return this.pipelineService.findMany({
...args,
where: args.where
? {
AND: [args.where, accessibleBy(ability).Pipeline],
}
: accessibleBy(ability).Pipeline,
select: prismaSelect.value,
});
}
}

View File

@ -1,33 +0,0 @@
[
{
"name": "New",
"color": "red",
"position": 0,
"type": "open"
},
{
"name": "Screening",
"color": "purple",
"position": 1,
"type": "ongoing"
},
{
"name": "Meeting",
"color": "sky",
"position": 2,
"type": "ongoing"
},
{
"name": "Proposal",
"color": "turquoise",
"position": 3,
"type": "ongoing"
},
{
"name": "Customer",
"color": "yellow",
"position": 4,
"type": "won"
}
]

View File

@ -1,6 +0,0 @@
{
"name": "Sales pipeline",
"icon": "💰",
"pipelineProgressableType": "Company"
}

View File

@ -1,28 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { PipelineProgressService } from './pipeline-progress.service';
describe('PipelineProgressService', () => {
let service: PipelineProgressService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PipelineProgressService,
{
provide: PrismaService,
useValue: prismaMock,
},
],
}).compile();
service = module.get<PipelineProgressService>(PipelineProgressService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,41 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class PipelineProgressService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.pipelineProgress.findFirst;
findFirstOrThrow =
this.prismaService.client.pipelineProgress.findFirstOrThrow;
findUnique = this.prismaService.client.pipelineProgress.findUnique;
findUniqueOrThrow =
this.prismaService.client.pipelineProgress.findUniqueOrThrow;
findMany = this.prismaService.client.pipelineProgress.findMany;
// Create
create = this.prismaService.client.pipelineProgress.create;
createMany = this.prismaService.client.pipelineProgress.createMany;
// Update
update = this.prismaService.client.pipelineProgress.update;
upsert = this.prismaService.client.pipelineProgress.upsert;
updateMany = this.prismaService.client.pipelineProgress.updateMany;
// Delete
delete = this.prismaService.client.pipelineProgress.delete;
deleteMany = this.prismaService.client.pipelineProgress.deleteMany;
// Aggregate
aggregate = this.prismaService.client.pipelineProgress.aggregate;
// Count
count = this.prismaService.client.pipelineProgress.count;
// GroupBy
groupBy = this.prismaService.client.pipelineProgress.groupBy;
}

View File

@ -1,28 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { PipelineStageService } from './pipeline-stage.service';
describe('PipelineStageService', () => {
let service: PipelineStageService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PipelineStageService,
{
provide: PrismaService,
useValue: prismaMock,
},
],
}).compile();
service = module.get<PipelineStageService>(PipelineStageService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,58 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/database/prisma.service';
import seedPipelineStages from 'src/core/pipeline/seed-data/pipeline-stages.json';
@Injectable()
export class PipelineStageService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.pipelineStage.findFirst;
findFirstOrThrow = this.prismaService.client.pipelineStage.findFirstOrThrow;
findUnique = this.prismaService.client.pipelineStage.findUnique;
findUniqueOrThrow = this.prismaService.client.pipelineStage.findUniqueOrThrow;
findMany = this.prismaService.client.pipelineStage.findMany;
// Create
create = this.prismaService.client.pipelineStage.create;
createMany = this.prismaService.client.pipelineStage.createMany;
// Update
update = this.prismaService.client.pipelineStage.update;
upsert = this.prismaService.client.pipelineStage.upsert;
updateMany = this.prismaService.client.pipelineStage.updateMany;
// Delete
delete = this.prismaService.client.pipelineStage.delete;
deleteMany = this.prismaService.client.pipelineStage.deleteMany;
// Aggregate
aggregate = this.prismaService.client.pipelineStage.aggregate;
// Count
count = this.prismaService.client.pipelineStage.count;
// GroupBy
groupBy = this.prismaService.client.pipelineStage.groupBy;
// Customs
async createDefaultPipelineStages({
workspaceId,
pipelineId,
}: {
workspaceId: string;
pipelineId: string;
}) {
const pipelineStages = seedPipelineStages.map((pipelineStage) => ({
...pipelineStage,
workspaceId,
pipelineId,
}));
return this.createMany({
data: pipelineStages,
});
}
}

View File

@ -1,28 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { PipelineService } from './pipeline.service';
describe('PipelineService', () => {
let service: PipelineService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PipelineService,
{
provide: PrismaService,
useValue: prismaMock,
},
],
}).compile();
service = module.get<PipelineService>(PipelineService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,56 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PipelineProgressableType } from '@prisma/client';
import { PrismaService } from 'src/database/prisma.service';
import seedSalesPipeline from 'src/core/pipeline/seed-data/sales-pipeline.json';
@Injectable()
export class PipelineService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.pipeline.findFirst;
findFirstOrThrow = this.prismaService.client.pipeline.findFirstOrThrow;
findUnique = this.prismaService.client.pipeline.findUnique;
findUniqueOrThrow = this.prismaService.client.pipeline.findUniqueOrThrow;
findMany = this.prismaService.client.pipeline.findMany;
// Create
create = this.prismaService.client.pipeline.create;
createMany = this.prismaService.client.pipeline.createMany;
// Update
update = this.prismaService.client.pipeline.update;
upsert = this.prismaService.client.pipeline.upsert;
updateMany = this.prismaService.client.pipeline.updateMany;
// Delete
delete = this.prismaService.client.pipeline.delete;
deleteMany = this.prismaService.client.pipeline.deleteMany;
// Aggregate
aggregate = this.prismaService.client.pipeline.aggregate;
// Count
count = this.prismaService.client.pipeline.count;
// GroupBy
groupBy = this.prismaService.client.pipeline.groupBy;
// Customs
async createDefaultPipeline({ workspaceId }: { workspaceId: string }) {
const pipeline = {
...seedSalesPipeline,
pipelineProgressableType:
seedSalesPipeline.pipelineProgressableType as PipelineProgressableType,
workspaceId,
};
return this.create({
data: pipeline,
});
}
}

View File

@ -0,0 +1,11 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsDate, IsNotEmpty } from 'class-validator';
@InputType()
export class CreateRefreshTokenInput {
@IsDate()
@IsNotEmpty()
@Field()
expiresAt: Date;
}

View File

@ -0,0 +1,24 @@
import {
BeforeCreateOneHook,
CreateOneInputType,
} from '@ptc-org/nestjs-query-graphql';
import { v4 as uuidv4 } from 'uuid';
import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity';
export class BeforeCreateOneRefreshToken<T extends RefreshToken>
implements BeforeCreateOneHook<T, any>
{
async run(
instance: CreateOneInputType<T>,
context: any,
): Promise<CreateOneInputType<T>> {
const userId = context?.req?.user?.user?.id;
instance.input.userId = userId;
// FIXME: These fields should be autogenerated, we need to run a migration for this
instance.input.id = uuidv4();
instance.input.updatedAt = new Date();
return instance;
}
}

View File

@ -0,0 +1,41 @@
import {
AutoResolverOpts,
PagingStrategies,
ReadResolverOpts,
} from '@ptc-org/nestjs-query-graphql';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { RefreshToken } from './refresh-token.entity';
import { CreateRefreshTokenInput } from './dtos/create-refresh-token.input';
export const refreshTokenAutoResolverOpts: AutoResolverOpts<
any,
any,
unknown,
unknown,
ReadResolverOpts<any>,
PagingStrategies
>[] = [
{
EntityClass: RefreshToken,
DTOClass: RefreshToken,
CreateDTOClass: CreateRefreshTokenInput,
enableTotalCount: true,
pagingStrategy: PagingStrategies.CURSOR,
read: {
many: { disabled: true },
one: { disabled: true },
},
create: {
many: { disabled: true },
},
update: {
many: { disabled: true },
one: { disabled: true },
},
delete: { many: { disabled: true }, one: { disabled: true } },
guards: [JwtAuthGuard],
},
];

View File

@ -0,0 +1,50 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';
import {
Entity,
Column,
PrimaryGeneratedColumn,
ManyToOne,
JoinColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { BeforeCreateOne, IDField } from '@ptc-org/nestjs-query-graphql';
import { User } from 'src/core/user/user.entity';
import { BeforeCreateOneRefreshToken } from './hooks/before-create-one-refresh-token.hook';
@Entity({ name: 'refreshToken', schema: 'core' })
@ObjectType('RefreshToken')
@BeforeCreateOne(BeforeCreateOneRefreshToken)
export class RefreshToken {
@IDField(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => User, (user) => user.refreshTokens)
@JoinColumn({ name: 'userId' })
user: User;
@Column()
userId: string;
@Field()
@Column('timestamp with time zone')
expiresAt: Date;
@Column('timestamp with time zone', { nullable: true })
deletedAt: Date | null;
@Column('timestamp with time zone', { nullable: true })
revokedAt: Date | null;
@Field()
@CreateDateColumn({ type: 'timestamp with time zone' })
createdAt: Date;
@Field()
@UpdateDateColumn({ type: 'timestamp with time zone' })
updatedAt: Date;
}

View File

@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
// eslint-disable-next-line no-restricted-imports
import config from '../../../ormconfig';
import { RefreshToken } from './refresh-token.entity';
import { refreshTokenAutoResolverOpts } from './refresh-token.auto-resolver-opts';
import { RefreshTokenService } from './services/refresh-token.service';
@Module({
imports: [
TypeOrmModule.forRoot(config),
NestjsQueryGraphQLModule.forFeature({
imports: [NestjsQueryTypeOrmModule.forFeature([RefreshToken])],
services: [RefreshTokenService],
resolvers: refreshTokenAutoResolverOpts,
}),
],
})
export class RefreshTokenModule {}

View File

@ -0,0 +1,28 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity';
import { RefreshTokenService } from './refresh-token.service';
describe('RefreshTokenService', () => {
let service: RefreshTokenService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
RefreshTokenService,
{
provide: getRepositoryToken(RefreshToken),
useValue: {},
},
],
}).compile();
service = module.get<RefreshTokenService>(RefreshTokenService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,5 @@
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity';
export class RefreshTokenService extends TypeOrmQueryService<RefreshToken> {}

View File

@ -0,0 +1,33 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
@ObjectType('UserWorkspaceMemberName')
export class UserWorkspaceMemberName {
@Field({ nullable: false })
firstName: string;
@Field({ nullable: false })
lastName: string;
}
@ObjectType('UserWorkspaceMember')
export class UserWorkspaceMember {
@IDField(() => ID)
id: string;
@Field(() => UserWorkspaceMemberName)
name: UserWorkspaceMemberName;
@Field({ nullable: false })
colorScheme: string;
@Field({ nullable: true })
avatarUrl: string;
@Field({ nullable: false })
locale: string;
@Field({ nullable: false })
allowImpersonation: boolean;
}

View File

@ -1,8 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { WorkspaceService } from 'src/core/workspace/services/workspace.service';
import { User } from 'src/core/user/user.entity';
import { UserService } from './user.service';
@ -14,11 +13,7 @@ describe('UserService', () => {
providers: [
UserService,
{
provide: PrismaService,
useValue: prismaMock,
},
{
provide: WorkspaceService,
provide: getRepositoryToken(User),
useValue: {},
},
],

View File

@ -0,0 +1,85 @@
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm';
import { assert } from 'src/utils/assert';
import { User } from 'src/core/user/user.entity';
import { UserWorkspaceMember } from 'src/core/user/dtos/workspace-member.dto';
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
export class UserService extends TypeOrmQueryService<User> {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
private readonly dataSourceService: DataSourceService,
private readonly typeORMService: TypeORMService,
) {
super(userRepository);
}
async loadWorkspaceMember(user: User) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
user.defaultWorkspace.id,
);
const workspaceDataSource = await this.typeORMService.connectToDataSource(
dataSourceMetadata,
);
const workspaceMembers = await workspaceDataSource?.query(
`SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${user.id}'`,
);
assert(workspaceMembers.length === 1, 'WorkspaceMember not found');
const userWorkspaceMember = new UserWorkspaceMember();
userWorkspaceMember.id = workspaceMembers[0].id;
userWorkspaceMember.colorScheme = workspaceMembers[0].colorScheme;
userWorkspaceMember.locale = workspaceMembers[0].locale;
userWorkspaceMember.allowImpersonation =
workspaceMembers[0].allowImpersonation;
userWorkspaceMember.avatarUrl = workspaceMembers[0].avatarUrl;
userWorkspaceMember.name = {
firstName: workspaceMembers[0].nameFirstName,
lastName: workspaceMembers[0].nameLastName,
};
return userWorkspaceMember;
}
async createWorkspaceMember(user: User, avatarUrl?: string) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
user.defaultWorkspace.id,
);
const workspaceDataSource = await this.typeORMService.connectToDataSource(
dataSourceMetadata,
);
await workspaceDataSource?.query(
`INSERT INTO ${dataSourceMetadata.schema}."workspaceMember"
("nameFirstName", "nameLastName", "colorScheme", "userId", "allowImpersonation", "avatarUrl")
VALUES ('${user.firstName}', '${user.lastName}', 'Light', '${
user.id
}', true, '${avatarUrl ?? ''}')`,
);
}
async deleteUser({
workspaceId: _workspaceId,
userId,
}: {
workspaceId: string;
userId: string;
}) {
const user = await this.userRepository.findBy({ id: userId });
assert(user, 'User not found');
return user;
}
}

View File

@ -0,0 +1,38 @@
import {
AutoResolverOpts,
ReadResolverOpts,
PagingStrategies,
} from '@ptc-org/nestjs-query-graphql';
import { User } from 'src/core/user/user.entity';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
export const userAutoResolverOpts: AutoResolverOpts<
any,
any,
unknown,
unknown,
ReadResolverOpts<any>,
PagingStrategies
>[] = [
{
EntityClass: User,
DTOClass: User,
enableTotalCount: true,
pagingStrategy: PagingStrategies.CURSOR,
read: {
many: { disabled: true },
one: { disabled: true },
},
create: {
many: { disabled: true },
one: { disabled: true },
},
update: {
many: { disabled: true },
one: { disabled: true },
},
delete: { many: { disabled: true }, one: { disabled: true } },
guards: [JwtAuthGuard],
},
];

View File

@ -0,0 +1,74 @@
import { ID, Field, ObjectType } from '@nestjs/graphql';
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
ManyToOne,
} from 'typeorm';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { UserWorkspaceMember } from 'src/core/user/dtos/workspace-member.dto';
@Entity({ name: 'user', schema: 'core' })
@ObjectType('User')
export class User {
@IDField(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;
@Field()
@Column({ nullable: true })
firstName: string;
@Field()
@Column({ nullable: true })
lastName: string;
@Field()
@Column()
email: string;
@Field()
@Column({ default: false })
emailVerified: boolean;
@Field({ nullable: true })
@Column({ default: false })
disabled: boolean;
@Field({ nullable: true })
@Column({ nullable: true })
passwordHash: string;
@Field()
@Column({ default: false })
canImpersonate: boolean;
@Field()
@CreateDateColumn({ type: 'timestamp with time zone' })
createdAt: Date;
@Field()
@UpdateDateColumn({ type: 'timestamp with time zone' })
updatedAt: Date;
@Field({ nullable: true })
@Column({ nullable: true })
deletedAt: Date;
@Field(() => Workspace, { nullable: false })
@ManyToOne(() => Workspace, (workspace) => workspace.users)
defaultWorkspace: Workspace;
@OneToMany(() => RefreshToken, (refreshToken) => refreshToken.user)
refreshTokens: RefreshToken[];
@Field(() => UserWorkspaceMember, { nullable: false })
workspaceMember: UserWorkspaceMember;
}

View File

@ -1,23 +1,34 @@
/* eslint-disable no-restricted-imports */
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { FileModule } from 'src/core/file/file.module';
import { WorkspaceModule } from 'src/core/workspace/workspace.module';
import { EnvironmentModule } from 'src/integrations/environment/environment.module';
import { AbilityModule } from 'src/ability/ability.module';
import { PrismaModule } from 'src/database/prisma.module';
import { User } from 'src/core/user/user.entity';
import { UserResolver } from 'src/core/user/user.resolver';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { UserService } from './user.service';
import { UserResolver } from './user.resolver';
import config from '../../../ormconfig';
import { userAutoResolverOpts } from './user.auto-resolver-opts';
import { UserService } from './services/user.service';
@Module({
imports: [
TypeOrmModule.forRoot(config),
NestjsQueryGraphQLModule.forFeature({
imports: [NestjsQueryTypeOrmModule.forFeature([User]), TypeORMModule],
resolvers: userAutoResolverOpts,
}),
DataSourceModule,
FileModule,
WorkspaceModule,
EnvironmentModule,
AbilityModule,
PrismaModule,
],
providers: [UserService, UserResolver],
exports: [UserService],
providers: [UserService, UserResolver, TypeORMService],
})
export class UserModule {}

View File

@ -1,42 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AbilityFactory } from 'src/ability/ability.factory';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { UserResolver } from './user.resolver';
import { UserService } from './user.service';
describe('UserResolver', () => {
let resolver: UserResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserResolver,
{
provide: UserService,
useValue: {},
},
{
provide: AbilityFactory,
useValue: {},
},
{
provide: FileUploadService,
useValue: {},
},
{
provide: EnvironmentService,
useValue: {},
},
],
}).compile();
resolver = module.get<UserResolver>(UserResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -1,48 +1,32 @@
import {
Args,
Resolver,
Query,
ResolveField,
Args,
Parent,
ResolveField,
Mutation,
} from '@nestjs/graphql';
import { UseFilters, UseGuards } from '@nestjs/common';
import { UseGuards } from '@nestjs/common';
import crypto from 'crypto';
import { accessibleBy } from '@casl/prisma';
import { Prisma, Workspace } from '@prisma/client';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
import { SupportDriver } from 'src/integrations/environment/interfaces/support.interface';
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
import { FindManyUserArgs } from 'src/core/@generated/user/find-many-user.args';
import { User } from 'src/core/@generated/user/user.model';
import { ExceptionFilter } from 'src/filters/exception.filter';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import {
PrismaSelect,
PrismaSelector,
} from 'src/decorators/prisma-select.decorator';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
DeleteUserAbilityHandler,
ReadUserAbilityHandler,
UpdateUserAbilityHandler,
} from 'src/ability/handlers/user.ability-handler';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
import { AuthUser } from 'src/decorators/auth-user.decorator';
import { assert } from 'src/utils/assert';
import { UpdateOneUserArgs } from 'src/core/@generated/user/update-one-user.args';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { assert } from 'src/utils/assert';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { User } from 'src/core/user/user.entity';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { UserWorkspaceMember } from 'src/core/user/dtos/workspace-member.dto';
import { UserService } from './user.service';
import { UserService } from './services/user.service';
const getHMACKey = (email?: string, key?: string | null) => {
if (!email || !key) return null;
@ -56,85 +40,24 @@ const getHMACKey = (email?: string, key?: string | null) => {
export class UserResolver {
constructor(
private readonly userService: UserService,
private readonly environmentService: EnvironmentService,
private readonly fileUploadService: FileUploadService,
private environmentService: EnvironmentService,
) {}
@Query(() => User)
async currentUser(
@AuthUser() { id }: User,
@PrismaSelector({ modelName: 'User' })
prismaSelect: PrismaSelect<'User'>,
) {
const select = prismaSelect.value;
const user = await this.userService.findUnique({
where: {
id,
},
select,
async currentUser(@AuthUser() { id }: User) {
const user = await this.userService.findById(id, {
relations: [{ name: 'defaultWorkspace', query: {} }],
});
assert(user, 'User not found');
return user;
}
@UseFilters(ExceptionFilter)
@Query(() => [User], {
@ResolveField(() => UserWorkspaceMember, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(ReadUserAbilityHandler)
async findManyUser(
@Args() args: FindManyUserArgs,
@UserAbility() ability: AppAbility,
@PrismaSelector({ modelName: 'User' })
prismaSelect: PrismaSelect<'User'>,
): Promise<Partial<User>[]> {
return await this.userService.findMany({
where: args.where
? {
AND: [args.where, accessibleBy(ability).User],
}
: accessibleBy(ability).User,
orderBy: args.orderBy,
cursor: args.cursor,
take: args.take,
skip: args.skip,
distinct: args.distinct,
select: prismaSelect.value,
});
}
@Mutation(() => User)
@UseGuards(AbilityGuard)
@CheckAbilities(UpdateUserAbilityHandler)
async updateUser(
@Args() args: UpdateOneUserArgs,
@AuthUser() { id }: User,
@PrismaSelector({ modelName: 'User' })
prismaSelect: PrismaSelect<'User'>,
) {
const user = await this.userService.findUnique({
where: {
id,
},
select: prismaSelect.value,
});
assert(user, 'User not found');
return this.userService.update({
where: args.where,
data: args.data,
select: prismaSelect.value,
} as Prisma.UserUpdateArgs);
}
@ResolveField(() => String, {
nullable: false,
})
displayName(@Parent() parent: User): string {
return `${parent.firstName ?? ''} ${parent.lastName ?? ''}`;
async workspaceMember(@Parent() user: User): Promise<UserWorkspaceMember> {
return this.userService.loadWorkspaceMember(user);
}
@ResolveField(() => String, {
@ -154,6 +77,10 @@ export class UserResolver {
@Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename, mimetype }: FileUpload,
): Promise<string> {
if (!id) {
throw new Error('User not found');
}
const stream = createReadStream();
const buffer = await streamToBuffer(stream);
const fileFolder = FileFolder.ProfilePicture;
@ -165,20 +92,11 @@ export class UserResolver {
fileFolder,
});
await this.userService.update({
where: { id },
data: {
avatarUrl: paths[0],
},
});
return paths[0];
}
@Mutation(() => User)
@UseGuards(AbilityGuard)
@CheckAbilities(DeleteUserAbilityHandler)
async deleteUserAccount(
async deleteUser(
@AuthUser() { id: userId }: User,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {

View File

@ -1,140 +0,0 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from 'src/database/prisma.service';
import { assert } from 'src/utils/assert';
import { WorkspaceService } from 'src/core/workspace/services/workspace.service';
export type UserPayload = {
displayName: string | undefined | null;
email: string;
};
@Injectable()
export class UserService {
constructor(
private readonly prismaService: PrismaService,
private readonly workspaceService: WorkspaceService,
) {}
// Find
findFirst = this.prismaService.client.user.findFirst;
findFirstOrThrow = this.prismaService.client.user.findFirstOrThrow;
findUnique = this.prismaService.client.user.findUnique;
findUniqueOrThrow = this.prismaService.client.user.findUniqueOrThrow;
findMany = this.prismaService.client.user.findMany;
// Create
create = this.prismaService.client.user.create;
createMany = this.prismaService.client.user.createMany;
// Update
update = this.prismaService.client.user.update;
upsert = this.prismaService.client.user.upsert;
updateMany = this.prismaService.client.user.updateMany;
// Delete
delete = this.prismaService.client.user.delete;
deleteMany = this.prismaService.client.user.deleteMany;
// Aggregate
aggregate = this.prismaService.client.user.aggregate;
// Count
count = this.prismaService.client.user.count;
// GroupBy
groupBy = this.prismaService.client.user.groupBy;
// Customs
async createUser<T extends Prisma.UserCreateArgs>(
args: Prisma.SelectSubset<T, Prisma.UserCreateArgs>,
workspaceId?: string,
): Promise<Prisma.UserGetPayload<T>> {
assert(args.data.email, 'email is missing', BadRequestException);
// Create workspace if not exists
const workspace = workspaceId
? await this.workspaceService.findUnique({
where: {
id: workspaceId,
},
})
: await this.workspaceService.createDefaultWorkspace();
assert(workspace, 'workspace is missing', BadRequestException);
// Create user
const user = await this.prismaService.client.user.upsert({
where: {
email: args.data.email,
},
create: {
...(args.data as Prisma.UserCreateInput),
defaultWorkspaceId: workspace.id,
},
update: {},
...(args.select ? { select: args.select } : {}),
...(args.include ? { include: args.include } : {}),
} as Prisma.UserUpsertArgs);
return user as Prisma.UserGetPayload<T>;
}
async deleteUser({
workspaceId,
userId,
}: {
workspaceId: string;
userId: string;
}) {
const { workspaceMember, refreshToken } = this.prismaService.client;
const user = await this.findUnique({
where: {
id: userId,
},
select: {
id: true,
},
});
assert(user, 'User not found');
const workspace = await this.workspaceService.findUnique({
where: { id: workspaceId },
select: { id: true },
});
assert(workspace, 'Workspace not found');
const workSpaceMembers = await workspaceMember.findMany({
where: {
workspaceId,
},
});
const isLastMember =
workSpaceMembers.length === 1 && workSpaceMembers[0].userId === userId;
if (isLastMember) {
// Delete entire workspace
await this.workspaceService.deleteWorkspace({
workspaceId,
});
} else {
await this.prismaService.client.$transaction([
workspaceMember.deleteMany({
where: { userId },
}),
refreshToken.deleteMany({
where: { userId },
}),
this.delete({ where: { id: userId } }),
]);
}
return user;
}
}

View File

@ -1,11 +0,0 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from 'src/database/prisma.module';
import { AbilityModule } from 'src/ability/ability.module';
import { WebHookResolver } from 'src/core/web-hook/web-hook.resolver';
@Module({
imports: [PrismaModule, AbilityModule],
providers: [WebHookResolver],
})
export class WebHookModule {}

View File

@ -1,72 +0,0 @@
import { NotFoundException, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { accessibleBy } from '@casl/prisma';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
CreateWebHookAbilityHandler,
DeleteWebHookAbilityHandler,
ReadWebHookAbilityHandler,
} from 'src/ability/handlers/web-hook.ability-handler';
import { PrismaService } from 'src/database/prisma.service';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
import { CreateOneWebHookArgs } from 'src/core/@generated/web-hook/create-one-web-hook.args';
import { DeleteOneWebHookArgs } from 'src/core/@generated/web-hook/delete-one-web-hook.args';
import { FindManyWebHookArgs } from 'src/core/@generated/web-hook/find-many-web-hook.args';
import { WebHook } from 'src/core/@generated/web-hook/web-hook.model';
@UseGuards(JwtAuthGuard)
@Resolver(() => WebHook)
export class WebHookResolver {
constructor(private readonly prismaService: PrismaService) {}
@Mutation(() => WebHook)
@UseGuards(AbilityGuard)
@CheckAbilities(CreateWebHookAbilityHandler)
async createOneWebHook(
@Args() args: CreateOneWebHookArgs,
@AuthWorkspace() { id: workspaceId }: Workspace,
): Promise<WebHook> {
return this.prismaService.client.webHook.create({
data: {
...args.data,
...{ workspace: { connect: { id: workspaceId } } },
},
});
}
@Mutation(() => WebHook, { nullable: false })
@UseGuards(AbilityGuard)
@CheckAbilities(DeleteWebHookAbilityHandler)
async deleteOneWebHook(@Args() args: DeleteOneWebHookArgs): Promise<WebHook> {
const hookToDelete = this.prismaService.client.webHook.findUnique({
where: args.where,
});
if (!hookToDelete) {
throw new NotFoundException();
}
return await this.prismaService.client.webHook.delete({
where: args.where,
});
}
@Query(() => [WebHook])
@UseGuards(AbilityGuard)
@CheckAbilities(ReadWebHookAbilityHandler)
async findManyWebHook(
@Args() args: FindManyWebHookArgs,
@UserAbility() ability: AppAbility,
) {
const filterOptions = [accessibleBy(ability).WorkspaceMember];
if (args.where) filterOptions.push(args.where);
return this.prismaService.client.webHook.findMany({
...args,
where: { AND: filterOptions },
});
}
}

View File

@ -0,0 +1,26 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsOptional, IsString } from 'class-validator';
@InputType()
export class UpdateWorkspaceInput {
@Field({ nullable: true })
@IsString()
@IsOptional()
domainName?: string;
@Field({ nullable: true })
@IsString()
@IsOptional()
displayName?: string;
@Field({ nullable: true })
@IsString()
@IsOptional()
logo?: string;
@Field({ nullable: true })
@IsString()
@IsOptional()
inviteHash?: string;
}

View File

@ -1,32 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { WorkspaceMemberService } from 'src/core/workspace/services/workspace-member.service';
import { AbilityFactory } from 'src/ability/ability.factory';
import { WorkspaceMemberResolver } from './workspace-member.resolver';
describe('WorkspaceMemberResolver', () => {
let resolver: WorkspaceMemberResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
WorkspaceMemberResolver,
{
provide: WorkspaceMemberService,
useValue: {},
},
{
provide: AbilityFactory,
useValue: {},
},
],
}).compile();
resolver = module.get<WorkspaceMemberResolver>(WorkspaceMemberResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -1,102 +0,0 @@
import { Args, Query, Resolver, Mutation } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { accessibleBy } from '@casl/prisma';
import { Prisma } from '@prisma/client';
import { WorkspaceMember } from 'src/core/@generated/workspace-member/workspace-member.model';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
DeleteWorkspaceMemberAbilityHandler,
ReadWorkspaceMemberAbilityHandler,
UpdateWorkspaceMemberAbilityHandler,
} from 'src/ability/handlers/workspace-member.ability-handler';
import { FindManyWorkspaceMemberArgs } from 'src/core/@generated/workspace-member/find-many-workspace-member.args';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
import {
PrismaSelect,
PrismaSelector,
} from 'src/decorators/prisma-select.decorator';
import { WorkspaceMemberService } from 'src/core/workspace/services/workspace-member.service';
import { DeleteOneWorkspaceMemberArgs } from 'src/core/@generated/workspace-member/delete-one-workspace-member.args';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { AuthUser } from 'src/decorators/auth-user.decorator';
import { User } from 'src/core/@generated/user/user.model';
import { UpdateOneWorkspaceMemberArgs } from 'src/core/@generated/workspace-member/update-one-workspace-member.args';
@UseGuards(JwtAuthGuard)
@Resolver(() => WorkspaceMember)
export class WorkspaceMemberResolver {
constructor(
private readonly workspaceMemberService: WorkspaceMemberService,
) {}
@Query(() => [WorkspaceMember])
@UseGuards(AbilityGuard)
@CheckAbilities(ReadWorkspaceMemberAbilityHandler)
async findManyWorkspaceMember(
@Args() args: FindManyWorkspaceMemberArgs,
@UserAbility() ability: AppAbility,
@PrismaSelector({ modelName: 'WorkspaceMember' })
prismaSelect: PrismaSelect<'WorkspaceMember'>,
): Promise<Partial<WorkspaceMember>[]> {
return this.workspaceMemberService.findMany({
...args,
where: args.where
? {
AND: [args.where, accessibleBy(ability).WorkspaceMember],
}
: accessibleBy(ability).WorkspaceMember,
select: prismaSelect.value,
});
}
@Mutation(() => WorkspaceMember)
async allowImpersonation(
@Args('allowImpersonation') allowImpersonation: boolean,
@AuthUser() user: User,
@PrismaSelector({ modelName: 'WorkspaceMember' })
prismaSelect: PrismaSelect<'WorkspaceMember'>,
): Promise<Partial<WorkspaceMember>> {
return this.workspaceMemberService.update({
where: {
userId: user.id,
},
data: {
allowImpersonation,
},
select: prismaSelect.value,
});
}
@Mutation(() => WorkspaceMember)
@UseGuards(AbilityGuard)
@CheckAbilities(DeleteWorkspaceMemberAbilityHandler)
async deleteWorkspaceMember(
@Args() args: DeleteOneWorkspaceMemberArgs,
@PrismaSelector({ modelName: 'WorkspaceMember' })
prismaSelect: PrismaSelect<'WorkspaceMember'>,
): Promise<Partial<WorkspaceMember>> {
return this.workspaceMemberService.delete({
where: args.where,
select: prismaSelect.value,
});
}
@Mutation(() => WorkspaceMember)
@UseGuards(AbilityGuard)
@CheckAbilities(UpdateWorkspaceMemberAbilityHandler)
async UpdateOneWorkspaceMember(
@Args() args: UpdateOneWorkspaceMemberArgs,
@PrismaSelector({ modelName: 'WorkspaceMember' })
prismaSelect: PrismaSelect<'WorkspaceMember'>,
): Promise<Partial<WorkspaceMember>> {
return this.workspaceMemberService.update({
data: args.data,
where: args.where,
select: prismaSelect.value,
} as Prisma.WorkspaceMemberUpdateArgs);
}
}

View File

@ -1,28 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { WorkspaceService } from 'src/core/workspace/services/workspace.service';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { AbilityFactory } from 'src/ability/ability.factory';
import { WorkspaceResolver } from './workspace.resolver';
describe('WorkspaceResolver', () => {
let resolver: WorkspaceResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
WorkspaceResolver,
{ provide: WorkspaceService, useValue: {} },
{ provide: AbilityFactory, useValue: {} },
{ provide: FileUploadService, useValue: {} },
],
}).compile();
resolver = module.get<WorkspaceResolver>(WorkspaceResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -1,115 +0,0 @@
import { Query, Args, Mutation, Resolver } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { WorkspaceService } from 'src/core/workspace/services/workspace.service';
import {
PrismaSelect,
PrismaSelector,
} from 'src/decorators/prisma-select.decorator';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { WorkspaceUpdateInput } from 'src/core/@generated/workspace/workspace-update.input';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { assert } from 'src/utils/assert';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
UpdateWorkspaceAbilityHandler,
DeleteWorkspaceAbilityHandler,
} from 'src/ability/handlers/workspace.ability-handler';
@UseGuards(JwtAuthGuard)
@Resolver(() => Workspace)
export class WorkspaceResolver {
constructor(
private readonly workspaceService: WorkspaceService,
private readonly fileUploadService: FileUploadService,
) {}
@Mutation(() => Workspace)
@UseGuards(AbilityGuard)
@CheckAbilities(UpdateWorkspaceAbilityHandler)
async updateWorkspace(
@Args('data') data: WorkspaceUpdateInput,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'Workspace' })
prismaSelect: PrismaSelect<'Workspace'>,
) {
return this.workspaceService.update({
where: {
id: workspace.id,
},
data: {
...data,
},
select: prismaSelect.value,
} as Prisma.WorkspaceUpdateArgs);
}
@Query(() => Workspace)
async currentWorkspace(
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'Workspace' })
prismaSelect: PrismaSelect<'Workspace'>,
) {
const selectedWorkspace = await this.workspaceService.findUnique({
where: {
id: workspace.id,
},
select: prismaSelect.value,
});
assert(selectedWorkspace, 'User not found');
return selectedWorkspace;
}
@UseGuards(AbilityGuard)
@CheckAbilities(UpdateWorkspaceAbilityHandler)
@Mutation(() => String)
async uploadWorkspaceLogo(
@AuthWorkspace() workspace: Workspace,
@Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename, mimetype }: FileUpload,
): Promise<string> {
const stream = createReadStream();
const buffer = await streamToBuffer(stream);
const fileFolder = FileFolder.WorkspaceLogo;
const { paths } = await this.fileUploadService.uploadImage({
file: buffer,
filename,
mimeType: mimetype,
fileFolder,
});
await this.workspaceService.update({
where: { id: workspace.id },
data: {
logo: paths[0],
},
});
return paths[0];
}
@UseGuards(AbilityGuard)
@CheckAbilities(DeleteWorkspaceAbilityHandler)
@Mutation(() => Workspace)
async deleteCurrentWorkspace(
@AuthWorkspace() { id: workspaceId }: Workspace,
@PrismaSelector({ modelName: 'Workspace' })
{ value: select }: PrismaSelect<'Workspace'>,
) {
return this.workspaceService.deleteWorkspace({
workspaceId,
select,
});
}
}

View File

@ -1,28 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { WorkspaceMemberService } from './workspace-member.service';
describe('WorkspaceMemberService', () => {
let service: WorkspaceMemberService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
WorkspaceMemberService,
{
provide: PrismaService,
useValue: prismaMock,
},
],
}).compile();
service = module.get<WorkspaceMemberService>(WorkspaceMemberService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,40 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class WorkspaceMemberService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.workspaceMember.findFirst;
findFirstOrThrow = this.prismaService.client.workspaceMember.findFirstOrThrow;
findUnique = this.prismaService.client.workspaceMember.findUnique;
findUniqueOrThrow =
this.prismaService.client.workspaceMember.findUniqueOrThrow;
findMany = this.prismaService.client.workspaceMember.findMany;
// Create
create = this.prismaService.client.workspaceMember.create;
createMany = this.prismaService.client.workspaceMember.createMany;
// Update
update = this.prismaService.client.workspaceMember.update;
upsert = this.prismaService.client.workspaceMember.upsert;
updateMany = this.prismaService.client.workspaceMember.updateMany;
// Delete
delete = this.prismaService.client.workspaceMember.delete;
deleteMany = this.prismaService.client.workspaceMember.deleteMany;
// Aggregate
aggregate = this.prismaService.client.workspaceMember.aggregate;
// Count
count = this.prismaService.client.workspaceMember.count;
// GroupBy
groupBy = this.prismaService.client.workspaceMember.groupBy;
}

View File

@ -1,13 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { PipelineService } from 'src/core/pipeline/services/pipeline.service';
import { PipelineStageService } from 'src/core/pipeline/services/pipeline-stage.service';
import { PersonService } from 'src/core/person/person.service';
import { CompanyService } from 'src/core/company/company.service';
import { PipelineProgressService } from 'src/core/pipeline/services/pipeline-progress.service';
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { WorkspaceService } from './workspace.service';
@ -19,31 +13,7 @@ describe('WorkspaceService', () => {
providers: [
WorkspaceService,
{
provide: PrismaService,
useValue: prismaMock,
},
{
provide: PipelineService,
useValue: {},
},
{
provide: PipelineStageService,
useValue: {},
},
{
provide: PersonService,
useValue: {},
},
{
provide: CompanyService,
useValue: {},
},
{
provide: PipelineProgressService,
useValue: {},
},
{
provide: WorkspaceManagerService,
provide: getRepositoryToken(Workspace),
useValue: {},
},
],

View File

@ -1,167 +1,27 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Prisma } from '@prisma/client';
import { v4 } from 'uuid';
import assert from 'assert';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm';
import { CompanyService } from 'src/core/company/company.service';
import { PersonService } from 'src/core/person/person.service';
import { PipelineProgressService } from 'src/core/pipeline/services/pipeline-progress.service';
import { PipelineStageService } from 'src/core/pipeline/services/pipeline-stage.service';
import { PipelineService } from 'src/core/pipeline/services/pipeline.service';
import { PrismaService } from 'src/database/prisma.service';
import { assert } from 'src/utils/assert';
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
import { Workspace } from 'src/core/workspace/workspace.entity';
@Injectable()
export class WorkspaceService {
export class WorkspaceService extends TypeOrmQueryService<Workspace> {
constructor(
private readonly prismaService: PrismaService,
private readonly pipelineService: PipelineService,
private readonly companyService: CompanyService,
private readonly personService: PersonService,
private readonly pipelineStageService: PipelineStageService,
private readonly pipelineProgressService: PipelineProgressService,
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
private readonly workspaceManagerService: WorkspaceManagerService,
) {}
// Find
findFirst = this.prismaService.client.workspace.findFirst;
findFirstOrThrow = this.prismaService.client.workspace.findFirstOrThrow;
findUnique = this.prismaService.client.workspace.findUnique;
findUniqueOrThrow = this.prismaService.client.workspace.findUniqueOrThrow;
findMany = this.prismaService.client.workspace.findMany;
// Create
create = this.prismaService.client.workspace.create;
createMany = this.prismaService.client.workspace.createMany;
// Update
update = this.prismaService.client.workspace.update;
upsert = this.prismaService.client.workspace.upsert;
updateMany = this.prismaService.client.workspace.updateMany;
// Delete
delete = this.prismaService.client.workspace.delete;
deleteMany = this.prismaService.client.workspace.deleteMany;
// Aggregate
aggregate = this.prismaService.client.workspace.aggregate;
// Count
count = this.prismaService.client.workspace.count;
// GroupBy
groupBy = this.prismaService.client.workspace.groupBy;
// Customs
async createDefaultWorkspace() {
const workspace = await this.create({
data: {
inviteHash: v4(),
},
});
// Create workspace schema
await this.workspaceManagerService.init(workspace.id);
// Create default companies
const companies = await this.companyService.createDefaultCompanies({
workspaceId: workspace.id,
});
// Create default people
await this.personService.createDefaultPeople({
workspaceId: workspace.id,
companies,
});
// Create default pipeline
const pipeline = await this.pipelineService.createDefaultPipeline({
workspaceId: workspace.id,
});
// Create default stages
await this.pipelineStageService.createDefaultPipelineStages({
pipelineId: pipeline.id,
workspaceId: workspace.id,
});
return workspace;
) {
super(workspaceRepository);
}
async deleteWorkspace({
workspaceId,
select = { id: true },
}: {
workspaceId: string;
select?: Prisma.WorkspaceSelect;
}) {
const workspace = await this.findUnique({
where: { id: workspaceId },
select,
});
async deleteWorkspace(id: string) {
const workspace = await this.workspaceRepository.findOneBy({ id });
assert(workspace, 'Workspace not found');
const where = { workspaceId };
const {
workspaceMember,
attachment,
comment,
activityTarget,
activity,
apiKey,
favorite,
webHook,
} = this.prismaService.client;
// We don't delete user or refresh tokens as they can belong to another workspace
await this.prismaService.client.$transaction([
this.pipelineProgressService.deleteMany({
where,
}),
this.companyService.deleteMany({
where,
}),
this.personService.deleteMany({
where,
}),
this.pipelineStageService.deleteMany({
where,
}),
this.pipelineService.deleteMany({
where,
}),
workspaceMember.deleteMany({
where,
}),
attachment.deleteMany({
where,
}),
comment.deleteMany({
where,
}),
activityTarget.deleteMany({
where,
}),
activity.deleteMany({
where,
}),
apiKey.deleteMany({
where,
}),
favorite.deleteMany({
where,
}),
webHook.deleteMany({
where,
}),
this.delete({ where: { id: workspaceId } }),
]);
await this.workspaceManagerService.delete(workspaceId);
await this.workspaceManagerService.delete(id);
return workspace;
}

View File

@ -0,0 +1,41 @@
import {
AutoResolverOpts,
PagingStrategies,
ReadResolverOpts,
} from '@ptc-org/nestjs-query-graphql';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { UpdateWorkspaceInput } from 'src/core/workspace/dtos/update-workspace-input';
import { Workspace } from './workspace.entity';
export const workspaceAutoResolverOpts: AutoResolverOpts<
any,
any,
unknown,
unknown,
ReadResolverOpts<any>,
PagingStrategies
>[] = [
{
EntityClass: Workspace,
DTOClass: Workspace,
UpdateDTOClass: UpdateWorkspaceInput,
enableTotalCount: true,
pagingStrategy: PagingStrategies.CURSOR,
read: {
many: { disabled: true },
one: { disabled: true },
},
create: {
many: { disabled: true },
one: { disabled: true },
},
update: {
one: { disabled: true },
many: { disabled: true },
},
delete: { many: { disabled: true }, one: { disabled: true } },
guards: [JwtAuthGuard],
},
];

View File

@ -0,0 +1,52 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import {
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { User } from 'src/core/user/user.entity';
@Entity({ name: 'workspace', schema: 'core' })
@ObjectType('Workspace')
export class Workspace {
@IDField(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;
@Field({ nullable: true })
@Column({ nullable: true })
domainName?: string;
@Field({ nullable: true })
@Column({ nullable: true })
displayName?: string;
@Field({ nullable: true })
@Column({ nullable: true })
logo?: string;
@Field({ nullable: true })
@Column({ nullable: true })
inviteHash?: string;
@Field({ nullable: true })
@Column({ nullable: true })
deletedAt?: Date;
@Field()
@CreateDateColumn({ type: 'timestamp with time zone' })
createdAt: Date;
@Field()
@UpdateDateColumn({ type: 'timestamp with time zone' })
updatedAt: Date;
@OneToMany(() => User, (user) => user.defaultWorkspace)
users: User[];
}

View File

@ -1,34 +1,35 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { PipelineModule } from 'src/core/pipeline/pipeline.module';
import { CompanyModule } from 'src/core/company/company.module';
import { PersonModule } from 'src/core/person/person.module';
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { FileModule } from 'src/core/file/file.module';
import { WorkspaceManagerModule } from 'src/workspace/workspace-manager/workspace-manager.module';
import { AbilityModule } from 'src/ability/ability.module';
import { PrismaModule } from 'src/database/prisma.module';
import { WorkspaceResolver } from 'src/core/workspace/workspace.resolver';
// eslint-disable-next-line no-restricted-imports
import config from '../../../ormconfig';
import { Workspace } from './workspace.entity';
import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
import { WorkspaceService } from './services/workspace.service';
import { WorkspaceMemberService } from './services/workspace-member.service';
import { WorkspaceMemberResolver } from './resolvers/workspace-member.resolver';
import { WorkspaceResolver } from './resolvers/workspace.resolver';
@Module({
imports: [
AbilityModule,
PipelineModule,
CompanyModule,
PersonModule,
WorkspaceManagerModule,
PrismaModule,
TypeOrmModule.forRoot(config),
NestjsQueryGraphQLModule.forFeature({
imports: [
NestjsQueryTypeOrmModule.forFeature([Workspace]),
WorkspaceManagerModule,
FileModule,
],
services: [WorkspaceService],
resolvers: workspaceAutoResolverOpts,
}),
],
providers: [
WorkspaceService,
FileUploadService,
WorkspaceMemberService,
WorkspaceMemberResolver,
WorkspaceResolver,
],
exports: [WorkspaceService, WorkspaceMemberService],
exports: [WorkspaceService],
providers: [WorkspaceResolver, WorkspaceService],
})
export class WorkspaceModule {}

Some files were not shown because too many files have changed in this diff Show More