Move defaultAvatarUrl on userWorkspace + migration command (#12100)
closes https://github.com/twentyhq/core-team-issues/issues/883
This commit is contained in:
@ -0,0 +1,87 @@
|
|||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
import { Command } from 'nest-commander';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||||
|
RunOnWorkspaceArgs,
|
||||||
|
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||||
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
|
||||||
|
@Command({
|
||||||
|
name: 'upgrade:0-54:migrate-default-avatar-url-to-user-workspace',
|
||||||
|
description: 'Migrate default avatar url to user workspace',
|
||||||
|
})
|
||||||
|
export class MigrateDefaultAvatarUrlToUserWorkspaceCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Workspace, 'core')
|
||||||
|
protected readonly workspaceRepository: Repository<Workspace>,
|
||||||
|
@InjectRepository(UserWorkspace, 'core')
|
||||||
|
protected readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
||||||
|
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
|
) {
|
||||||
|
super(workspaceRepository, twentyORMGlobalManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async runOnWorkspace({
|
||||||
|
index,
|
||||||
|
total,
|
||||||
|
workspaceId,
|
||||||
|
options,
|
||||||
|
}: RunOnWorkspaceArgs): Promise<void> {
|
||||||
|
this.logger.log(
|
||||||
|
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.migrateDefaultAvatarUrlToUserWorkspace({
|
||||||
|
workspaceId,
|
||||||
|
dryRun: !!options.dryRun,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async migrateDefaultAvatarUrlToUserWorkspace({
|
||||||
|
workspaceId,
|
||||||
|
dryRun,
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
dryRun: boolean;
|
||||||
|
}) {
|
||||||
|
const workspace = await this.workspaceRepository.findOneOrFail({
|
||||||
|
where: {
|
||||||
|
id: workspaceId,
|
||||||
|
},
|
||||||
|
relations: ['workspaceUsers', 'workspaceUsers.user'],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const workspaceUser of workspace.workspaceUsers) {
|
||||||
|
if (isNonEmptyString(workspaceUser.user.defaultAvatarUrl)) {
|
||||||
|
const userWorkspacesCount = await this.userWorkspaceRepository.count({
|
||||||
|
where: {
|
||||||
|
userId: workspaceUser.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userWorkspacesCount === 1) {
|
||||||
|
if (!dryRun)
|
||||||
|
await this.userWorkspaceRepository.update(
|
||||||
|
{
|
||||||
|
userId: workspaceUser.user.id,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultAvatarUrl: workspaceUser.user.defaultAvatarUrl,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Updated default avatar url for user ${workspaceUser.user.id} on user workspace ${workspaceUser.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,8 +5,10 @@ import { CleanNotFoundFilesCommand } from 'src/database/commands/upgrade-version
|
|||||||
import { FixCreatedByDefaultValueCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-created-by-default-value.command';
|
import { FixCreatedByDefaultValueCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-created-by-default-value.command';
|
||||||
import { FixStandardSelectFieldsPositionCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-fix-standard-select-fields-position.command';
|
import { FixStandardSelectFieldsPositionCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-fix-standard-select-fields-position.command';
|
||||||
import { LowercaseUserAndInvitationEmailsCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-lowercase-user-and-invitation-emails.command';
|
import { LowercaseUserAndInvitationEmailsCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-lowercase-user-and-invitation-emails.command';
|
||||||
|
import { MigrateDefaultAvatarUrlToUserWorkspaceCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-migrate-default-avatar-url-to-user-workspace.command';
|
||||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||||
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
@ -17,7 +19,10 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Workspace, AppToken, User], 'core'),
|
TypeOrmModule.forFeature(
|
||||||
|
[Workspace, AppToken, User, UserWorkspace],
|
||||||
|
'core',
|
||||||
|
),
|
||||||
TypeOrmModule.forFeature(
|
TypeOrmModule.forFeature(
|
||||||
[FieldMetadataEntity, ObjectMetadataEntity],
|
[FieldMetadataEntity, ObjectMetadataEntity],
|
||||||
'metadata',
|
'metadata',
|
||||||
@ -32,12 +37,14 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor
|
|||||||
FixCreatedByDefaultValueCommand,
|
FixCreatedByDefaultValueCommand,
|
||||||
CleanNotFoundFilesCommand,
|
CleanNotFoundFilesCommand,
|
||||||
LowercaseUserAndInvitationEmailsCommand,
|
LowercaseUserAndInvitationEmailsCommand,
|
||||||
|
MigrateDefaultAvatarUrlToUserWorkspaceCommand,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
FixStandardSelectFieldsPositionCommand,
|
FixStandardSelectFieldsPositionCommand,
|
||||||
FixCreatedByDefaultValueCommand,
|
FixCreatedByDefaultValueCommand,
|
||||||
CleanNotFoundFilesCommand,
|
CleanNotFoundFilesCommand,
|
||||||
LowercaseUserAndInvitationEmailsCommand,
|
LowercaseUserAndInvitationEmailsCommand,
|
||||||
|
MigrateDefaultAvatarUrlToUserWorkspaceCommand,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class V0_54_UpgradeVersionCommandModule {}
|
export class V0_54_UpgradeVersionCommandModule {}
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddDefaultAvatarUrlColumnInUserWorkspaceTable1747401483135
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'AddDefaultAvatarUrlColumnInUserWorkspaceTable1747401483135';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" ADD "defaultAvatarUrl" character varying`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."userWorkspace" DROP COLUMN "defaultAvatarUrl"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -39,7 +39,6 @@ describe('SignInUpService', () => {
|
|||||||
let service: SignInUpService;
|
let service: SignInUpService;
|
||||||
let UserRepository: Repository<User>;
|
let UserRepository: Repository<User>;
|
||||||
let WorkspaceRepository: Repository<Workspace>;
|
let WorkspaceRepository: Repository<Workspace>;
|
||||||
let fileUploadService: FileUploadService;
|
|
||||||
let workspaceInvitationService: WorkspaceInvitationService;
|
let workspaceInvitationService: WorkspaceInvitationService;
|
||||||
let userWorkspaceService: UserWorkspaceService;
|
let userWorkspaceService: UserWorkspaceService;
|
||||||
let twentyConfigService: TwentyConfigService;
|
let twentyConfigService: TwentyConfigService;
|
||||||
@ -137,7 +136,6 @@ describe('SignInUpService', () => {
|
|||||||
service = module.get<SignInUpService>(SignInUpService);
|
service = module.get<SignInUpService>(SignInUpService);
|
||||||
UserRepository = module.get(getRepositoryToken(User, 'core'));
|
UserRepository = module.get(getRepositoryToken(User, 'core'));
|
||||||
WorkspaceRepository = module.get(getRepositoryToken(Workspace, 'core'));
|
WorkspaceRepository = module.get(getRepositoryToken(Workspace, 'core'));
|
||||||
fileUploadService = module.get<FileUploadService>(FileUploadService);
|
|
||||||
workspaceInvitationService = module.get<WorkspaceInvitationService>(
|
workspaceInvitationService = module.get<WorkspaceInvitationService>(
|
||||||
WorkspaceInvitationService,
|
WorkspaceInvitationService,
|
||||||
);
|
);
|
||||||
@ -249,11 +247,6 @@ describe('SignInUpService', () => {
|
|||||||
id: 'newWorkspaceId',
|
id: 'newWorkspaceId',
|
||||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||||
} as Workspace);
|
} as Workspace);
|
||||||
jest.spyOn(fileUploadService, 'uploadImage').mockResolvedValue({
|
|
||||||
id: '',
|
|
||||||
mimeType: '',
|
|
||||||
paths: ['path/to/image'],
|
|
||||||
});
|
|
||||||
jest.spyOn(UserRepository, 'create').mockReturnValue({} as User);
|
jest.spyOn(UserRepository, 'create').mockReturnValue({} as User);
|
||||||
jest
|
jest
|
||||||
.spyOn(domainManagerService, 'generateSubdomain')
|
.spyOn(domainManagerService, 'generateSubdomain')
|
||||||
@ -274,7 +267,12 @@ describe('SignInUpService', () => {
|
|||||||
expect(WorkspaceRepository.save).toHaveBeenCalled();
|
expect(WorkspaceRepository.save).toHaveBeenCalled();
|
||||||
expect(UserRepository.create).toHaveBeenCalled();
|
expect(UserRepository.create).toHaveBeenCalled();
|
||||||
expect(UserRepository.save).toHaveBeenCalled();
|
expect(UserRepository.save).toHaveBeenCalled();
|
||||||
expect(fileUploadService.uploadImage).toHaveBeenCalled();
|
expect(userWorkspaceService.create).toHaveBeenCalledWith({
|
||||||
|
workspaceId: 'newWorkspaceId',
|
||||||
|
userId: 'newUserId',
|
||||||
|
isExistingUser: false,
|
||||||
|
pictureUrl: 'pictureUrl',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle signIn on workspace in pending state', async () => {
|
it('should handle signIn on workspace in pending state', async () => {
|
||||||
|
|||||||
@ -2,14 +2,11 @@ import { HttpService } from '@nestjs/axios';
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import FileType from 'file-type';
|
|
||||||
import { TWENTY_ICONS_BASE_URL } from 'twenty-shared/constants';
|
import { TWENTY_ICONS_BASE_URL } from 'twenty-shared/constants';
|
||||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
|
||||||
|
|
||||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||||
import {
|
import {
|
||||||
AuthException,
|
AuthException,
|
||||||
@ -39,7 +36,6 @@ import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-in
|
|||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||||
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
|
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
|
||||||
import { getImageBufferFromUrl } from 'src/utils/image';
|
|
||||||
import { isWorkEmail } from 'src/utils/is-work-email';
|
import { isWorkEmail } from 'src/utils/is-work-email';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -232,14 +228,10 @@ export class SignInUpService {
|
|||||||
newUserWithPicture: PartialUserWithPicture;
|
newUserWithPicture: PartialUserWithPicture;
|
||||||
};
|
};
|
||||||
|
|
||||||
const user = await this.saveNewUser(
|
const user = await this.saveNewUser(userData.newUserWithPicture, {
|
||||||
userData.newUserWithPicture,
|
canAccessFullAdminPanel: false,
|
||||||
params.workspace.id,
|
canImpersonate: false,
|
||||||
{
|
});
|
||||||
canAccessFullAdminPanel: false,
|
|
||||||
canImpersonate: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.activateOnboardingForUser(user, params.workspace);
|
await this.activateOnboardingForUser(user, params.workspace);
|
||||||
|
|
||||||
@ -284,7 +276,6 @@ export class SignInUpService {
|
|||||||
|
|
||||||
private async saveNewUser(
|
private async saveNewUser(
|
||||||
newUserWithPicture: PartialUserWithPicture,
|
newUserWithPicture: PartialUserWithPicture,
|
||||||
workspaceId: string,
|
|
||||||
{
|
{
|
||||||
canImpersonate,
|
canImpersonate,
|
||||||
canAccessFullAdminPanel,
|
canAccessFullAdminPanel,
|
||||||
@ -293,13 +284,8 @@ export class SignInUpService {
|
|||||||
canAccessFullAdminPanel: boolean;
|
canAccessFullAdminPanel: boolean;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const defaultAvatarUrl = await this.uploadPicture(
|
|
||||||
newUserWithPicture.picture,
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
const userCreated = this.userRepository.create({
|
const userCreated = this.userRepository.create({
|
||||||
...newUserWithPicture,
|
...newUserWithPicture,
|
||||||
defaultAvatarUrl,
|
|
||||||
canImpersonate,
|
canImpersonate,
|
||||||
canAccessFullAdminPanel,
|
canAccessFullAdminPanel,
|
||||||
});
|
});
|
||||||
@ -368,15 +354,22 @@ export class SignInUpService {
|
|||||||
|
|
||||||
const workspace = await this.workspaceRepository.save(workspaceToCreate);
|
const workspace = await this.workspaceRepository.save(workspaceToCreate);
|
||||||
|
|
||||||
const user =
|
const isExistingUser = userData.type === 'existingUser';
|
||||||
userData.type === 'existingUser'
|
const user = isExistingUser
|
||||||
? userData.existingUser
|
? userData.existingUser
|
||||||
: await this.saveNewUser(userData.newUserWithPicture, workspace.id, {
|
: await this.saveNewUser(userData.newUserWithPicture, {
|
||||||
canImpersonate,
|
canImpersonate,
|
||||||
canAccessFullAdminPanel,
|
canAccessFullAdminPanel,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.userWorkspaceService.create(user.id, workspace.id);
|
await this.userWorkspaceService.create({
|
||||||
|
userId: user.id,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
isExistingUser,
|
||||||
|
pictureUrl: isExistingUser
|
||||||
|
? undefined
|
||||||
|
: userData.newUserWithPicture.picture,
|
||||||
|
});
|
||||||
|
|
||||||
await this.activateOnboardingForUser(user, workspace);
|
await this.activateOnboardingForUser(user, workspace);
|
||||||
|
|
||||||
@ -387,30 +380,4 @@ export class SignInUpService {
|
|||||||
|
|
||||||
return { user, workspace };
|
return { user, workspace };
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadPicture(
|
|
||||||
picture: string | null | undefined,
|
|
||||||
workspaceId: string,
|
|
||||||
): Promise<string | undefined> {
|
|
||||||
if (!picture) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = await getImageBufferFromUrl(
|
|
||||||
picture,
|
|
||||||
this.httpService.axiosRef,
|
|
||||||
);
|
|
||||||
|
|
||||||
const type = await FileType.fromBuffer(buffer);
|
|
||||||
|
|
||||||
const { paths } = await this.fileUploadService.uploadImage({
|
|
||||||
file: buffer,
|
|
||||||
filename: `${v4()}.${type?.ext}`,
|
|
||||||
mimeType: type?.mime,
|
|
||||||
fileFolder: FileFolder.ProfilePicture,
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return paths[0];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { FileUploadResolver } from 'src/engine/core-modules/file/file-upload/resolvers/file-upload.resolver';
|
import { FileUploadResolver } from 'src/engine/core-modules/file/file-upload/resolvers/file-upload.resolver';
|
||||||
@ -5,7 +6,7 @@ import { FileUploadService } from 'src/engine/core-modules/file/file-upload/serv
|
|||||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [FileModule],
|
imports: [FileModule, HttpModule],
|
||||||
providers: [FileUploadService, FileUploadResolver],
|
providers: [FileUploadService, FileUploadResolver],
|
||||||
exports: [FileUploadService, FileUploadResolver],
|
exports: [FileUploadService, FileUploadResolver],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,22 +1,25 @@
|
|||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
|
import FileType from 'file-type';
|
||||||
import { JSDOM } from 'jsdom';
|
import { JSDOM } from 'jsdom';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4, v4 } from 'uuid';
|
||||||
|
|
||||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||||
|
|
||||||
import { settings } from 'src/engine/constants/settings';
|
import { settings } from 'src/engine/constants/settings';
|
||||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||||
import { getCropSize } from 'src/utils/image';
|
import { getCropSize, getImageBufferFromUrl } from 'src/utils/image';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FileUploadService {
|
export class FileUploadService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly fileStorage: FileStorageService,
|
private readonly fileStorage: FileStorageService,
|
||||||
private readonly fileService: FileService,
|
private readonly fileService: FileService,
|
||||||
|
private readonly httpService: HttpService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private async _uploadFile({
|
private async _uploadFile({
|
||||||
@ -93,6 +96,31 @@ export class FileUploadService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async uploadImageFromUrl({
|
||||||
|
imageUrl,
|
||||||
|
fileFolder,
|
||||||
|
workspaceId,
|
||||||
|
}: {
|
||||||
|
imageUrl: string;
|
||||||
|
fileFolder: FileFolder;
|
||||||
|
workspaceId: string;
|
||||||
|
}) {
|
||||||
|
const buffer = await getImageBufferFromUrl(
|
||||||
|
imageUrl,
|
||||||
|
this.httpService.axiosRef,
|
||||||
|
);
|
||||||
|
|
||||||
|
const type = await FileType.fromBuffer(buffer);
|
||||||
|
|
||||||
|
return await this.uploadImage({
|
||||||
|
file: buffer,
|
||||||
|
filename: `${v4()}.${type?.ext}`,
|
||||||
|
mimeType: type?.mime,
|
||||||
|
fileFolder,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async uploadImage({
|
async uploadImage({
|
||||||
file,
|
file,
|
||||||
filename,
|
filename,
|
||||||
|
|||||||
@ -6,8 +6,13 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
|
|||||||
|
|
||||||
import { FileService } from './file.service';
|
import { FileService } from './file.service';
|
||||||
|
|
||||||
|
jest.mock('uuid', () => ({
|
||||||
|
v4: jest.fn(() => 'mocked-uuid'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('FileService', () => {
|
describe('FileService', () => {
|
||||||
let service: FileService;
|
let service: FileService;
|
||||||
|
let fileStorageService: FileStorageService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
@ -15,7 +20,9 @@ describe('FileService', () => {
|
|||||||
FileService,
|
FileService,
|
||||||
{
|
{
|
||||||
provide: FileStorageService,
|
provide: FileStorageService,
|
||||||
useValue: {},
|
useValue: {
|
||||||
|
copy: jest.fn(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: TwentyConfigService,
|
provide: TwentyConfigService,
|
||||||
@ -29,9 +36,35 @@ describe('FileService', () => {
|
|||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<FileService>(FileService);
|
service = module.get<FileService>(FileService);
|
||||||
|
fileStorageService = module.get<FileStorageService>(FileStorageService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('copyFileFromWorkspaceToWorkspace - should copy a file to a new workspace', async () => {
|
||||||
|
const result = await service.copyFileFromWorkspaceToWorkspace(
|
||||||
|
'workspaceId',
|
||||||
|
'path/to/file',
|
||||||
|
'newWorkspaceId',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fileStorageService.copy).toHaveBeenCalledWith({
|
||||||
|
from: {
|
||||||
|
folderPath: 'workspace-workspaceId/path/to',
|
||||||
|
filename: 'file',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
folderPath: 'workspace-newWorkspaceId/path/to',
|
||||||
|
filename: 'mocked-uuid',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
'workspace-newWorkspaceId',
|
||||||
|
'path/to',
|
||||||
|
'mocked-uuid',
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { basename, dirname, extname } from 'path';
|
||||||
import { Stream } from 'stream';
|
import { Stream } from 'stream';
|
||||||
|
|
||||||
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
|
||||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
@ -74,4 +77,30 @@ export class FileService {
|
|||||||
folderPath: workspaceFolderPath,
|
folderPath: workspaceFolderPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async copyFileFromWorkspaceToWorkspace(
|
||||||
|
fromWorkspaceId: string,
|
||||||
|
fromPath: string,
|
||||||
|
toWorkspaceId: string,
|
||||||
|
) {
|
||||||
|
const subFolder = dirname(fromPath);
|
||||||
|
const fromWorkspaceFolderPath = `workspace-${fromWorkspaceId}`;
|
||||||
|
const toWorkspaceFolderPath = `workspace-${toWorkspaceId}`;
|
||||||
|
const fromFilename = basename(fromPath);
|
||||||
|
|
||||||
|
const toFilename = uuidV4() + extname(fromFilename);
|
||||||
|
|
||||||
|
await this.fileStorageService.copy({
|
||||||
|
from: {
|
||||||
|
folderPath: `${fromWorkspaceFolderPath}/${subFolder}`,
|
||||||
|
filename: fromFilename,
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
folderPath: `${toWorkspaceFolderPath}/${subFolder}`,
|
||||||
|
filename: toFilename,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return [toWorkspaceFolderPath, subFolder, toFilename];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,6 +60,9 @@ export class UserWorkspace {
|
|||||||
@Column()
|
@Column()
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
defaultAvatarUrl: string;
|
||||||
|
|
||||||
@Field()
|
@Field()
|
||||||
@CreateDateColumn({ type: 'timestamptz' })
|
@CreateDateColumn({ type: 'timestamptz' })
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
|||||||
|
|
||||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||||
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||||
|
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||||
|
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||||
import { TwoFactorMethod } from 'src/engine/core-modules/two-factor-method/two-factor-method.entity';
|
import { TwoFactorMethod } from 'src/engine/core-modules/two-factor-method/two-factor-method.entity';
|
||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
import { UserWorkspaceResolver } from 'src/engine/core-modules/user-workspace/user-workspace.resolver';
|
import { UserWorkspaceResolver } from 'src/engine/core-modules/user-workspace/user-workspace.resolver';
|
||||||
@ -34,6 +36,8 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
|
|||||||
DomainManagerModule,
|
DomainManagerModule,
|
||||||
TwentyORMModule,
|
TwentyORMModule,
|
||||||
UserRoleModule,
|
UserRoleModule,
|
||||||
|
FileUploadModule,
|
||||||
|
FileModule,
|
||||||
],
|
],
|
||||||
services: [UserWorkspaceService],
|
services: [UserWorkspaceService],
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,13 +1,18 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { DataSource, Repository } from 'typeorm';
|
import { DataSource, IsNull, Not, Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||||
|
|
||||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
|
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
|
||||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||||
|
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||||
|
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
||||||
|
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
@ -32,6 +37,8 @@ describe('UserWorkspaceService', () => {
|
|||||||
let domainManagerService: DomainManagerService;
|
let domainManagerService: DomainManagerService;
|
||||||
let twentyORMGlobalManager: TwentyORMGlobalManager;
|
let twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||||
let userRoleService: UserRoleService;
|
let userRoleService: UserRoleService;
|
||||||
|
let fileService: FileService;
|
||||||
|
let fileUploadService: FileUploadService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
@ -46,6 +53,7 @@ describe('UserWorkspaceService', () => {
|
|||||||
countBy: jest.fn(),
|
countBy: jest.fn(),
|
||||||
exists: jest.fn(),
|
exists: jest.fn(),
|
||||||
findOne: jest.fn(),
|
findOne: jest.fn(),
|
||||||
|
findOneOrFail: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -103,10 +111,29 @@ describe('UserWorkspaceService', () => {
|
|||||||
assignRoleToUserWorkspace: jest.fn(),
|
assignRoleToUserWorkspace: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: FileStorageService,
|
||||||
|
useValue: {
|
||||||
|
copy: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: FileUploadService,
|
||||||
|
useValue: {
|
||||||
|
uploadImageFromUrl: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: FileService,
|
||||||
|
useValue: {
|
||||||
|
copyFileFromWorkspaceToWorkspace: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<UserWorkspaceService>(UserWorkspaceService);
|
service = module.get<UserWorkspaceService>(UserWorkspaceService);
|
||||||
|
fileService = module.get<FileService>(FileService);
|
||||||
userWorkspaceRepository = module.get(
|
userWorkspaceRepository = module.get(
|
||||||
getRepositoryToken(UserWorkspace, 'core'),
|
getRepositoryToken(UserWorkspace, 'core'),
|
||||||
);
|
);
|
||||||
@ -127,6 +154,7 @@ describe('UserWorkspaceService', () => {
|
|||||||
TwentyORMGlobalManager,
|
TwentyORMGlobalManager,
|
||||||
);
|
);
|
||||||
userRoleService = module.get<UserRoleService>(UserRoleService);
|
userRoleService = module.get<UserRoleService>(UserRoleService);
|
||||||
|
fileUploadService = module.get<FileUploadService>(FileUploadService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
@ -134,7 +162,139 @@ describe('UserWorkspaceService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
it('should create a user workspace and emit an event', async () => {
|
it("should create a user workspace with a default avatar url if it's an existing user with a user workspace having a default avatar url", async () => {
|
||||||
|
const userId = 'user-id';
|
||||||
|
const workspaceId = 'workspace-id';
|
||||||
|
const userWorkspace = { userId, workspaceId } as UserWorkspace;
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(userWorkspaceRepository, 'create')
|
||||||
|
.mockReturnValue(userWorkspace);
|
||||||
|
jest
|
||||||
|
.spyOn(userWorkspaceRepository, 'save')
|
||||||
|
.mockResolvedValue(userWorkspace);
|
||||||
|
jest.spyOn(userWorkspaceRepository, 'findOne').mockResolvedValue({
|
||||||
|
defaultAvatarUrl: 'path/to/file',
|
||||||
|
} as UserWorkspace);
|
||||||
|
jest
|
||||||
|
.spyOn(fileService, 'copyFileFromWorkspaceToWorkspace')
|
||||||
|
.mockResolvedValue(['', 'path/to', 'copy']);
|
||||||
|
jest
|
||||||
|
.spyOn(workspaceEventEmitter, 'emitCustomBatchEvent')
|
||||||
|
.mockImplementation();
|
||||||
|
|
||||||
|
const result = await service.create({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
isExistingUser: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(userWorkspaceRepository.findOne).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
defaultAvatarUrl: Not(IsNull()),
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
createdAt: 'ASC',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(userWorkspaceRepository.create).toHaveBeenCalledWith({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
defaultAvatarUrl: 'path/to/copy',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(userWorkspaceRepository.save).toHaveBeenCalledWith(userWorkspace);
|
||||||
|
expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith(
|
||||||
|
USER_SIGNUP_EVENT_NAME,
|
||||||
|
[{ userId }],
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(userWorkspace);
|
||||||
|
});
|
||||||
|
it("should create a user workspace without a default avatar url if it's an existing user without any user workspace having a default avatar url", async () => {
|
||||||
|
const userId = 'user-id';
|
||||||
|
const workspaceId = 'workspace-id';
|
||||||
|
const userWorkspace = { userId, workspaceId } as UserWorkspace;
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(userWorkspaceRepository, 'create')
|
||||||
|
.mockReturnValue(userWorkspace);
|
||||||
|
jest
|
||||||
|
.spyOn(userWorkspaceRepository, 'save')
|
||||||
|
.mockResolvedValue(userWorkspace);
|
||||||
|
|
||||||
|
jest.spyOn(userWorkspaceRepository, 'findOne').mockResolvedValue(null);
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(workspaceEventEmitter, 'emitCustomBatchEvent')
|
||||||
|
.mockImplementation();
|
||||||
|
|
||||||
|
const result = await service.create({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
isExistingUser: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(userWorkspaceRepository.create).toHaveBeenCalledWith({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
defaultAvatarUrl: undefined,
|
||||||
|
});
|
||||||
|
expect(userWorkspaceRepository.save).toHaveBeenCalledWith(userWorkspace);
|
||||||
|
expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith(
|
||||||
|
USER_SIGNUP_EVENT_NAME,
|
||||||
|
[{ userId }],
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(userWorkspace);
|
||||||
|
});
|
||||||
|
it("should create a user workspace with a default avatar url if it's a new user with a picture url", async () => {
|
||||||
|
const userId = 'user-id';
|
||||||
|
const workspaceId = 'workspace-id';
|
||||||
|
const userWorkspace = { userId, workspaceId } as UserWorkspace;
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(userWorkspaceRepository, 'create')
|
||||||
|
.mockReturnValue(userWorkspace);
|
||||||
|
jest
|
||||||
|
.spyOn(userWorkspaceRepository, 'save')
|
||||||
|
.mockResolvedValue(userWorkspace);
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(workspaceEventEmitter, 'emitCustomBatchEvent')
|
||||||
|
.mockImplementation();
|
||||||
|
jest.spyOn(fileUploadService, 'uploadImageFromUrl').mockResolvedValue({
|
||||||
|
paths: ['path/to/file'],
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const result = await service.create({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
isExistingUser: false,
|
||||||
|
pictureUrl: 'picture-url',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fileUploadService.uploadImageFromUrl).toHaveBeenCalledWith({
|
||||||
|
imageUrl: 'picture-url',
|
||||||
|
fileFolder: FileFolder.ProfilePicture,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
expect(userWorkspaceRepository.create).toHaveBeenCalledWith({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
defaultAvatarUrl: 'path/to/file',
|
||||||
|
});
|
||||||
|
expect(userWorkspaceRepository.save).toHaveBeenCalledWith(userWorkspace);
|
||||||
|
expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith(
|
||||||
|
USER_SIGNUP_EVENT_NAME,
|
||||||
|
[{ userId }],
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(userWorkspace);
|
||||||
|
});
|
||||||
|
it("should create a user workspace without a default avatar url if it's a new user without a picture url", async () => {
|
||||||
const userId = 'user-id';
|
const userId = 'user-id';
|
||||||
const workspaceId = 'workspace-id';
|
const workspaceId = 'workspace-id';
|
||||||
const userWorkspace = { userId, workspaceId } as UserWorkspace;
|
const userWorkspace = { userId, workspaceId } as UserWorkspace;
|
||||||
@ -149,11 +309,17 @@ describe('UserWorkspaceService', () => {
|
|||||||
.spyOn(workspaceEventEmitter, 'emitCustomBatchEvent')
|
.spyOn(workspaceEventEmitter, 'emitCustomBatchEvent')
|
||||||
.mockImplementation();
|
.mockImplementation();
|
||||||
|
|
||||||
const result = await service.create(userId, workspaceId);
|
const result = await service.create({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
isExistingUser: false,
|
||||||
|
pictureUrl: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
expect(userWorkspaceRepository.create).toHaveBeenCalledWith({
|
expect(userWorkspaceRepository.create).toHaveBeenCalledWith({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
defaultAvatarUrl: undefined,
|
||||||
});
|
});
|
||||||
expect(userWorkspaceRepository.save).toHaveBeenCalledWith(userWorkspace);
|
expect(userWorkspaceRepository.save).toHaveBeenCalledWith(userWorkspace);
|
||||||
expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith(
|
expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith(
|
||||||
@ -214,6 +380,10 @@ describe('UserWorkspaceService', () => {
|
|||||||
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
|
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
|
||||||
.mockResolvedValue(workspaceMemberRepository as any);
|
.mockResolvedValue(workspaceMemberRepository as any);
|
||||||
|
|
||||||
|
jest.spyOn(userWorkspaceRepository, 'findOneOrFail').mockResolvedValue({
|
||||||
|
defaultAvatarUrl: 'userWorkspace-avatar-url',
|
||||||
|
} as UserWorkspace);
|
||||||
|
|
||||||
await service.createWorkspaceMember(workspaceId, user);
|
await service.createWorkspaceMember(workspaceId, user);
|
||||||
|
|
||||||
expect(workspaceMemberRepository.insert).toHaveBeenCalledWith({
|
expect(workspaceMemberRepository.insert).toHaveBeenCalledWith({
|
||||||
@ -225,7 +395,7 @@ describe('UserWorkspaceService', () => {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
userEmail: user.email,
|
userEmail: user.email,
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
avatarUrl: 'avatar-url',
|
avatarUrl: 'userWorkspace-avatar-url',
|
||||||
});
|
});
|
||||||
expect(objectMetadataRepository.findOneOrFail).toHaveBeenCalledWith({
|
expect(objectMetadataRepository.findOneOrFail).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
@ -284,7 +454,11 @@ describe('UserWorkspaceService', () => {
|
|||||||
user.id,
|
user.id,
|
||||||
workspace.id,
|
workspace.id,
|
||||||
);
|
);
|
||||||
expect(service.create).toHaveBeenCalledWith(user.id, workspace.id);
|
expect(service.create).toHaveBeenCalledWith({
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
userId: user.id,
|
||||||
|
isExistingUser: true,
|
||||||
|
});
|
||||||
expect(service.createWorkspaceMember).toHaveBeenCalledWith(
|
expect(service.createWorkspaceMember).toHaveBeenCalledWith(
|
||||||
workspace.id,
|
workspace.id,
|
||||||
user,
|
user,
|
||||||
|
|||||||
@ -4,9 +4,10 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||||
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { Repository } from 'typeorm';
|
import { IsNull, Not, Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||||
|
|
||||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
|
||||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
|
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
|
||||||
import {
|
import {
|
||||||
@ -15,13 +16,14 @@ import {
|
|||||||
} from 'src/engine/core-modules/auth/auth.exception';
|
} from 'src/engine/core-modules/auth/auth.exception';
|
||||||
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
|
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
|
||||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||||
|
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
||||||
|
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import {
|
import {
|
||||||
PermissionsException,
|
PermissionsException,
|
||||||
@ -42,21 +44,40 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
|||||||
private readonly userRepository: Repository<User>,
|
private readonly userRepository: Repository<User>,
|
||||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||||
private readonly dataSourceService: DataSourceService,
|
|
||||||
private readonly typeORMService: TypeORMService,
|
|
||||||
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
||||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||||
private readonly domainManagerService: DomainManagerService,
|
private readonly domainManagerService: DomainManagerService,
|
||||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
private readonly userRoleService: UserRoleService,
|
private readonly userRoleService: UserRoleService,
|
||||||
|
private readonly fileUploadService: FileUploadService,
|
||||||
|
private readonly fileService: FileService,
|
||||||
) {
|
) {
|
||||||
super(userWorkspaceRepository);
|
super(userWorkspaceRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(userId: string, workspaceId: string): Promise<UserWorkspace> {
|
async create({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
isExistingUser,
|
||||||
|
pictureUrl,
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
isExistingUser: boolean;
|
||||||
|
pictureUrl?: string;
|
||||||
|
}): Promise<UserWorkspace> {
|
||||||
|
const defaultAvatarUrl = await this.computeDefaultAvatarUrl(
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
isExistingUser,
|
||||||
|
pictureUrl,
|
||||||
|
);
|
||||||
|
|
||||||
const userWorkspace = this.userWorkspaceRepository.create({
|
const userWorkspace = this.userWorkspaceRepository.create({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
defaultAvatarUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.workspaceEventEmitter.emitCustomBatchEvent(
|
this.workspaceEventEmitter.emitCustomBatchEvent(
|
||||||
@ -78,6 +99,13 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const userWorkspace = await this.userWorkspaceRepository.findOneOrFail({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await workspaceMemberRepository.insert({
|
await workspaceMemberRepository.insert({
|
||||||
name: {
|
name: {
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
@ -86,7 +114,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
|||||||
colorScheme: 'System',
|
colorScheme: 'System',
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
userEmail: user.email,
|
userEmail: user.email,
|
||||||
avatarUrl: user.defaultAvatarUrl ?? '',
|
avatarUrl: userWorkspace.defaultAvatarUrl ?? '',
|
||||||
locale: (user.locale ?? SOURCE_LOCALE) as keyof typeof APP_LOCALES,
|
locale: (user.locale ?? SOURCE_LOCALE) as keyof typeof APP_LOCALES,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -133,7 +161,11 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!userWorkspace) {
|
if (!userWorkspace) {
|
||||||
userWorkspace = await this.create(user.id, workspace.id);
|
userWorkspace = await this.create({
|
||||||
|
userId: user.id,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
isExistingUser: true,
|
||||||
|
});
|
||||||
|
|
||||||
await this.createWorkspaceMember(workspace.id, user);
|
await this.createWorkspaceMember(workspace.id, user);
|
||||||
|
|
||||||
@ -307,4 +339,44 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
|||||||
|
|
||||||
return workspaceMember;
|
return workspaceMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async computeDefaultAvatarUrl(
|
||||||
|
userId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
isExistingUser: boolean,
|
||||||
|
pictureUrl?: string,
|
||||||
|
) {
|
||||||
|
if (isExistingUser) {
|
||||||
|
const userWorkspace = await this.userWorkspaceRepository.findOne({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
defaultAvatarUrl: Not(IsNull()),
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
createdAt: 'ASC',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isDefined(userWorkspace?.defaultAvatarUrl)) return;
|
||||||
|
|
||||||
|
const [_, subFolder, filename] =
|
||||||
|
await this.fileService.copyFileFromWorkspaceToWorkspace(
|
||||||
|
userWorkspace.workspaceId,
|
||||||
|
userWorkspace.defaultAvatarUrl,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return `${subFolder}/${filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDefined(pictureUrl)) return;
|
||||||
|
|
||||||
|
const { paths } = await this.fileUploadService.uploadImageFromUrl({
|
||||||
|
imageUrl: pictureUrl,
|
||||||
|
fileFolder: FileFolder.ProfilePicture,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return paths[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user