Twenty standard and workspace custom applications 1/3 (#15625)
# Introduction related to https://github.com/twentyhq/core-team-issues/issues/1833 In this PR we're starting the sync-metadata and standardIds deprecation by introducing `twenty-standard` application that will regroup every standard object such as company and opportunities. But also the `custom-workspace-application` which is an app created at the same time as a workspace and that will regroup everything configure within the workspace ( custom objects fields etc ) ## What's done On both new workspace and seeded workspace creation: - Creating a custom workspace app - Creating a twenty standard app - Refactored the seed core schema and workspace creation to be run within a transaction in order to handle circular dependency foreignkey requirements ( which is deferred for app toward workspace ) - Updated workspace entity to have a custom workspace relation ( nullable for the moment until we implem an upgrade command to handle retro comp ) - Integration testing on user, workspace creation deletion and expected default apps creation - ~~Soft deleted user on `deleteUser`~~ Done by marie and rebased on it ## What's next - Update seeder to propagate the `twenty-standard` workspace `applicationId` to every standard synchronized entities ( cheap and fast iteration through the about to be deprecated sync-metadata as an easy way to synchronize standards metadata entities ). - Update seeder to propagate the `custom-workspace-application` workspace `applicationId` to anything custom ( `pets` and `rockets` ) - Prepend `custom-workspace-application` `applicationId` to every metadata API operations ( create a specific cache etc ) - Upgrade command on all existing workspace to create a custom app and associate its applicationId to any existing custom entities - Make `universalIdentifier` and `applicationId` required for any syncable entity
This commit is contained in:
+55
-53
@@ -19,27 +19,27 @@ import {
|
||||
ServerlessFunctionTriggerManifest,
|
||||
} from 'src/engine/core-modules/application/types/application.types';
|
||||
import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service';
|
||||
import { Sources } from 'src/engine/core-modules/file-storage/types/source.type';
|
||||
import { CronTriggerV2Service } from 'src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service';
|
||||
import { FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { DatabaseEventTriggerV2Service } from 'src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service';
|
||||
import { FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2';
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { getFlatEntitiesByApplicationId } from 'src/engine/metadata-modules/flat-entity/utils/get-flat-entities-by-application-id.util';
|
||||
import { getSubFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-or-throw.util';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service';
|
||||
import { RouteTriggerV2Service } from 'src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service';
|
||||
import { FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
|
||||
import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service';
|
||||
import { ServerlessFunctionV2Service } from 'src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service';
|
||||
import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
|
||||
import { Sources } from 'src/engine/core-modules/file-storage/types/source.type';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2';
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationSyncService {
|
||||
@@ -81,13 +81,22 @@ export class ApplicationSyncService {
|
||||
applicationId: application.id,
|
||||
});
|
||||
|
||||
await this.syncServerlessFunctions({
|
||||
serverlessFunctionsToSync: manifest.serverlessFunctions,
|
||||
code: manifest.sources,
|
||||
workspaceId,
|
||||
applicationId: application.id,
|
||||
serverlessFunctionLayerId: application.serverlessFunctionLayerId,
|
||||
});
|
||||
if (manifest.serverlessFunctions.length > 0) {
|
||||
if (!isDefined(application.serverlessFunctionLayerId)) {
|
||||
throw new ApplicationException(
|
||||
`Failed to sync serverless function, could not find a serverless function layer.`,
|
||||
ApplicationExceptionCode.FIELD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await this.syncServerlessFunctions({
|
||||
serverlessFunctionsToSync: manifest.serverlessFunctions,
|
||||
code: manifest.sources,
|
||||
workspaceId,
|
||||
applicationId: application.id,
|
||||
serverlessFunctionLayerId: application.serverlessFunctionLayerId,
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log('✅ Application sync from manifest completed');
|
||||
}
|
||||
@@ -100,57 +109,46 @@ export class ApplicationSyncService {
|
||||
}: ApplicationInput & {
|
||||
workspaceId: string;
|
||||
}): Promise<ApplicationEntity> {
|
||||
const application = await this.applicationService.findByUniversalIdentifier(
|
||||
manifest.application.universalIdentifier,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const name = manifest.application.displayName ?? packageJson.name;
|
||||
|
||||
if (!isDefined(application)) {
|
||||
const serverlessFunctionLayer =
|
||||
await this.serverlessFunctionLayerService.create(
|
||||
{
|
||||
packageJson,
|
||||
yarnLock,
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const application = await this.applicationService.create({
|
||||
const application =
|
||||
(await this.applicationService.findByUniversalIdentifier({
|
||||
universalIdentifier: manifest.application.universalIdentifier,
|
||||
workspaceId,
|
||||
})) ??
|
||||
(await this.applicationService.create({
|
||||
universalIdentifier: manifest.application.universalIdentifier,
|
||||
name,
|
||||
description: manifest.application.description,
|
||||
version: packageJson.version,
|
||||
sourcePath: 'cli-sync', // Placeholder for CLI-synced apps
|
||||
serverlessFunctionLayerId: serverlessFunctionLayer.id,
|
||||
serverlessFunctionLayerId: null,
|
||||
workspaceId,
|
||||
});
|
||||
}));
|
||||
|
||||
await this.applicationVariableService.upsertManyApplicationVariableEntities(
|
||||
let serverlessFunctionLayerId = application.serverlessFunctionLayerId;
|
||||
|
||||
if (manifest.serverlessFunctions.length > 0) {
|
||||
if (!isDefined(serverlessFunctionLayerId)) {
|
||||
serverlessFunctionLayerId = (
|
||||
await this.serverlessFunctionLayerService.create(
|
||||
{
|
||||
packageJson,
|
||||
yarnLock,
|
||||
},
|
||||
workspaceId,
|
||||
)
|
||||
).id;
|
||||
}
|
||||
|
||||
await this.serverlessFunctionLayerService.update(
|
||||
serverlessFunctionLayerId,
|
||||
{
|
||||
applicationVariables: manifest.application.applicationVariables,
|
||||
applicationId: application.id,
|
||||
packageJson,
|
||||
yarnLock,
|
||||
},
|
||||
);
|
||||
|
||||
return application;
|
||||
}
|
||||
|
||||
await this.serverlessFunctionLayerService.update(
|
||||
application.serverlessFunctionLayerId,
|
||||
{
|
||||
packageJson,
|
||||
yarnLock,
|
||||
},
|
||||
);
|
||||
|
||||
await this.applicationService.update(application.id, {
|
||||
name,
|
||||
description: manifest.application.description,
|
||||
version: packageJson.version,
|
||||
});
|
||||
|
||||
await this.applicationVariableService.upsertManyApplicationVariableEntities(
|
||||
{
|
||||
applicationVariables: manifest.application.applicationVariables,
|
||||
@@ -158,7 +156,12 @@ export class ApplicationSyncService {
|
||||
},
|
||||
);
|
||||
|
||||
return application;
|
||||
return await this.applicationService.update(application.id, {
|
||||
name,
|
||||
description: manifest.application.description,
|
||||
version: packageJson.version,
|
||||
serverlessFunctionLayerId,
|
||||
});
|
||||
}
|
||||
|
||||
private async syncFields({
|
||||
@@ -947,8 +950,7 @@ export class ApplicationSyncService {
|
||||
);
|
||||
|
||||
const application = await this.applicationService.findByUniversalIdentifier(
|
||||
applicationUniversalIdentifier,
|
||||
workspaceId,
|
||||
{ universalIdentifier: applicationUniversalIdentifier, workspaceId },
|
||||
);
|
||||
|
||||
if (!isDefined(application)) {
|
||||
|
||||
@@ -35,8 +35,8 @@ export class ApplicationEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
universalIdentifier?: string;
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
universalIdentifier: string;
|
||||
|
||||
@Column({ nullable: false, type: 'text' })
|
||||
name: string;
|
||||
@@ -56,8 +56,8 @@ export class ApplicationEntity {
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
workspaceId: string;
|
||||
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
serverlessFunctionLayerId: string;
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
serverlessFunctionLayerId: string | null;
|
||||
|
||||
@ManyToOne(() => WorkspaceEntity, {
|
||||
onDelete: 'CASCADE',
|
||||
|
||||
@@ -36,6 +36,7 @@ import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/
|
||||
WorkspaceMigrationV2Module,
|
||||
PermissionsModule,
|
||||
],
|
||||
exports: [ApplicationService],
|
||||
providers: [ApplicationResolver, ApplicationService, ApplicationSyncService],
|
||||
})
|
||||
export class ApplicationModule {}
|
||||
|
||||
@@ -2,15 +2,15 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { type QueryRunner, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { PackageJson } from 'src/engine/core-modules/application/types/application.types';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import {
|
||||
ApplicationException,
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/twenty-standard-applications';
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationService {
|
||||
@@ -64,10 +64,13 @@ export class ApplicationService {
|
||||
});
|
||||
}
|
||||
|
||||
async findByUniversalIdentifier(
|
||||
universalIdentifier: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
async findByUniversalIdentifier({
|
||||
universalIdentifier,
|
||||
workspaceId,
|
||||
}: {
|
||||
universalIdentifier: string;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
return this.applicationRepository.findOne({
|
||||
where: {
|
||||
universalIdentifier,
|
||||
@@ -76,34 +79,51 @@ export class ApplicationService {
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: {
|
||||
universalIdentifier?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
serverlessFunctionLayerId: string;
|
||||
sourcePath: string;
|
||||
workspaceId: string;
|
||||
}): Promise<ApplicationEntity> {
|
||||
async createTwentyStandardApplication(
|
||||
{
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
return await this.create(
|
||||
{
|
||||
...TWENTY_STANDARD_APPLICATION,
|
||||
serverlessFunctionLayerId: null,
|
||||
workspaceId,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
async create(
|
||||
data: {
|
||||
universalIdentifier?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
serverlessFunctionLayerId: string | null;
|
||||
sourcePath: string;
|
||||
workspaceId: string;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
): Promise<ApplicationEntity> {
|
||||
const application = this.applicationRepository.create({
|
||||
...data,
|
||||
sourceType: 'local',
|
||||
});
|
||||
|
||||
if (queryRunner) {
|
||||
return queryRunner.manager.save(ApplicationEntity, application);
|
||||
}
|
||||
|
||||
return this.applicationRepository.save(application);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
data: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
sourcePath?: string;
|
||||
packageJson?: PackageJson;
|
||||
yarnLock?: string;
|
||||
packageChecksum?: string;
|
||||
},
|
||||
data: Parameters<typeof this.applicationRepository.update>[1],
|
||||
): Promise<ApplicationEntity> {
|
||||
await this.applicationRepository.update({ id }, data);
|
||||
|
||||
@@ -117,10 +137,10 @@ export class ApplicationService {
|
||||
}
|
||||
|
||||
async delete(universalIdentifier: string, workspaceId: string) {
|
||||
const application = await this.findByUniversalIdentifier(
|
||||
const application = await this.findByUniversalIdentifier({
|
||||
universalIdentifier,
|
||||
workspaceId,
|
||||
);
|
||||
});
|
||||
|
||||
if (!isDefined(application)) {
|
||||
throw new Error(`Application does not exist`);
|
||||
|
||||
@@ -3,10 +3,10 @@ import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
||||
import { ApplicationVariableEntityDTO } from 'src/engine/core-modules/applicationVariable/dtos/application-variable.dto';
|
||||
import { AgentDTO } from 'src/engine/metadata-modules/agent/dtos/agent.dto';
|
||||
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
import { ApplicationVariableEntityDTO } from 'src/engine/core-modules/applicationVariable/dtos/application-variable.dto';
|
||||
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
||||
|
||||
@ObjectType('Application')
|
||||
export class ApplicationDTO {
|
||||
@@ -38,4 +38,8 @@ export class ApplicationDTO {
|
||||
|
||||
@Field(() => [ApplicationVariableEntityDTO])
|
||||
applicationVariables: ApplicationVariableEntityDTO[];
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
universalIdentifier: string;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
|
||||
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||
import { MessagingFolderSyncManagerModule } from 'src/modules/messaging/message-folder-manager/messaging-folder-sync-manager.module';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
|
||||
import { TwoFactorAuthenticationMethodEntity } from '../two-factor-authentication/entities/two-factor-authentication-method.entity';
|
||||
import { TwoFactorAuthenticationModule } from '../two-factor-authentication/two-factor-authentication.module';
|
||||
@@ -113,6 +114,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
AuditModule,
|
||||
SubdomainManagerModule,
|
||||
DomainServerConfigModule,
|
||||
ApplicationModule,
|
||||
],
|
||||
controllers: [
|
||||
GoogleAuthController,
|
||||
|
||||
-409
@@ -1,409 +0,0 @@
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import { type AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import {
|
||||
type AuthProviderWithPasswordType,
|
||||
type ExistingUserOrPartialUserWithPicture,
|
||||
type SignInUpBaseParams,
|
||||
} from 'src/engine/core-modules/auth/types/signInUp.type';
|
||||
import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { type UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
|
||||
jest.mock('src/utils/image', () => {
|
||||
return {
|
||||
getImageBufferFromUrl: () => Promise.resolve(Buffer.from('')),
|
||||
};
|
||||
});
|
||||
|
||||
describe('SignInUpService', () => {
|
||||
let service: SignInUpService;
|
||||
let UserRepository: Repository<UserEntity>;
|
||||
let WorkspaceRepository: Repository<WorkspaceEntity>;
|
||||
let workspaceInvitationService: WorkspaceInvitationService;
|
||||
let userWorkspaceService: UserWorkspaceService;
|
||||
let twentyConfigService: TwentyConfigService;
|
||||
let subdomainManagerService: SubdomainManagerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SignInUpService,
|
||||
{
|
||||
provide: getRepositoryToken(UserEntity),
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(WorkspaceEntity),
|
||||
useValue: {
|
||||
save: jest.fn(),
|
||||
create: jest.fn(),
|
||||
get: jest.fn(),
|
||||
count: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: FileUploadService,
|
||||
useValue: {
|
||||
uploadImage: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceInvitationService,
|
||||
useValue: {
|
||||
validatePersonalInvitation: jest.fn(),
|
||||
invalidateWorkspaceInvitation: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserWorkspaceService,
|
||||
useValue: {
|
||||
addUserToWorkspaceIfUserNotInWorkspace: jest.fn(),
|
||||
checkUserWorkspaceExists: jest.fn(),
|
||||
create: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: OnboardingService,
|
||||
useValue: {
|
||||
setOnboardingConnectAccountPending: jest.fn(),
|
||||
setOnboardingInviteTeamPending: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: HttpService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: LoginTokenService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: TwentyConfigService,
|
||||
useValue: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
markEmailAsVerified: jest.fn().mockReturnValue({
|
||||
id: 'test-user-id',
|
||||
email: 'test@test.com',
|
||||
isEmailVerified: true,
|
||||
} as UserEntity),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SubdomainManagerService,
|
||||
useValue: {
|
||||
generateSubdomain: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserRoleService,
|
||||
useValue: {
|
||||
assignRoleToUserWorkspace: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: FeatureFlagService,
|
||||
useValue: {
|
||||
isFeatureEnabled: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceEventEmitter,
|
||||
useValue: {
|
||||
emitCustomBatchEvent: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: MetricsService,
|
||||
useValue: {
|
||||
incrementCounter: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SignInUpService>(SignInUpService);
|
||||
UserRepository = module.get(getRepositoryToken(UserEntity));
|
||||
WorkspaceRepository = module.get(getRepositoryToken(WorkspaceEntity));
|
||||
workspaceInvitationService = module.get<WorkspaceInvitationService>(
|
||||
WorkspaceInvitationService,
|
||||
);
|
||||
userWorkspaceService =
|
||||
module.get<UserWorkspaceService>(UserWorkspaceService);
|
||||
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
|
||||
subdomainManagerService = module.get<SubdomainManagerService>(
|
||||
SubdomainManagerService,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle signInUp with valid personal invitation', async () => {
|
||||
const params: SignInUpBaseParams &
|
||||
ExistingUserOrPartialUserWithPicture &
|
||||
AuthProviderWithPasswordType = {
|
||||
invitation: { value: 'invitationToken' } as AppTokenEntity,
|
||||
workspace: {
|
||||
id: 'workspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
} as WorkspaceEntity,
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: { email: 'test@example.com' } as UserEntity,
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(workspaceInvitationService, 'validatePersonalInvitation')
|
||||
.mockResolvedValue({
|
||||
isValid: true,
|
||||
workspace: params.workspace as WorkspaceEntity,
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(workspaceInvitationService, 'invalidateWorkspaceInvitation')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
jest
|
||||
.spyOn(userWorkspaceService, 'addUserToWorkspaceIfUserNotInWorkspace')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.signInUp(params);
|
||||
|
||||
expect(result.workspace).toEqual(params.workspace);
|
||||
expect(result.user).toBeDefined();
|
||||
expect(
|
||||
workspaceInvitationService.validatePersonalInvitation,
|
||||
).toHaveBeenCalledWith({
|
||||
workspacePersonalInviteToken: 'invitationToken',
|
||||
email: 'test@example.com',
|
||||
});
|
||||
expect(
|
||||
workspaceInvitationService.invalidateWorkspaceInvitation,
|
||||
).toHaveBeenCalledWith(
|
||||
(params.workspace as WorkspaceEntity).id,
|
||||
'test@example.com',
|
||||
);
|
||||
expect(
|
||||
userWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace,
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle signInUp on existing workspace without invitation', async () => {
|
||||
const params: SignInUpBaseParams &
|
||||
ExistingUserOrPartialUserWithPicture &
|
||||
AuthProviderWithPasswordType = {
|
||||
workspace: {
|
||||
id: 'workspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
} as WorkspaceEntity,
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: { email: 'test@example.com' } as UserEntity,
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(userWorkspaceService, 'addUserToWorkspaceIfUserNotInWorkspace')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.signInUp(params);
|
||||
|
||||
expect(result.workspace).toEqual(params.workspace);
|
||||
expect(result.user).toBeDefined();
|
||||
expect(
|
||||
userWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace,
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle signUp on new workspace for a new user', async () => {
|
||||
const params: SignInUpBaseParams &
|
||||
ExistingUserOrPartialUserWithPicture &
|
||||
AuthProviderWithPasswordType = {
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'newUserWithPicture',
|
||||
newUserWithPicture: {
|
||||
email: 'newuser@example.com',
|
||||
picture: 'pictureUrl',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue(false);
|
||||
jest.spyOn(WorkspaceRepository, 'count').mockResolvedValue(0);
|
||||
jest
|
||||
.spyOn(WorkspaceRepository, 'create')
|
||||
.mockReturnValue({} as WorkspaceEntity);
|
||||
jest.spyOn(WorkspaceRepository, 'save').mockResolvedValue({
|
||||
id: 'newWorkspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
} as WorkspaceEntity);
|
||||
jest.spyOn(UserRepository, 'create').mockReturnValue({} as UserEntity);
|
||||
jest
|
||||
.spyOn(subdomainManagerService, 'generateSubdomain')
|
||||
.mockResolvedValue('a-subdomain');
|
||||
jest
|
||||
.spyOn(UserRepository, 'save')
|
||||
|
||||
.mockResolvedValue({ id: 'newUserId' } as UserEntity);
|
||||
jest
|
||||
.spyOn(userWorkspaceService, 'create')
|
||||
.mockResolvedValue({} as UserWorkspaceEntity);
|
||||
|
||||
const result = await service.signInUp(params);
|
||||
|
||||
expect(result.workspace).toBeDefined();
|
||||
expect(result.user).toBeDefined();
|
||||
expect(WorkspaceRepository.create).toHaveBeenCalled();
|
||||
expect(WorkspaceRepository.save).toHaveBeenCalled();
|
||||
expect(UserRepository.create).toHaveBeenCalled();
|
||||
expect(UserRepository.save).toHaveBeenCalled();
|
||||
expect(userWorkspaceService.create).toHaveBeenCalledWith({
|
||||
workspaceId: 'newWorkspaceId',
|
||||
userId: 'newUserId',
|
||||
isExistingUser: false,
|
||||
pictureUrl: 'pictureUrl',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle signIn on workspace in pending state', async () => {
|
||||
const params: SignInUpBaseParams &
|
||||
ExistingUserOrPartialUserWithPicture &
|
||||
AuthProviderWithPasswordType = {
|
||||
workspace: {
|
||||
id: 'workspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
|
||||
} as WorkspaceEntity,
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: { email: 'test@example.com' } as UserEntity,
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue(false);
|
||||
jest
|
||||
.spyOn(userWorkspaceService, 'addUserToWorkspaceIfUserNotInWorkspace')
|
||||
.mockResolvedValue(undefined);
|
||||
jest
|
||||
.spyOn(userWorkspaceService, 'checkUserWorkspaceExists')
|
||||
.mockResolvedValue({} as UserWorkspaceEntity);
|
||||
|
||||
const result = await service.signInUp(params);
|
||||
|
||||
expect(result.workspace).toEqual(params.workspace);
|
||||
expect(result.user).toBeDefined();
|
||||
expect(
|
||||
userWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace,
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw - handle signUp on workspace in pending state', async () => {
|
||||
const params: SignInUpBaseParams &
|
||||
ExistingUserOrPartialUserWithPicture &
|
||||
AuthProviderWithPasswordType = {
|
||||
workspace: {
|
||||
id: 'workspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
|
||||
} as WorkspaceEntity,
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: { email: 'test@example.com' } as UserEntity,
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue(false);
|
||||
jest
|
||||
.spyOn(userWorkspaceService, 'checkUserWorkspaceExists')
|
||||
.mockResolvedValue(null);
|
||||
|
||||
await expect(() => service.signInUp(params)).rejects.toThrow(
|
||||
new AuthException(
|
||||
'User is not part of the workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle signup for existing user on new workspace', async () => {
|
||||
const params: SignInUpBaseParams &
|
||||
ExistingUserOrPartialUserWithPicture &
|
||||
AuthProviderWithPasswordType = {
|
||||
workspace: null,
|
||||
authParams: {
|
||||
provider: AuthProviderEnum.Password,
|
||||
password: 'validPassword',
|
||||
},
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: { email: 'existinguser@example.com' } as UserEntity,
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue(false);
|
||||
jest.spyOn(WorkspaceRepository, 'count').mockResolvedValue(0);
|
||||
jest
|
||||
.spyOn(WorkspaceRepository, 'create')
|
||||
.mockReturnValue({} as WorkspaceEntity);
|
||||
jest.spyOn(WorkspaceRepository, 'save').mockResolvedValue({
|
||||
id: 'newWorkspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
|
||||
} as WorkspaceEntity);
|
||||
jest.spyOn(userWorkspaceService, 'create').mockResolvedValue({} as any);
|
||||
|
||||
const result = await service.signInUp(params);
|
||||
|
||||
expect(result.workspace).toBeDefined();
|
||||
expect(result.user).toBeDefined();
|
||||
expect(WorkspaceRepository.create).toHaveBeenCalled();
|
||||
expect(WorkspaceRepository.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+114
-46
@@ -1,15 +1,16 @@
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { TWENTY_ICONS_BASE_URL } from 'twenty-shared/constants';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Repository } from 'typeorm';
|
||||
import { type DataSource, type QueryRunner, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
|
||||
import { type AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
@@ -38,6 +39,7 @@ import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-in
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
import { computeWorkspaceCustomCreateApplicationInput } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/compute-workspace-custom-create-application-input';
|
||||
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
|
||||
import { isWorkEmail } from 'src/utils/is-work-email';
|
||||
|
||||
@@ -58,6 +60,9 @@ export class SignInUpService {
|
||||
private readonly subdomainManagerService: SubdomainManagerService,
|
||||
private readonly userService: UserService,
|
||||
private readonly metricsService: MetricsService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
@InjectDataSource()
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
async computePartialUserFromUserPayload(
|
||||
@@ -263,7 +268,10 @@ export class SignInUpService {
|
||||
canImpersonate: false,
|
||||
});
|
||||
|
||||
await this.activateOnboardingForUser(user, params.workspace);
|
||||
await this.activateOnboardingForUser({
|
||||
user,
|
||||
workspace: params.workspace,
|
||||
});
|
||||
|
||||
await this.userWorkspaceService.addUserToWorkspaceIfUserNotInWorkspace(
|
||||
user,
|
||||
@@ -289,21 +297,33 @@ export class SignInUpService {
|
||||
}
|
||||
|
||||
private async activateOnboardingForUser(
|
||||
user: UserEntity,
|
||||
workspace: WorkspaceEntity,
|
||||
{
|
||||
user,
|
||||
workspace,
|
||||
}: {
|
||||
user: UserEntity;
|
||||
workspace: WorkspaceEntity;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
await this.onboardingService.setOnboardingConnectAccountPending({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
value: true,
|
||||
});
|
||||
|
||||
if (user.firstName === '' && user.lastName === '') {
|
||||
await this.onboardingService.setOnboardingCreateProfilePending({
|
||||
await this.onboardingService.setOnboardingConnectAccountPending(
|
||||
{
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
value: true,
|
||||
});
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
if (user.firstName === '' && user.lastName === '') {
|
||||
await this.onboardingService.setOnboardingCreateProfilePending(
|
||||
{
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
value: true,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +336,7 @@ export class SignInUpService {
|
||||
canImpersonate: boolean;
|
||||
canAccessFullAdminPanel: boolean;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const userCreated = this.userRepository.create({
|
||||
...newUserWithPicture,
|
||||
@@ -323,7 +344,9 @@ export class SignInUpService {
|
||||
canAccessFullAdminPanel,
|
||||
});
|
||||
|
||||
const savedUser = await this.userRepository.save(userCreated);
|
||||
const savedUser = queryRunner
|
||||
? await queryRunner.manager.save(UserEntity, userCreated)
|
||||
: await this.userRepository.save(userCreated);
|
||||
|
||||
const serverUrl = this.twentyConfigService.get('SERVER_URL');
|
||||
|
||||
@@ -435,43 +458,88 @@ export class SignInUpService {
|
||||
const logo =
|
||||
isWorkEmailFound && (await isLogoUrlValid()) ? logoUrl : undefined;
|
||||
|
||||
const workspaceToCreate = this.workspaceRepository.create({
|
||||
subdomain: await this.subdomainManagerService.generateSubdomain(
|
||||
isWorkEmailFound ? { userEmail: email } : {},
|
||||
),
|
||||
displayName: '',
|
||||
inviteHash: v4(),
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
|
||||
logo,
|
||||
});
|
||||
const workspaceId = v4();
|
||||
const workspaceCustomApplicationCreateInput =
|
||||
computeWorkspaceCustomCreateApplicationInput({
|
||||
workspace: {
|
||||
id: workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
const workspace = await this.workspaceRepository.save(workspaceToCreate);
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
const isExistingUser = userData.type === 'existingUser';
|
||||
const user = isExistingUser
|
||||
? userData.existingUser
|
||||
: await this.saveNewUser(userData.newUserWithPicture, {
|
||||
canImpersonate,
|
||||
canAccessFullAdminPanel,
|
||||
});
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
await this.userWorkspaceService.create({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
isExistingUser,
|
||||
pictureUrl: isExistingUser
|
||||
? undefined
|
||||
: userData.newUserWithPicture.picture,
|
||||
});
|
||||
try {
|
||||
const workspaceCustomApplication = await this.applicationService.create(
|
||||
{
|
||||
...workspaceCustomApplicationCreateInput,
|
||||
serverlessFunctionLayerId: null,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.activateOnboardingForUser(user, workspace);
|
||||
const workspaceToCreate = this.workspaceRepository.create({
|
||||
id: workspaceId,
|
||||
subdomain: await this.subdomainManagerService.generateSubdomain(
|
||||
isWorkEmailFound ? { userEmail: email } : {},
|
||||
),
|
||||
workspaceCustomApplicationId: workspaceCustomApplication.id,
|
||||
displayName: '',
|
||||
inviteHash: v4(),
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION,
|
||||
logo,
|
||||
});
|
||||
|
||||
await this.onboardingService.setOnboardingInviteTeamPending({
|
||||
workspaceId: workspace.id,
|
||||
value: true,
|
||||
});
|
||||
const workspace = await queryRunner.manager.save(
|
||||
WorkspaceEntity,
|
||||
workspaceToCreate,
|
||||
);
|
||||
|
||||
return { user, workspace };
|
||||
const isExistingUser = userData.type === 'existingUser';
|
||||
const user = isExistingUser
|
||||
? userData.existingUser
|
||||
: await this.saveNewUser(
|
||||
userData.newUserWithPicture,
|
||||
{
|
||||
canImpersonate,
|
||||
canAccessFullAdminPanel,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.userWorkspaceService.create(
|
||||
{
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
isExistingUser,
|
||||
pictureUrl: isExistingUser
|
||||
? undefined
|
||||
: userData.newUserWithPicture.picture,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.activateOnboardingForUser({ user, workspace }, queryRunner);
|
||||
|
||||
await this.onboardingService.setOnboardingInviteTeamPending(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
value: true,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return { user, workspace };
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
async signUpWithoutWorkspace(
|
||||
|
||||
+54
-31
@@ -1,6 +1,6 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { type QueryRunner, IsNull, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
KeyValuePairEntity,
|
||||
@@ -50,19 +50,22 @@ export class KeyValuePairService<
|
||||
}));
|
||||
}
|
||||
|
||||
async set<K extends keyof KeyValueTypesMap>({
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
value,
|
||||
type,
|
||||
}: {
|
||||
userId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
key: Extract<K, string>;
|
||||
value: KeyValueTypesMap[K];
|
||||
type: KeyValuePairType;
|
||||
}) {
|
||||
async set<K extends keyof KeyValueTypesMap>(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
value,
|
||||
type,
|
||||
}: {
|
||||
userId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
key: Extract<K, string>;
|
||||
value: KeyValueTypesMap[K];
|
||||
type: KeyValuePairType;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const upsertData = {
|
||||
userId,
|
||||
workspaceId,
|
||||
@@ -84,24 +87,36 @@ export class KeyValuePairService<
|
||||
? '"workspaceId" is NULL'
|
||||
: undefined;
|
||||
|
||||
await this.keyValuePairRepository.upsert(upsertData, {
|
||||
conflictPaths,
|
||||
indexPredicate,
|
||||
});
|
||||
if (queryRunner) {
|
||||
await queryRunner.manager
|
||||
.getRepository(KeyValuePairEntity)
|
||||
.upsert(upsertData, {
|
||||
conflictPaths,
|
||||
indexPredicate,
|
||||
});
|
||||
} else {
|
||||
await this.keyValuePairRepository.upsert(upsertData, {
|
||||
conflictPaths,
|
||||
indexPredicate,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async delete({
|
||||
userId,
|
||||
workspaceId,
|
||||
type,
|
||||
key,
|
||||
}: {
|
||||
userId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
type: KeyValuePairType;
|
||||
key: Extract<keyof KeyValueTypesMap, string>;
|
||||
}) {
|
||||
await this.keyValuePairRepository.delete({
|
||||
async delete(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
type,
|
||||
key,
|
||||
}: {
|
||||
userId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
type: KeyValuePairType;
|
||||
key: Extract<keyof KeyValueTypesMap, string>;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const deleteConditions = {
|
||||
...(userId === undefined
|
||||
? {}
|
||||
: userId === null
|
||||
@@ -114,6 +129,14 @@ export class KeyValuePairService<
|
||||
: { workspaceId }),
|
||||
type,
|
||||
key,
|
||||
});
|
||||
};
|
||||
|
||||
if (queryRunner) {
|
||||
await queryRunner.manager
|
||||
.getRepository(KeyValuePairEntity)
|
||||
.delete(deleteConditions);
|
||||
} else {
|
||||
await this.keyValuePairRepository.delete(deleteConditions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
|
||||
@@ -107,81 +108,108 @@ export class OnboardingService {
|
||||
return OnboardingStatus.COMPLETED;
|
||||
}
|
||||
|
||||
async setOnboardingConnectAccountPending({
|
||||
userId,
|
||||
workspaceId,
|
||||
value,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
value: boolean;
|
||||
}) {
|
||||
async setOnboardingConnectAccountPending(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
value,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
value: boolean;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
if (!value) {
|
||||
await this.userVarsService.delete({
|
||||
userId,
|
||||
workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_PENDING,
|
||||
});
|
||||
await this.userVarsService.delete(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_PENDING,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.userVarsService.set({
|
||||
userId,
|
||||
workspaceId: workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_PENDING,
|
||||
value: true,
|
||||
});
|
||||
await this.userVarsService.set(
|
||||
{
|
||||
userId,
|
||||
workspaceId: workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_PENDING,
|
||||
value: true,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
async setOnboardingInviteTeamPending({
|
||||
workspaceId,
|
||||
value,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
value: boolean;
|
||||
}) {
|
||||
async setOnboardingInviteTeamPending(
|
||||
{
|
||||
workspaceId,
|
||||
value,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
value: boolean;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
if (!value) {
|
||||
await this.userVarsService.delete({
|
||||
await this.userVarsService.delete(
|
||||
{
|
||||
workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_INVITE_TEAM_PENDING,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.userVarsService.set(
|
||||
{
|
||||
workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_INVITE_TEAM_PENDING,
|
||||
});
|
||||
value: true,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
async setOnboardingCreateProfilePending(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
value,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
value: boolean;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
if (!value) {
|
||||
await this.userVarsService.delete(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_PENDING,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.userVarsService.set({
|
||||
workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_INVITE_TEAM_PENDING,
|
||||
value: true,
|
||||
});
|
||||
}
|
||||
|
||||
async setOnboardingCreateProfilePending({
|
||||
userId,
|
||||
workspaceId,
|
||||
value,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
value: boolean;
|
||||
}) {
|
||||
if (!value) {
|
||||
await this.userVarsService.delete({
|
||||
await this.userVarsService.set(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_PENDING,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.userVarsService.set({
|
||||
userId,
|
||||
workspaceId,
|
||||
key: OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_PENDING,
|
||||
value: true,
|
||||
});
|
||||
value: true,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
async setOnboardingBookOnboardingPending({
|
||||
|
||||
+18
-11
@@ -420,23 +420,30 @@ describe('UserWorkspaceService', () => {
|
||||
user.id,
|
||||
workspace.id,
|
||||
);
|
||||
expect(service.create).toHaveBeenCalledWith({
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
isExistingUser: true,
|
||||
});
|
||||
expect(service.create).toHaveBeenCalled();
|
||||
expect(service.create).toHaveBeenCalledWith(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
isExistingUser: true,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
expect(service.createWorkspaceMember).toHaveBeenCalledWith(
|
||||
workspace.id,
|
||||
user,
|
||||
);
|
||||
expect(userRoleService.assignRoleToUserWorkspace).toHaveBeenCalledWith({
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId: userWorkspace.id,
|
||||
roleId: workspace.defaultRoleId,
|
||||
});
|
||||
expect(userRoleService.assignRoleToUserWorkspace).toHaveBeenCalledWith(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId: userWorkspace.id,
|
||||
roleId: workspace.defaultRoleId,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
expect(
|
||||
workspaceInvitationService.invalidateWorkspaceInvitation,
|
||||
).toHaveBeenCalledWith(workspace.id, user.email);
|
||||
).toHaveBeenCalledWith(workspace.id, user.email, undefined);
|
||||
});
|
||||
|
||||
it('should not add user to workspace if already in workspace', async () => {
|
||||
|
||||
+36
-23
@@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
import { type QueryRunner, IsNull, Not, Repository } from 'typeorm';
|
||||
|
||||
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
@@ -57,17 +57,20 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
|
||||
super(userWorkspaceRepository);
|
||||
}
|
||||
|
||||
async create({
|
||||
userId,
|
||||
workspaceId,
|
||||
isExistingUser,
|
||||
pictureUrl,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
isExistingUser: boolean;
|
||||
pictureUrl?: string;
|
||||
}): Promise<UserWorkspaceEntity> {
|
||||
async create(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
isExistingUser,
|
||||
pictureUrl,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
isExistingUser: boolean;
|
||||
pictureUrl?: string;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
): Promise<UserWorkspaceEntity> {
|
||||
const defaultAvatarUrl = await this.computeDefaultAvatarUrl(
|
||||
userId,
|
||||
workspaceId,
|
||||
@@ -81,7 +84,9 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
|
||||
defaultAvatarUrl,
|
||||
});
|
||||
|
||||
return this.userWorkspaceRepository.save(userWorkspace);
|
||||
return queryRunner
|
||||
? queryRunner.manager.save(UserWorkspaceEntity, userWorkspace)
|
||||
: this.userWorkspaceRepository.save(userWorkspace);
|
||||
}
|
||||
|
||||
async createWorkspaceMember(workspaceId: string, user: UserEntity) {
|
||||
@@ -126,6 +131,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
|
||||
async addUserToWorkspaceIfUserNotInWorkspace(
|
||||
user: UserEntity,
|
||||
workspace: WorkspaceEntity,
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
let userWorkspace = await this.checkUserWorkspaceExists(
|
||||
user.id,
|
||||
@@ -133,11 +139,14 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
|
||||
);
|
||||
|
||||
if (!userWorkspace) {
|
||||
userWorkspace = await this.create({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
isExistingUser: true,
|
||||
});
|
||||
userWorkspace = await this.create(
|
||||
{
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
isExistingUser: true,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.createWorkspaceMember(workspace.id, user);
|
||||
|
||||
@@ -150,15 +159,19 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
|
||||
);
|
||||
}
|
||||
|
||||
await this.userRoleService.assignRoleToUserWorkspace({
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId: userWorkspace.id,
|
||||
roleId: defaultRoleId,
|
||||
});
|
||||
await this.userRoleService.assignRoleToUserWorkspace(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId: userWorkspace.id,
|
||||
roleId: defaultRoleId,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.workspaceInvitationService.invalidateWorkspaceInvitation(
|
||||
workspace.id,
|
||||
user.email,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { type Repository, type UpdateResult } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { type UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
@@ -43,6 +44,7 @@ describe('UserService', () => {
|
||||
findOne: jest.fn(),
|
||||
save: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -67,6 +69,10 @@ describe('UserService', () => {
|
||||
deleteUserWorkspace: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ApplicationService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { msg } from '@lingui/core/macro';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
import { type QueryRunner, IsNull, Not, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
@@ -275,11 +275,13 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
|
||||
return user;
|
||||
}
|
||||
|
||||
async markEmailAsVerified(userId: string) {
|
||||
async markEmailAsVerified(userId: string, queryRunner?: QueryRunner) {
|
||||
const user = await this.findUserByIdOrThrow(userId);
|
||||
|
||||
user.isEmailVerified = true;
|
||||
|
||||
return await this.userRepository.save(user);
|
||||
return queryRunner
|
||||
? await queryRunner.manager.save(UserEntity, user)
|
||||
: await this.userRepository.save(user);
|
||||
}
|
||||
}
|
||||
|
||||
+46
-32
@@ -1,5 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { KeyValuePairType } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { KeyValuePairService } from 'src/engine/core-modules/key-value-pair/key-value-pair.service';
|
||||
import { mergeUserVars } from 'src/engine/core-modules/user/user-vars/utils/merge-user-vars.util';
|
||||
@@ -126,40 +128,52 @@ export class UserVarsService<
|
||||
return mergeUserVars<Extract<keyof KeyValueTypesMap, string>>(result);
|
||||
}
|
||||
|
||||
set<K extends keyof KeyValueTypesMap>({
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
value,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
key: Extract<K, string>;
|
||||
value: KeyValueTypesMap[K];
|
||||
}) {
|
||||
return this.keyValuePairService.set({
|
||||
userId,
|
||||
workspaceId,
|
||||
key: key,
|
||||
value,
|
||||
type: KeyValuePairType.USER_VARIABLE,
|
||||
});
|
||||
}
|
||||
|
||||
async delete({
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
key: Extract<keyof KeyValueTypesMap, string>;
|
||||
}) {
|
||||
return this.keyValuePairService.delete({
|
||||
set<K extends keyof KeyValueTypesMap>(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
type: KeyValuePairType.USER_VARIABLE,
|
||||
});
|
||||
value,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
key: Extract<K, string>;
|
||||
value: KeyValueTypesMap[K];
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
return this.keyValuePairService.set(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key: key,
|
||||
value,
|
||||
type: KeyValuePairType.USER_VARIABLE,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
async delete(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
key: Extract<keyof KeyValueTypesMap, string>;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
return this.keyValuePairService.delete(
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
type: KeyValuePairType.USER_VARIABLE,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+16
-4
@@ -9,8 +9,8 @@ import { addMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
import { SendInviteLinkEmail } from 'twenty-emails';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { getAppPath, isDefined } from 'twenty-shared/utils';
|
||||
import { type QueryRunner, IsNull, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
AppTokenEntity,
|
||||
@@ -203,10 +203,22 @@ export class WorkspaceInvitationService {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
async invalidateWorkspaceInvitation(workspaceId: string, email: string) {
|
||||
async invalidateWorkspaceInvitation(
|
||||
workspaceId: string,
|
||||
email: string,
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const appToken = await this.getOneWorkspaceInvitation(workspaceId, email);
|
||||
|
||||
if (appToken) {
|
||||
if (!isDefined(appToken)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (queryRunner) {
|
||||
await queryRunner.manager
|
||||
.getRepository(AppTokenEntity)
|
||||
.delete(appToken.id);
|
||||
} else {
|
||||
await this.appTokenRepository.delete(appToken.id);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -247,7 +247,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
);
|
||||
|
||||
await this.workspaceManagerService.init({
|
||||
workspaceId: workspace.id,
|
||||
workspace,
|
||||
userId: user.id,
|
||||
});
|
||||
await this.userWorkspaceService.createWorkspaceMember(workspace.id, user);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import { Application } from 'cloudflare/resources/zero-trust/access/applications/applications';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import {
|
||||
Check,
|
||||
@@ -9,6 +10,8 @@ import {
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
Relation,
|
||||
@@ -19,6 +22,7 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
|
||||
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApprovedAccessDomainEntity } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
|
||||
import { EmailingDomainEntity } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
@@ -272,4 +276,22 @@ export class WorkspaceEntity {
|
||||
@Field(() => String, { nullable: false })
|
||||
@Column({ type: 'varchar', nullable: false, default: 'auto' })
|
||||
routerModel: ModelId;
|
||||
|
||||
// TODO prastoin
|
||||
// Temporarily setting as nullable for retro compatibility, not udpating TypeScript types
|
||||
@Field(() => String, { nullable: true })
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
workspaceCustomApplicationId: string;
|
||||
|
||||
@ManyToOne(() => ApplicationEntity, {
|
||||
onDelete: 'RESTRICT',
|
||||
nullable: false,
|
||||
})
|
||||
@JoinColumn({ name: 'workspaceCustomApplicationId' })
|
||||
workspaceCustomApplication: Relation<ApplicationEntity>;
|
||||
|
||||
@OneToMany(() => ApplicationEntity, (application) => application.workspace, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
applications: Relation<Application[]>;
|
||||
}
|
||||
|
||||
@@ -292,6 +292,11 @@ export class WorkspaceResolver {
|
||||
);
|
||||
}
|
||||
|
||||
@ResolveField(() => String)
|
||||
workspaceCustomApplicationId(@Parent() workspace: WorkspaceEntity) {
|
||||
return workspace.workspaceCustomApplicationId;
|
||||
}
|
||||
|
||||
@ResolveField(() => Boolean)
|
||||
isMicrosoftAuthEnabled(@Parent() workspace: WorkspaceEntity) {
|
||||
return (
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
@@ -17,7 +17,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([RouteTriggerEntity]),
|
||||
AuthModule,
|
||||
TokenModule,
|
||||
WorkspaceDomainsModule,
|
||||
ServerlessFunctionModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Not, Repository } from 'typeorm';
|
||||
import { type QueryRunner, In, Not, Repository } from 'typeorm';
|
||||
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import {
|
||||
@@ -29,15 +29,18 @@ export class UserRoleService {
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
) {}
|
||||
|
||||
public async assignRoleToUserWorkspace({
|
||||
workspaceId,
|
||||
userWorkspaceId,
|
||||
roleId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
userWorkspaceId: string;
|
||||
roleId: string;
|
||||
}): Promise<void> {
|
||||
public async assignRoleToUserWorkspace(
|
||||
{
|
||||
workspaceId,
|
||||
userWorkspaceId,
|
||||
roleId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
userWorkspaceId: string;
|
||||
roleId: string;
|
||||
},
|
||||
queryRunner?: QueryRunner,
|
||||
): Promise<void> {
|
||||
const validationResult = await this.validateAssignRoleInput({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
@@ -48,13 +51,17 @@ export class UserRoleService {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRoleTarget = await this.roleTargetsRepository.save({
|
||||
const roleTargetsRepo = queryRunner
|
||||
? queryRunner.manager.getRepository(RoleTargetsEntity)
|
||||
: this.roleTargetsRepository;
|
||||
|
||||
const newRoleTarget = await roleTargetsRepo.save({
|
||||
roleId,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.roleTargetsRepository.delete({
|
||||
await roleTargetsRepo.delete({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
id: Not(newRoleTarget.id),
|
||||
|
||||
+5
@@ -3,6 +3,7 @@ import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { type DataSource, type Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -104,6 +105,10 @@ describe('WorkspaceManagerService', () => {
|
||||
provide: RoleService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: ApplicationService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: UserRoleService,
|
||||
useValue: {},
|
||||
|
||||
+13
-7
@@ -1,13 +1,19 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
const tableName = 'billingCustomer';
|
||||
|
||||
export const seedBillingCustomers = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await dataSource
|
||||
type SeedBillingCustomersArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const seedBillingCustomers = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedBillingCustomersArgs) => {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, ['workspaceId', 'stripeCustomerId'])
|
||||
|
||||
+13
-7
@@ -1,13 +1,19 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
const tableName = 'billingSubscription';
|
||||
|
||||
export const seedBillingSubscriptions = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await dataSource
|
||||
type SeedBillingSubscriptionsArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const seedBillingSubscriptions = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedBillingSubscriptionsArgs) => {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, [
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
export const WORKSPACE_FIELDS_TO_SEED = [
|
||||
'id',
|
||||
'displayName',
|
||||
'subdomain',
|
||||
'inviteHash',
|
||||
'logo',
|
||||
'activationStatus',
|
||||
'isTwoFactorAuthenticationEnforced',
|
||||
'version',
|
||||
'workspaceCustomApplicationId',
|
||||
] as const satisfies (keyof WorkspaceEntity)[];
|
||||
|
||||
export type CreateWorkspaceInput = Pick<
|
||||
WorkspaceEntity,
|
||||
(typeof WORKSPACE_FIELDS_TO_SEED)[number]
|
||||
>;
|
||||
|
||||
export const SEED_APPLE_WORKSPACE_ID = '20202020-1c25-4d02-bf25-6aeccf7ea419';
|
||||
export const SEED_YCOMBINATOR_WORKSPACE_ID =
|
||||
'3b8e6458-5fc1-4e63-8563-008ccddaa6db';
|
||||
|
||||
export type SeededWorkspacesIds =
|
||||
| typeof SEED_APPLE_WORKSPACE_ID
|
||||
| typeof SEED_YCOMBINATOR_WORKSPACE_ID;
|
||||
|
||||
export const SEEDER_CREATE_WORKSPACE_INPUT = {
|
||||
[SEED_APPLE_WORKSPACE_ID]: {
|
||||
id: SEED_APPLE_WORKSPACE_ID,
|
||||
displayName: 'Apple',
|
||||
subdomain: 'apple',
|
||||
inviteHash: 'apple.dev-invite-hash',
|
||||
logo: 'https://twentyhq.github.io/placeholder-images/workspaces/apple-logo.png',
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION, // will be set to active after default role creation
|
||||
isTwoFactorAuthenticationEnforced: false,
|
||||
},
|
||||
[SEED_YCOMBINATOR_WORKSPACE_ID]: {
|
||||
id: SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
displayName: 'YCombinator',
|
||||
subdomain: 'yc',
|
||||
inviteHash: 'yc.dev-invite-hash',
|
||||
logo: 'https://twentyhq.github.io/placeholder-images/workspaces/ycombinator-logo.png',
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION, // will be set to active after default role creation
|
||||
isTwoFactorAuthenticationEnforced: false,
|
||||
},
|
||||
} as const satisfies Record<
|
||||
SeededWorkspacesIds,
|
||||
Omit<CreateWorkspaceInput, 'version' | 'workspaceCustomApplicationId'>
|
||||
>;
|
||||
+4
-4
@@ -12,14 +12,14 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import {
|
||||
RANDOM_USER_WORKSPACE_IDS,
|
||||
USER_WORKSPACE_DATA_SEED_IDS,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
import { API_KEY_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/api-key-data-seeds.constant';
|
||||
import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role';
|
||||
|
||||
|
||||
+53
-25
@@ -1,11 +1,11 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { AgentChatMessageRole } from 'src/engine/metadata-modules/agent/agent-chat-message.entity';
|
||||
import { USER_WORKSPACE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { USER_WORKSPACE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||
|
||||
const agentChatThreadTableName = 'agentChatThread';
|
||||
const agentChatMessageTableName = 'agentChatMessage';
|
||||
@@ -43,11 +43,17 @@ export const AGENT_CHAT_MESSAGE_PART_DATA_SEED_IDS = {
|
||||
YCOMBINATOR_MESSAGE_4_PART_1: '20202020-0000-4000-8000-000000000054',
|
||||
};
|
||||
|
||||
const seedChatThreads = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
type SeedChatThreadsArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
const seedChatThreads = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedChatThreadsArgs) => {
|
||||
let threadId: string;
|
||||
let userWorkspaceId: string;
|
||||
|
||||
@@ -65,7 +71,7 @@ const seedChatThreads = async (
|
||||
|
||||
const now = new Date();
|
||||
|
||||
await dataSource
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${agentChatThreadTableName}`, [
|
||||
@@ -88,12 +94,19 @@ const seedChatThreads = async (
|
||||
return threadId;
|
||||
};
|
||||
|
||||
const seedChatMessages = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
threadId: string,
|
||||
) => {
|
||||
type SeedChatMessagesArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
threadId: string;
|
||||
};
|
||||
|
||||
const seedChatMessages = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
threadId,
|
||||
}: SeedChatMessagesArgs) => {
|
||||
let messageIds: string[];
|
||||
let partIds: string[];
|
||||
let messages: Array<{
|
||||
@@ -274,7 +287,7 @@ const seedChatMessages = async (
|
||||
);
|
||||
}
|
||||
|
||||
await dataSource
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${agentChatMessageTableName}`, [
|
||||
@@ -287,7 +300,7 @@ const seedChatMessages = async (
|
||||
.values(messages)
|
||||
.execute();
|
||||
|
||||
await dataSource
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${agentChatMessagePartTableName}`, [
|
||||
@@ -303,12 +316,27 @@ const seedChatMessages = async (
|
||||
.execute();
|
||||
};
|
||||
|
||||
export const seedAgents = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
const threadId = await seedChatThreads(dataSource, schemaName, workspaceId);
|
||||
|
||||
await seedChatMessages(dataSource, schemaName, workspaceId, threadId);
|
||||
type SeedAgentsArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const seedAgents = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedAgentsArgs) => {
|
||||
const threadId = await seedChatThreads({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await seedChatMessages({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
threadId,
|
||||
});
|
||||
};
|
||||
|
||||
+13
-7
@@ -1,15 +1,21 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { API_KEY_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/api-key-data-seeds.constant';
|
||||
|
||||
const tableName = 'apiKey';
|
||||
|
||||
export const seedApiKeys = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await dataSource
|
||||
type SeedApiKeysArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const seedApiKeys = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedApiKeysArgs) => {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, [
|
||||
|
||||
+71
-18
@@ -1,18 +1,26 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
|
||||
import { type ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { seedBillingCustomers } from 'src/engine/workspace-manager/dev-seeder/core/billing/utils/seed-billing-customers.util';
|
||||
import { seedBillingSubscriptions } from 'src/engine/workspace-manager/dev-seeder/core/billing/utils/seed-billing-subscriptions.util';
|
||||
import {
|
||||
type SeededWorkspacesIds,
|
||||
SEEDER_CREATE_WORKSPACE_INPUT,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { seedAgents } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-agents.util';
|
||||
import { seedApiKeys } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-api-keys.util';
|
||||
import { seedFeatureFlags } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util';
|
||||
import { seedUserWorkspaces } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||
import { seedUsers } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-users.util';
|
||||
import { seedWorkspaces } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
import { createWorkspace } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspace.util';
|
||||
import { computeWorkspaceCustomCreateApplicationInput } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/compute-workspace-custom-create-application-input';
|
||||
import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version-major-minor-patch';
|
||||
|
||||
type SeedCoreSchemaArgs = {
|
||||
dataSource: DataSource;
|
||||
workspaceId: string;
|
||||
workspaceId: SeededWorkspacesIds;
|
||||
appVersion: string | undefined;
|
||||
applicationService: ApplicationService;
|
||||
seedBilling?: boolean;
|
||||
seedFeatureFlags?: boolean;
|
||||
};
|
||||
@@ -21,30 +29,75 @@ export const seedCoreSchema = async ({
|
||||
appVersion,
|
||||
dataSource,
|
||||
workspaceId,
|
||||
applicationService,
|
||||
seedBilling = true,
|
||||
seedFeatureFlags: shouldSeedFeatureFlags = true,
|
||||
}: SeedCoreSchemaArgs) => {
|
||||
const schemaName = 'core';
|
||||
|
||||
await seedWorkspaces({
|
||||
dataSource,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
appVersion,
|
||||
});
|
||||
await seedUsers(dataSource, schemaName);
|
||||
await seedUserWorkspaces(dataSource, schemaName, workspaceId);
|
||||
const createWorkspaceStaticInput = SEEDER_CREATE_WORKSPACE_INPUT[workspaceId];
|
||||
const workspaceCustomApplicationCreateInput =
|
||||
computeWorkspaceCustomCreateApplicationInput({
|
||||
workspace: {
|
||||
id: workspaceId,
|
||||
displayName: createWorkspaceStaticInput.displayName,
|
||||
},
|
||||
});
|
||||
|
||||
await seedAgents(dataSource, schemaName, workspaceId);
|
||||
const version = extractVersionMajorMinorPatch(appVersion);
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
|
||||
await seedApiKeys(dataSource, schemaName, workspaceId);
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
if (shouldSeedFeatureFlags) {
|
||||
await seedFeatureFlags(dataSource, schemaName, workspaceId);
|
||||
}
|
||||
try {
|
||||
const customWorkspaceApplication = await applicationService.create(
|
||||
{
|
||||
...workspaceCustomApplicationCreateInput,
|
||||
serverlessFunctionLayerId: null,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
if (seedBilling) {
|
||||
await seedBillingCustomers(dataSource, schemaName, workspaceId);
|
||||
await seedBillingSubscriptions(dataSource, schemaName, workspaceId);
|
||||
await createWorkspace({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
createWorkspaceInput: {
|
||||
...createWorkspaceStaticInput,
|
||||
version,
|
||||
workspaceCustomApplicationId: customWorkspaceApplication.id,
|
||||
},
|
||||
});
|
||||
|
||||
await seedUsers({ queryRunner, schemaName });
|
||||
|
||||
await seedUserWorkspaces({ queryRunner, schemaName, workspaceId });
|
||||
|
||||
await applicationService.createTwentyStandardApplication(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await seedAgents({ queryRunner, schemaName, workspaceId });
|
||||
|
||||
await seedApiKeys({ queryRunner, schemaName, workspaceId });
|
||||
|
||||
if (shouldSeedFeatureFlags) {
|
||||
await seedFeatureFlags({ queryRunner, schemaName, workspaceId });
|
||||
}
|
||||
|
||||
if (seedBilling) {
|
||||
await seedBillingCustomers({ queryRunner, schemaName, workspaceId });
|
||||
await seedBillingSubscriptions({ queryRunner, schemaName, workspaceId });
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
};
|
||||
|
||||
+26
-14
@@ -1,16 +1,22 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
|
||||
const tableName = 'featureFlag';
|
||||
|
||||
export const seedFeatureFlags = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await dataSource
|
||||
type SeedFeatureFlagsArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const seedFeatureFlags = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedFeatureFlagsArgs) => {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, ['key', 'workspaceId', 'value'])
|
||||
@@ -90,12 +96,18 @@ export const seedFeatureFlags = async (
|
||||
.execute();
|
||||
};
|
||||
|
||||
export const deleteFeatureFlags = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await dataSource
|
||||
type DeleteFeatureFlagsArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const deleteFeatureFlags = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: DeleteFeatureFlagsArgs) => {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(`${schemaName}.${tableName}`)
|
||||
|
||||
+28
-16
@@ -1,12 +1,12 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { type UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { generateRandomUsers } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-random-users.util';
|
||||
import { USER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-users.util';
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { generateRandomUsers } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-random-users.util';
|
||||
import { USER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-users.util';
|
||||
|
||||
const tableName = 'userWorkspace';
|
||||
|
||||
@@ -28,11 +28,17 @@ const {
|
||||
|
||||
export const RANDOM_USER_WORKSPACE_IDS = randomUserWorkspaceIds;
|
||||
|
||||
export const seedUserWorkspaces = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
type SeedUserWorkspacesArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const seedUserWorkspaces = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedUserWorkspacesArgs) => {
|
||||
let userWorkspaces: Pick<
|
||||
UserWorkspaceEntity,
|
||||
'id' | 'userId' | 'workspaceId'
|
||||
@@ -89,7 +95,7 @@ export const seedUserWorkspaces = async (
|
||||
},
|
||||
];
|
||||
}
|
||||
await dataSource
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, ['id', 'userId', 'workspaceId'])
|
||||
@@ -98,12 +104,18 @@ export const seedUserWorkspaces = async (
|
||||
.execute();
|
||||
};
|
||||
|
||||
export const deleteUserWorkspaces = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await dataSource
|
||||
type DeleteUserWorkspacesArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const deleteUserWorkspaces = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: DeleteUserWorkspacesArgs) => {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(`${schemaName}.${tableName}`)
|
||||
|
||||
+8
-3
@@ -1,4 +1,4 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { generateRandomUsers } from './generate-random-users.util';
|
||||
|
||||
@@ -15,7 +15,12 @@ const { users: randomUsers, userIds: randomUserIds } = generateRandomUsers();
|
||||
|
||||
export const RANDOM_USER_IDS = randomUserIds;
|
||||
|
||||
export const seedUsers = async (dataSource: DataSource, schemaName: string) => {
|
||||
type SeedUsersArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
};
|
||||
|
||||
export const seedUsers = async ({ queryRunner, schemaName }: SeedUsersArgs) => {
|
||||
const originalUsers = [
|
||||
{
|
||||
id: USER_DATA_SEED_IDS.TIM,
|
||||
@@ -65,7 +70,7 @@ export const seedUsers = async (dataSource: DataSource, schemaName: string) => {
|
||||
|
||||
const allUsers = [...originalUsers, ...randomUsers];
|
||||
|
||||
await dataSource
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, [
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import {
|
||||
type CreateWorkspaceInput,
|
||||
WORKSPACE_FIELDS_TO_SEED,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
|
||||
const tableName = 'workspace';
|
||||
|
||||
export type SeedWorkspaceArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
createWorkspaceInput: CreateWorkspaceInput;
|
||||
};
|
||||
|
||||
export const createWorkspace = async ({
|
||||
schemaName,
|
||||
queryRunner,
|
||||
createWorkspaceInput,
|
||||
}: SeedWorkspaceArgs) => {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, WORKSPACE_FIELDS_TO_SEED)
|
||||
.orIgnore()
|
||||
.values(createWorkspaceInput)
|
||||
.execute();
|
||||
};
|
||||
|
||||
type DeleteWorkspacesArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const deleteWorkspaces = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: DeleteWorkspacesArgs) => {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(`${schemaName}.${tableName}`)
|
||||
.where(`${tableName}."id" = :id`, { id: workspaceId })
|
||||
.execute();
|
||||
};
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { type DataSource } from 'typeorm';
|
||||
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version-major-minor-patch';
|
||||
|
||||
const tableName = 'workspace';
|
||||
|
||||
export const SEED_APPLE_WORKSPACE_ID = '20202020-1c25-4d02-bf25-6aeccf7ea419';
|
||||
export const SEED_YCOMBINATOR_WORKSPACE_ID =
|
||||
'3b8e6458-5fc1-4e63-8563-008ccddaa6db';
|
||||
|
||||
export type SeedWorkspaceArgs = {
|
||||
dataSource: DataSource;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
appVersion: string | undefined;
|
||||
};
|
||||
|
||||
const workspaceSeederFields = [
|
||||
'id',
|
||||
'displayName',
|
||||
'subdomain',
|
||||
'inviteHash',
|
||||
'logo',
|
||||
'activationStatus',
|
||||
'version',
|
||||
'isTwoFactorAuthenticationEnforced',
|
||||
] as const satisfies (keyof WorkspaceEntity)[];
|
||||
|
||||
type WorkspaceSeederFields = Pick<
|
||||
WorkspaceEntity,
|
||||
(typeof workspaceSeederFields)[number]
|
||||
>;
|
||||
|
||||
export const seedWorkspaces = async ({
|
||||
schemaName,
|
||||
dataSource,
|
||||
workspaceId,
|
||||
appVersion,
|
||||
}: SeedWorkspaceArgs) => {
|
||||
const version = extractVersionMajorMinorPatch(appVersion);
|
||||
|
||||
const workspaces: Record<string, WorkspaceSeederFields> = {
|
||||
[SEED_APPLE_WORKSPACE_ID]: {
|
||||
id: SEED_APPLE_WORKSPACE_ID,
|
||||
displayName: 'Apple',
|
||||
subdomain: 'apple',
|
||||
inviteHash: 'apple.dev-invite-hash',
|
||||
logo: 'https://twentyhq.github.io/placeholder-images/workspaces/apple-logo.png',
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION, // will be set to active after default role creation
|
||||
version: version,
|
||||
isTwoFactorAuthenticationEnforced: false,
|
||||
},
|
||||
[SEED_YCOMBINATOR_WORKSPACE_ID]: {
|
||||
id: SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
displayName: 'YCombinator',
|
||||
subdomain: 'yc',
|
||||
inviteHash: 'yc.dev-invite-hash',
|
||||
logo: 'https://twentyhq.github.io/placeholder-images/workspaces/ycombinator-logo.png',
|
||||
activationStatus: WorkspaceActivationStatus.PENDING_CREATION, // will be set to active after default role creation
|
||||
version: version,
|
||||
isTwoFactorAuthenticationEnforced: false,
|
||||
},
|
||||
};
|
||||
|
||||
await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, workspaceSeederFields)
|
||||
.orIgnore()
|
||||
.values(workspaces[workspaceId])
|
||||
.execute();
|
||||
};
|
||||
|
||||
export const deleteWorkspaces = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
) => {
|
||||
await dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(`${schemaName}.${tableName}`)
|
||||
.where(`${tableName}."id" = :id`, { id: workspaceId })
|
||||
.execute();
|
||||
};
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import { generateRandomUsers } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-random-users.util';
|
||||
import { USER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-users.util';
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { generateRandomUsers } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-random-users.util';
|
||||
import { USER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-users.util';
|
||||
|
||||
type WorkspaceMemberDataSeed = {
|
||||
id: string;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -36,6 +37,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
|
||||
RoleModule,
|
||||
UserRoleModule,
|
||||
ApiKeyModule,
|
||||
ApplicationModule,
|
||||
FeatureFlagModule,
|
||||
FileStorageModule,
|
||||
WorkspaceSyncMetadataModule,
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/works
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { COMPANY_CUSTOM_FIELD_SEEDS } from 'src/engine/workspace-manager/dev-seeder/metadata/custom-fields/constants/company-custom-field-seeds.constant';
|
||||
import { PERSON_CUSTOM_FIELD_SEEDS } from 'src/engine/workspace-manager/dev-seeder/metadata/custom-fields/constants/person-custom-field-seeds.constant';
|
||||
import { PET_CUSTOM_FIELD_SEEDS } from 'src/engine/workspace-manager/dev-seeder/metadata/custom-fields/constants/pet-custom-field-seeds.constant';
|
||||
|
||||
+19
-1
@@ -1,8 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
@@ -11,6 +13,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadat
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { SeededWorkspacesIds } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { DevSeederPermissionsService } from 'src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service';
|
||||
import { seedCoreSchema } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-core-schema.util';
|
||||
import { seedPageLayoutTabs } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layout-tabs.util';
|
||||
@@ -18,6 +21,7 @@ import { seedPageLayoutWidgets } from 'src/engine/workspace-manager/dev-seeder/c
|
||||
import { seedPageLayouts } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layouts.util';
|
||||
import { DevSeederDataService } from 'src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service';
|
||||
import { DevSeederMetadataService } from 'src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/twenty-standard-applications';
|
||||
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
|
||||
|
||||
@Injectable()
|
||||
@@ -33,17 +37,19 @@ export class DevSeederService {
|
||||
private readonly devSeederPermissionsService: DevSeederPermissionsService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly devSeederDataService: DevSeederDataService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {}
|
||||
|
||||
public async seedDev(workspaceId: string): Promise<void> {
|
||||
public async seedDev(workspaceId: SeededWorkspacesIds): Promise<void> {
|
||||
const isBillingEnabled = this.twentyConfigService.get('IS_BILLING_ENABLED');
|
||||
const appVersion = this.twentyConfigService.get('APP_VERSION');
|
||||
|
||||
await seedCoreSchema({
|
||||
dataSource: this.coreDataSource,
|
||||
workspaceId,
|
||||
applicationService: this.applicationService,
|
||||
seedBilling: isBillingEnabled,
|
||||
appVersion,
|
||||
});
|
||||
@@ -62,6 +68,18 @@ export class DevSeederService {
|
||||
const featureFlags =
|
||||
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId);
|
||||
|
||||
const twentyStandardApplication =
|
||||
await this.applicationService.findByUniversalIdentifier({
|
||||
workspaceId,
|
||||
universalIdentifier: TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(twentyStandardApplication)) {
|
||||
throw new Error(
|
||||
'Seeder failed to find twenty standard application, should never occur',
|
||||
);
|
||||
}
|
||||
|
||||
await this.workspaceSyncMetadataService.synchronize({
|
||||
workspaceId: workspaceId,
|
||||
dataSourceId: dataSourceMetadata.id,
|
||||
|
||||
@@ -19,6 +19,7 @@ import { DevSeederModule } from 'src/engine/workspace-manager/dev-seeder/dev-see
|
||||
import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module';
|
||||
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
|
||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
|
||||
import { WorkspaceManagerService } from './workspace-manager.service';
|
||||
|
||||
@@ -38,6 +39,7 @@ import { WorkspaceManagerService } from './workspace-manager.service';
|
||||
TypeOrmModule.forFeature([UserWorkspaceEntity, WorkspaceEntity]),
|
||||
RoleModule,
|
||||
UserRoleModule,
|
||||
ApplicationModule,
|
||||
TypeOrmModule.forFeature([
|
||||
FieldMetadataEntity,
|
||||
RoleTargetsEntity,
|
||||
|
||||
@@ -3,10 +3,10 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { type DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
@@ -45,17 +45,18 @@ export class WorkspaceManagerService {
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(RoleTargetsEntity)
|
||||
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
||||
private readonly agentService: AgentService,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
public async init({
|
||||
workspaceId,
|
||||
workspace,
|
||||
userId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workspace: WorkspaceEntity;
|
||||
userId: string;
|
||||
}): Promise<void> {
|
||||
const workspaceId = workspace.id;
|
||||
const schemaCreationStart = performance.now();
|
||||
const schemaName =
|
||||
await this.workspaceDataSourceService.createWorkspaceDBSchema(
|
||||
@@ -78,6 +79,11 @@ export class WorkspaceManagerService {
|
||||
const featureFlags =
|
||||
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId);
|
||||
|
||||
await this.applicationService.createTwentyStandardApplication({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
// TODO later replace by twenty-standard installation aka workspaceMigration run
|
||||
await this.workspaceSyncMetadataService.synchronize({
|
||||
workspaceId,
|
||||
dataSourceId: dataSourceMetadata.id,
|
||||
|
||||
+2
@@ -9,6 +9,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
|
||||
import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module';
|
||||
import { SyncWorkspaceLoggerModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/services/sync-workspace-logger.module';
|
||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
|
||||
import { SyncWorkspaceMetadataCommand } from './sync-workspace-metadata.command';
|
||||
|
||||
@@ -22,6 +23,7 @@ import { SyncWorkspaceMetadataCommand } from './sync-workspace-metadata.command'
|
||||
FeatureFlagModule,
|
||||
TypeOrmModule.forFeature([WorkspaceEntity]),
|
||||
SyncWorkspaceLoggerModule,
|
||||
ApplicationModule,
|
||||
],
|
||||
providers: [SyncWorkspaceMetadataCommand],
|
||||
exports: [SyncWorkspaceMetadataCommand],
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
|
||||
export const TWENTY_STANDARD_APPLICATION = {
|
||||
universalIdentifier: '20202020-64aa-4b6f-b003-9c74b97cee20',
|
||||
name: 'Twenty Standard',
|
||||
description:
|
||||
'Twenty is an open-source CRM that allows you to manage your sales and customer relationships',
|
||||
version: '1.0.0',
|
||||
sourcePath: 'cli-sync',
|
||||
sourceType: 'local',
|
||||
} as const satisfies CreateApplicationInput;
|
||||
|
||||
export type CreateApplicationInput = Omit<
|
||||
ApplicationEntity,
|
||||
| 'workspaceId'
|
||||
| 'serverlessFunctionLayerId'
|
||||
| 'id'
|
||||
| 'createdAt'
|
||||
| 'updatedAt'
|
||||
| 'deletedAt'
|
||||
| 'workspace'
|
||||
| 'agents'
|
||||
| 'applicationVariables'
|
||||
| 'objects'
|
||||
| 'serverlessFunctions'
|
||||
>;
|
||||
export type TwentyStandardApplicationUniversalIdentifiers =
|
||||
(typeof TWENTY_STANDARD_APPLICATION)['universalIdentifier'];
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type CreateApplicationInput } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/twenty-standard-applications';
|
||||
|
||||
export const computeWorkspaceCustomCreateApplicationInput = ({
|
||||
workspace,
|
||||
applicationId = v4(),
|
||||
}: {
|
||||
applicationId?: string;
|
||||
workspace: Pick<WorkspaceEntity, 'id' | 'displayName'>;
|
||||
}) =>
|
||||
({
|
||||
description: 'Workspace custom application',
|
||||
name: `${isDefined(workspace.displayName) ? `${workspace.displayName}'s ` : ''}custom application`,
|
||||
sourcePath: 'workspace-custom',
|
||||
sourceType: 'local',
|
||||
version: '1.0.0',
|
||||
universalIdentifier: applicationId,
|
||||
workspaceId: workspace.id,
|
||||
id: applicationId,
|
||||
}) as const satisfies CreateApplicationInput & {
|
||||
workspaceId: string;
|
||||
id: string;
|
||||
};
|
||||
+2
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -29,6 +30,7 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ApplicationModule,
|
||||
FeatureFlagModule,
|
||||
WorkspaceMigrationBuilderModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
|
||||
Reference in New Issue
Block a user