Seed company workflow for email upserts (#18909)

@thomtrp

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Thomas des Francs
2026-03-27 13:47:07 +01:00
committed by GitHub
parent 56056b885d
commit 50ea560e57
20 changed files with 1350 additions and 467 deletions
@@ -2273,6 +2273,11 @@ type FieldConnection {
edges: [FieldEdge!]!
}
type LogicFunctionLogs {
"""Execution Logs"""
logs: String!
}
type DeleteTwoFactorAuthenticationMethod {
"""Boolean that confirms query was dispatched"""
success: Boolean!
@@ -2593,11 +2598,6 @@ type UsageAnalytics {
userDailyUsage: UsageUserDaily
}
type LogicFunctionLogs {
"""Execution Logs"""
logs: String!
}
type FrontComponent {
id: UUID!
name: String!
@@ -1937,6 +1937,12 @@ export interface FieldConnection {
__typename: 'FieldConnection'
}
export interface LogicFunctionLogs {
/** Execution Logs */
logs: Scalars['String']
__typename: 'LogicFunctionLogs'
}
export interface DeleteTwoFactorAuthenticationMethod {
/** Boolean that confirms query was dispatched */
success: Scalars['Boolean']
@@ -2298,12 +2304,6 @@ export interface UsageAnalytics {
__typename: 'UsageAnalytics'
}
export interface LogicFunctionLogs {
/** Execution Logs */
logs: Scalars['String']
__typename: 'LogicFunctionLogs'
}
export interface FrontComponent {
id: Scalars['UUID']
name: Scalars['String']
@@ -5097,6 +5097,13 @@ export interface FieldConnectionGenqlSelection{
__scalar?: boolean | number
}
export interface LogicFunctionLogsGenqlSelection{
/** Execution Logs */
logs?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface DeleteTwoFactorAuthenticationMethodGenqlSelection{
/** Boolean that confirms query was dispatched */
success?: boolean | number
@@ -5502,13 +5509,6 @@ export interface UsageAnalyticsGenqlSelection{
__scalar?: boolean | number
}
export interface LogicFunctionLogsGenqlSelection{
/** Execution Logs */
logs?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface FrontComponentGenqlSelection{
id?: boolean | number
name?: boolean | number
@@ -8049,6 +8049,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const LogicFunctionLogs_possibleTypes: string[] = ['LogicFunctionLogs']
export const isLogicFunctionLogs = (obj?: { __typename?: any } | null): obj is LogicFunctionLogs => {
if (!obj?.__typename) throw new Error('__typename is missing in "isLogicFunctionLogs"')
return LogicFunctionLogs_possibleTypes.includes(obj.__typename)
}
const DeleteTwoFactorAuthenticationMethod_possibleTypes: string[] = ['DeleteTwoFactorAuthenticationMethod']
export const isDeleteTwoFactorAuthenticationMethod = (obj?: { __typename?: any } | null): obj is DeleteTwoFactorAuthenticationMethod => {
if (!obj?.__typename) throw new Error('__typename is missing in "isDeleteTwoFactorAuthenticationMethod"')
@@ -8433,14 +8441,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const LogicFunctionLogs_possibleTypes: string[] = ['LogicFunctionLogs']
export const isLogicFunctionLogs = (obj?: { __typename?: any } | null): obj is LogicFunctionLogs => {
if (!obj?.__typename) throw new Error('__typename is missing in "isLogicFunctionLogs"')
return LogicFunctionLogs_possibleTypes.includes(obj.__typename)
}
const FrontComponent_possibleTypes: string[] = ['FrontComponent']
export const isFrontComponent = (obj?: { __typename?: any } | null): obj is FrontComponent => {
if (!obj?.__typename) throw new Error('__typename is missing in "isFrontComponent"')
@@ -63,8 +63,8 @@ export default {
210,
227,
244,
282,
283,
284,
299,
300,
325,
@@ -4458,6 +4458,14 @@ export default {
1
]
},
"LogicFunctionLogs": {
"logs": [
1
],
"__typename": [
1
]
},
"DeleteTwoFactorAuthenticationMethod": {
"success": [
6
@@ -4503,10 +4511,10 @@ export default {
},
"AuthTokenPair": {
"accessOrWorkspaceAgnosticToken": [
250
251
],
"refreshToken": [
250
251
],
"__typename": [
1
@@ -4514,7 +4522,7 @@ export default {
},
"AvailableWorkspacesAndAccessTokens": {
"tokens": [
251
252
],
"availableWorkspaces": [
224
@@ -4566,10 +4574,10 @@ export default {
},
"SignUp": {
"loginToken": [
250
251
],
"workspace": [
256
257
],
"__typename": [
1
@@ -4577,7 +4585,7 @@ export default {
},
"TransientToken": {
"transientToken": [
250
251
],
"__typename": [
1
@@ -4599,7 +4607,7 @@ export default {
},
"VerifyEmailAndGetLoginToken": {
"loginToken": [
250
251
],
"workspaceUrls": [
157
@@ -4618,7 +4626,7 @@ export default {
},
"AuthTokens": {
"tokens": [
251
252
],
"__typename": [
1
@@ -4626,7 +4634,7 @@ export default {
},
"LoginToken": {
"loginToken": [
250
251
],
"__typename": [
1
@@ -4656,10 +4664,10 @@ export default {
},
"Impersonate": {
"loginToken": [
250
251
],
"workspace": [
256
257
],
"__typename": [
1
@@ -4689,10 +4697,10 @@ export default {
},
"ApplicationTokenPair": {
"applicationAccessToken": [
250
251
],
"applicationRefreshToken": [
250
251
],
"__typename": [
1
@@ -4764,7 +4772,7 @@ export default {
1
],
"fields": [
271
272
],
"__typename": [
1
@@ -4861,10 +4869,10 @@ export default {
6
],
"objectPermissions": [
275
276
],
"fieldPermissions": [
276
277
],
"permissionFlags": [
1
@@ -4914,19 +4922,19 @@ export default {
1
],
"objects": [
272
],
"fields": [
271
],
"logicFunctions": [
273
],
"frontComponents": [
"fields": [
272
],
"logicFunctions": [
274
],
"frontComponents": [
275
],
"defaultRole": [
277
278
],
"sourcePackage": [
1
@@ -4986,13 +4994,13 @@ export default {
1
],
"driver": [
282
],
"status": [
283
],
"status": [
284
],
"verificationRecords": [
280
281
],
"verifiedAt": [
4
@@ -5039,7 +5047,7 @@ export default {
1
],
"location": [
285
286
],
"__typename": [
1
@@ -5067,13 +5075,13 @@ export default {
},
"ImapSmtpCaldavConnectionParameters": {
"IMAP": [
287
288
],
"SMTP": [
287
288
],
"CALDAV": [
287
288
],
"__typename": [
1
@@ -5093,7 +5101,7 @@ export default {
3
],
"connectionParameters": [
288
289
],
"__typename": [
1
@@ -5157,7 +5165,7 @@ export default {
1
],
"dailyUsage": [
293
294
],
"__typename": [
1
@@ -5165,13 +5173,13 @@ export default {
},
"UsageAnalytics": {
"usageByUser": [
292
293
],
"usageByOperationType": [
292
293
],
"timeSeries": [
293
294
],
"periodStart": [
4
@@ -5180,15 +5188,7 @@ export default {
4
],
"userDailyUsage": [
294
],
"__typename": [
1
]
},
"LogicFunctionLogs": {
"logs": [
1
295
],
"__typename": [
1
@@ -5235,7 +5235,7 @@ export default {
6
],
"applicationTokenPair": [
269
270
],
"__typename": [
1
@@ -6656,7 +6656,7 @@ export default {
}
],
"checkUserExists": [
264,
265,
{
"email": [
1,
@@ -6668,7 +6668,7 @@ export default {
}
],
"checkWorkspaceInviteHashIsValid": [
265,
266,
{
"inviteHash": [
1,
@@ -6686,7 +6686,7 @@ export default {
}
],
"validatePasswordResetToken": [
259,
260,
{
"passwordResetToken": [
1,
@@ -6769,7 +6769,7 @@ export default {
220
],
"getConnectedImapSmtpCaldavAccount": [
289,
290,
{
"id": [
3,
@@ -6778,7 +6778,7 @@ export default {
}
],
"getAutoCompleteAddress": [
284,
285,
{
"address": [
1,
@@ -6797,7 +6797,7 @@ export default {
}
],
"getAddressDetails": [
286,
287,
{
"placeId": [
1,
@@ -6889,19 +6889,19 @@ export default {
}
],
"getPostgresCredentials": [
291
292
],
"findManyPublicDomains": [
279
280
],
"getEmailingDomains": [
281
282
],
"findManyMarketplaceApps": [
278
279
],
"findOneMarketplaceApp": [
278,
279,
{
"universalIdentifier": [
1,
@@ -6924,7 +6924,7 @@ export default {
}
],
"getUsageAnalytics": [
295,
296,
{
"input": [
365
@@ -8309,7 +8309,7 @@ export default {
}
],
"getAuthorizationUrlForSSO": [
254,
255,
{
"input": [
466,
@@ -8318,7 +8318,7 @@ export default {
}
],
"getLoginTokenFromCredentials": [
263,
264,
{
"email": [
1,
@@ -8344,7 +8344,7 @@ export default {
}
],
"signIn": [
252,
253,
{
"email": [
1,
@@ -8366,7 +8366,7 @@ export default {
}
],
"verifyEmailAndGetLoginToken": [
260,
261,
{
"emailVerificationToken": [
1,
@@ -8386,7 +8386,7 @@ export default {
}
],
"verifyEmailAndGetWorkspaceAgnosticToken": [
252,
253,
{
"emailVerificationToken": [
1,
@@ -8402,7 +8402,7 @@ export default {
}
],
"getAuthTokensFromOTP": [
262,
263,
{
"otp": [
1,
@@ -8422,7 +8422,7 @@ export default {
}
],
"signUp": [
252,
253,
{
"email": [
1,
@@ -8444,7 +8444,7 @@ export default {
}
],
"signUpInWorkspace": [
257,
258,
{
"email": [
1,
@@ -8475,13 +8475,13 @@ export default {
}
],
"signUpInNewWorkspace": [
257
],
"generateTransientToken": [
258
],
"generateTransientToken": [
259
],
"getAuthTokensFromLoginToken": [
262,
263,
{
"loginToken": [
1,
@@ -8494,7 +8494,7 @@ export default {
}
],
"authorizeApp": [
249,
250,
{
"clientId": [
1,
@@ -8516,7 +8516,7 @@ export default {
}
],
"renewToken": [
262,
263,
{
"appToken": [
1,
@@ -8525,7 +8525,7 @@ export default {
}
],
"generateApiKeyToken": [
261,
262,
{
"apiKeyId": [
3,
@@ -8538,7 +8538,7 @@ export default {
}
],
"emailPasswordResetLink": [
253,
254,
{
"email": [
1,
@@ -8550,7 +8550,7 @@ export default {
}
],
"updatePasswordViaResetToken": [
255,
256,
{
"passwordResetToken": [
1,
@@ -8651,7 +8651,7 @@ export default {
}
],
"initiateOTPProvisioning": [
247,
248,
{
"loginToken": [
1,
@@ -8664,10 +8664,10 @@ export default {
}
],
"initiateOTPProvisioningForAuthenticatedUser": [
247
248
],
"deleteTwoFactorAuthenticationMethod": [
246,
247,
{
"twoFactorAuthenticationMethodId": [
3,
@@ -8676,7 +8676,7 @@ export default {
}
],
"verifyTwoFactorAuthenticationMethodForAuthenticatedUser": [
248,
249,
{
"otp": [
1,
@@ -8782,7 +8782,7 @@ export default {
}
],
"impersonate": [
266,
267,
{
"userId": [
3,
@@ -8804,7 +8804,7 @@ export default {
}
],
"saveImapSmtpCaldavAccount": [
290,
291,
{
"accountOwnerId": [
3,
@@ -9007,13 +9007,13 @@ export default {
}
],
"enablePostgresProxy": [
291
292
],
"disablePostgresProxy": [
291
292
],
"createPublicDomain": [
279,
280,
{
"domain": [
1,
@@ -9040,14 +9040,14 @@ export default {
}
],
"createEmailingDomain": [
281,
282,
{
"domain": [
1,
"String!"
],
"driver": [
282,
283,
"EmailingDomainDriver!"
]
}
@@ -9062,7 +9062,7 @@ export default {
}
],
"verifyEmailingDomain": [
281,
282,
{
"id": [
1,
@@ -9139,7 +9139,7 @@ export default {
}
],
"createDevelopmentApplication": [
267,
268,
{
"universalIdentifier": [
1,
@@ -9152,7 +9152,7 @@ export default {
}
],
"generateApplicationToken": [
269,
270,
{
"applicationId": [
3,
@@ -9161,7 +9161,7 @@ export default {
}
],
"syncApplication": [
268,
269,
{
"manifest": [
15,
@@ -9170,7 +9170,7 @@ export default {
}
],
"uploadApplicationFile": [
270,
271,
{
"file": [
372,
@@ -9204,7 +9204,7 @@ export default {
}
],
"renewApplicationToken": [
269,
270,
{
"applicationRefreshToken": [
1,
@@ -11464,7 +11464,7 @@ export default {
}
],
"logicFunctionLogs": [
296,
246,
{
"input": [
490,
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
import crypto from 'crypto';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';
import { type QueryRunner } from 'typeorm';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -40,6 +41,7 @@ type UpdateSourceFilesParams = Omit<
'builtHandlerPath'
> & {
sourceHandlerCode: string;
queryRunner?: QueryRunner;
};
type GetSourceCodeParams = Identifier & {
@@ -128,6 +130,7 @@ export class LogicFunctionResourceService {
workspaceId,
applicationUniversalIdentifier,
sourceHandlerCode,
queryRunner,
}: UpdateSourceFilesParams): Promise<void> {
await this.fileStorageService.writeFile({
workspaceId,
@@ -137,6 +140,20 @@ export class LogicFunctionResourceService {
sourceFile: sourceHandlerCode,
settings: { isTemporaryFile: false, toDelete: false },
mimeType: 'application/typescript',
queryRunner,
});
}
async deleteSourceFile({
sourceHandlerPath,
workspaceId,
applicationUniversalIdentifier,
}: GetSourceCodeParams): Promise<void> {
await this.fileStorageService.delete({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: sourceHandlerPath,
});
}
@@ -33,6 +33,7 @@ import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
describe('WorkspaceService', () => {
@@ -119,6 +120,7 @@ describe('WorkspaceService', () => {
PermissionsService,
FileCorePictureService,
AiModelRegistryService,
PrefillLogicFunctionService,
].map((service) => ({
provide: service,
useValue: {},
@@ -53,12 +53,14 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
import { prefillDashboards } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-dashboards';
import { prefillOpportunities } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-opportunities';
import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people';
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items';
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-companies.util';
import { prefillDashboards } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-dashboards.util';
import { prefillOpportunities } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-opportunities.util';
import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-people.util';
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util';
import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util';
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-migration/constant/default-feature-flags';
import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version-major-minor-patch';
@@ -111,6 +113,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
private readonly permissionsService: PermissionsService,
private readonly dnsManagerService: DnsManagerService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly prefillLogicFunctionService: PrefillLogicFunctionService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly subdomainManagerService: SubdomainManagerService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
@@ -681,6 +684,14 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
},
);
await this.prefillLogicFunctionService.ensureSeeded({
workspaceId,
definitions:
getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions(
workspaceId,
),
});
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
@@ -694,6 +705,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
await prefillWorkflows(
queryRunner.manager,
workspaceId,
schemaName,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
@@ -720,6 +732,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
);
}
}
throw error;
} finally {
await queryRunner.release();
@@ -41,12 +41,14 @@ import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { StandardObjectsPrefillModule } from 'src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill.module';
@Module({
imports: [
TypeORMModule,
TypeOrmModule.forFeature([BillingSubscriptionEntity, WorkspaceEntity]),
MetricsModule,
StandardObjectsPrefillModule,
NestjsQueryGraphQLModule.forFeature({
imports: [
AuditModule,
@@ -79,6 +81,7 @@ import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-m
WorkspaceManyOrAllFlatEntityMapsCacheModule,
ApplicationModule,
EnterpriseModule,
StandardObjectsPrefillModule,
],
services: [WorkspaceService],
resolvers: workspaceAutoResolverOpts,
@@ -121,8 +121,10 @@ import {
WORKSPACE_MEMBER_DATA_SEED_COLUMNS,
} from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
import { TimelineActivitySeederService } from 'src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service';
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items';
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util';
import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util';
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
type RecordSeedConfig = {
@@ -304,6 +306,7 @@ export class DevSeederDataService {
private readonly timelineActivitySeederService: TimelineActivitySeederService,
private readonly fileStorageService: FileStorageService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly prefillLogicFunctionService: PrefillLogicFunctionService,
) {}
public async seed({
@@ -331,6 +334,14 @@ export class DevSeederDataService {
const { seeds: attachmentSeeds, fileSeedMetadata: attachmentFileMeta } =
generateAttachmentSeedsForWorkspace(workspaceId);
await this.prefillLogicFunctionService.ensureSeeded({
workspaceId,
definitions:
getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions(
workspaceId,
),
});
await this.coreDataSource.transaction(
async (entityManager: WorkspaceEntityManager) => {
await this.seedRecordsInBatches({
@@ -359,6 +370,7 @@ export class DevSeederDataService {
await prefillWorkflows(
entityManager,
workspaceId,
schemaName,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
@@ -26,6 +26,7 @@ import { DevSeederDataService } from 'src/engine/workspace-manager/dev-seeder/da
import { TimelineActivitySeederService } from 'src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service';
import { DevSeederMetadataService } from 'src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service';
import { DevSeederService } from 'src/engine/workspace-manager/dev-seeder/services/dev-seeder.service';
import { StandardObjectsPrefillModule } from 'src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill.module';
import { TwentyStandardApplicationModule } from 'src/engine/workspace-manager/twenty-standard-application/twenty-standard-application.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@@ -48,6 +49,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
TypeOrmModule.forFeature([WorkspaceEntity, ObjectMetadataEntity]),
ObjectPermissionModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
StandardObjectsPrefillModule,
WorkspaceCacheModule,
WorkspaceMigrationModule,
TwentyStandardApplicationModule,
@@ -1,302 +0,0 @@
import { FieldActorSource } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type EntityManager } from 'typeorm';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields';
export const QUICK_LEAD_WORKFLOW_ID = '8b213cac-a68b-4ffe-817a-3ec994e9932d';
export const QUICK_LEAD_WORKFLOW_VERSION_ID =
'ac67974f-c524-4288-9d88-af8515400b68';
export const prefillWorkflows = async (
entityManager: EntityManager,
schemaName: string,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) => {
const { idByNameSingular: objectIdByNameSingular } = buildObjectIdByNameMaps(
flatObjectMetadataMaps,
);
const companyObjectMetadataId = objectIdByNameSingular['company'];
const personObjectMetadataId = objectIdByNameSingular['person'];
if (
!isDefined(companyObjectMetadataId) ||
!isDefined(personObjectMetadataId)
) {
throw new Error('Company or person object metadata not found');
}
const companyObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: companyObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
const personObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: personObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
if (!isDefined(companyObjectMetadata) || !isDefined(personObjectMetadata)) {
throw new Error('Company or person object metadata not found');
}
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.workflow`, [
'id',
'name',
'lastPublishedVersionId',
'statuses',
'position',
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
'createdByContext',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
])
.orIgnore()
.values([
{
id: QUICK_LEAD_WORKFLOW_ID,
name: 'Quick Lead',
lastPublishedVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID,
statuses: ['ACTIVE'],
position: 1,
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
createdByContext: {},
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
])
.returning('*')
.execute();
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.workflowVersion`, [
'id',
'name',
'trigger',
'steps',
'status',
'position',
'workflowId',
])
.orIgnore()
.values([
{
id: QUICK_LEAD_WORKFLOW_VERSION_ID,
name: 'v1',
trigger: JSON.stringify({
name: 'Launch manually',
type: 'MANUAL',
settings: {
outputSchema: {},
icon: 'IconUserPlus',
availability: { type: 'GLOBAL', locations: undefined },
},
nextStepIds: ['6e089bc9-aabd-435f-865f-f31c01c8f4a7'],
}),
steps: JSON.stringify([
{
id: '6e089bc9-aabd-435f-865f-f31c01c8f4a7',
name: 'Quick Lead Form',
type: 'FORM',
valid: false,
settings: {
input: [
{
id: '14d669f0-5249-4fa4-b0bb-f8bd408328d5',
name: 'firstName',
type: 'TEXT',
label: 'First name',
placeholder: 'Tim',
},
{
id: '4eb6ce85-d231-4aef-9837-744490c026d0',
name: 'lastName',
type: 'TEXT',
label: 'Last Name',
placeholder: 'Apple',
},
{
id: 'adbf0e9f-1427-49be-b4fb-092b34d97350',
name: 'email',
type: 'TEXT',
label: 'Email',
placeholder: 'timapple@apple.com',
},
{
id: '4ffc7992-9e65-4a4d-9baf-b52e62f2c273',
name: 'jobTitle',
type: 'TEXT',
label: 'Job title',
placeholder: 'CEO',
},
{
id: '42f11926-04ea-4924-94a4-2293cc748362',
name: 'companyName',
type: 'TEXT',
label: 'Company name',
placeholder: 'Apple',
},
{
id: 'd6ca80ee-26cd-466d-91bf-984d7205451c',
name: 'companyDomain',
type: 'TEXT',
label: 'Company domain',
placeholder: 'https://www.apple.com',
},
],
outputSchema: {
email: {
type: 'TEXT',
label: 'Email',
value: 'My text',
isLeaf: true,
},
jobTitle: {
type: 'TEXT',
label: 'Job title',
value: 'My text',
isLeaf: true,
},
lastName: {
type: 'TEXT',
label: 'Last Name',
value: 'My text',
isLeaf: true,
},
firstName: {
type: 'TEXT',
label: 'First name',
value: 'My text',
isLeaf: true,
},
companyName: {
type: 'TEXT',
label: 'Company name',
value: 'My text',
isLeaf: true,
},
companyDomain: {
type: 'TEXT',
label: 'Company domain',
value: 'My text',
isLeaf: true,
},
},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
__typename: 'WorkflowAction',
nextStepIds: ['0715b6cd-7cc1-4b98-971b-00f54dfe643b'],
},
{
id: '0715b6cd-7cc1-4b98-971b-00f54dfe643b',
name: 'Create Company',
type: 'CREATE_RECORD',
valid: false,
settings: {
input: {
objectName: 'company',
objectRecord: {
name: '{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.companyName}}',
domainName: {
primaryLinkUrl:
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.companyDomain}}',
primaryLinkLabel: '',
},
},
},
outputSchema: {
object: {
icon: 'IconBuildingSkyscraper',
label: 'Company',
value: 'A company',
isLeaf: true,
fieldIdName: 'id',
nameSingular: 'company',
},
_outputSchemaType: 'RECORD',
fields: generateObjectRecordFields({
objectMetadataInfo: {
flatObjectMetadata: companyObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
},
}),
},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
__typename: 'WorkflowAction',
nextStepIds: ['6f553ea7-b00e-4371-9d88-d8298568a246'],
},
{
id: '6f553ea7-b00e-4371-9d88-d8298568a246',
name: 'Create Person',
type: 'CREATE_RECORD',
valid: false,
settings: {
input: {
objectName: 'person',
objectRecord: {
name: {
lastName:
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.lastName}}',
firstName:
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.firstName}}',
},
emails: {
primaryEmail:
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.email}}',
additionalEmails: [],
},
companyId: '{{0715b6cd-7cc1-4b98-971b-00f54dfe643b.id}}',
},
},
outputSchema: {
fields: generateObjectRecordFields({
objectMetadataInfo: {
flatObjectMetadata: personObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
},
}),
},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
__typename: 'WorkflowAction',
nextStepIds: null,
},
]),
status: 'ACTIVE',
position: 1,
workflowId: QUICK_LEAD_WORKFLOW_ID,
},
])
.returning('*')
.execute();
};
@@ -0,0 +1,58 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
import { type PrefilledWorkflowCodeStepLogicFunctionDefinition } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
@Injectable()
export class PrefillLogicFunctionService {
constructor(
private readonly logicFunctionFromSourceService: LogicFunctionFromSourceService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
async ensureSeeded({
workspaceId,
definitions,
}: {
workspaceId: string;
definitions: PrefilledWorkflowCodeStepLogicFunctionDefinition[];
}) {
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
for (const definition of definitions) {
const existingLogicFunction = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: definition.id,
flatEntityMaps: flatLogicFunctionMaps,
});
if (isDefined(existingLogicFunction)) {
continue;
}
await this.logicFunctionFromSourceService.createOneFromSource({
workspaceId,
input: {
id: definition.id,
name: definition.name,
description: definition.description,
toolInputSchema: definition.toolInputSchema,
source: {
sourceHandlerCode: definition.sourceHandlerCode,
toolInputSchema: definition.toolInputSchema,
handlerName: 'main',
},
},
});
}
}
}
@@ -1,32 +0,0 @@
import { type DataSource, type EntityManager } from 'typeorm';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people';
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items';
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
export const standardObjectsPrefillData = async (
dataSource: DataSource,
schemaName: string,
workspaceId: string,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) => {
dataSource.transaction(async (entityManager: EntityManager) => {
await prefillCompanies(entityManager, schemaName);
await prefillPeople(entityManager, schemaName);
await prefillWorkflows(
entityManager,
schemaName,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
await prefillWorkflowCommandMenuItems(entityManager, workspaceId);
});
};
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
@Module({
imports: [LogicFunctionModule, WorkspaceManyOrAllFlatEntityMapsCacheModule],
providers: [PrefillLogicFunctionService],
exports: [PrefillLogicFunctionService],
})
export class StandardObjectsPrefillModule {}
@@ -7,14 +7,14 @@ import {
FIGMA_ID,
NOTION_ID,
STRIPE_ID,
} from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
} from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-companies.util';
import {
BRIAN_CHESKY_ID,
DARIO_AMODEI_ID,
DYLAN_FIELD_ID,
IVAN_ZHAO_ID,
PATRICK_COLLISON_ID,
} from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people';
} from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-people.util';
export const OPPORTUNITY_STRIPE_PLATFORM_MIGRATION_ID =
'822639e5-9bf7-40f1-8882-a11140362339';
@@ -7,7 +7,7 @@ import {
FIGMA_ID,
NOTION_ID,
STRIPE_ID,
} from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
} from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-companies.util';
export const BRIAN_CHESKY_ID = 'a2e78a5e-338b-46df-8811-fa08c7d19d35'; // Airbnb
export const DARIO_AMODEI_ID = '93c72d2e-e65c-44c4-99ad-f87f50349dcf'; // Anthropic
@@ -0,0 +1,336 @@
import { v5 as uuidv5 } from 'uuid';
const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_LOGIC_FUNCTION_ID_NAMESPACE =
'd41b0a2d-aa97-44dc-ad5d-89774164af27';
export const getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds = (
workspaceId: string,
) => ({
extractDomainLogicFunctionId: uuidv5(
`${workspaceId}:create-company-when-adding-new-person:extract-domain`,
CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_LOGIC_FUNCTION_ID_NAMESPACE,
),
findMatchingCompanyByDomainLogicFunctionId: uuidv5(
`${workspaceId}:create-company-when-adding-new-person:find-matching-company-by-domain`,
CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_LOGIC_FUNCTION_ID_NAMESPACE,
),
isPersonalEmailLogicFunctionId: uuidv5(
`${workspaceId}:create-company-when-adding-new-person:is-personal-email`,
CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_LOGIC_FUNCTION_ID_NAMESPACE,
),
});
const EXTRACT_DOMAIN_TOOL_INPUT_SCHEMA = {
type: 'object',
properties: {
email: {
type: 'string',
},
},
required: ['email'],
};
const IS_PERSONAL_EMAIL_TOOL_INPUT_SCHEMA = {
type: 'object',
properties: {
primaryEmail: {
type: 'string',
},
},
required: ['primaryEmail'],
};
const FIND_MATCHING_COMPANY_BY_DOMAIN_TOOL_INPUT_SCHEMA = {
type: 'object',
properties: {
companies: {
type: 'array',
items: {
type: 'object',
},
},
domain: {
type: 'string',
},
},
required: ['companies', 'domain'],
};
const EXTRACT_DOMAIN_LOGIC_FUNCTION_SOURCE = `const MULTI_PART_SUFFIXES = new Set([
'ac.uk',
'co.in',
'co.jp',
'co.kr',
'co.nz',
'co.uk',
'co.za',
'com.ar',
'com.au',
'com.br',
'com.cn',
'com.hk',
'com.mx',
'com.sg',
'com.tr',
'com.tw',
'com.ua',
'gov.uk',
'ne.jp',
'net.au',
'org.au',
'org.uk',
]);
const getRegistrableDomain = (host) => {
const normalizedHost = host.toLowerCase().replace(/^www\\./, '');
const parts = normalizedHost.split('.').filter(Boolean);
if (parts.length <= 2) {
return normalizedHost;
}
const lastTwo = parts.slice(-2).join('.');
if (MULTI_PART_SUFFIXES.has(lastTwo) && parts.length >= 3) {
return parts.slice(-3).join('.');
}
return lastTwo;
};
export const main = async (params) => {
const email =
typeof params?.email === 'string' ? params.email.trim().toLowerCase() : '';
const domainFromEmail = email.split('@')[1] ?? '';
const domain = domainFromEmail ? getRegistrableDomain(domainFromEmail) : '';
return {
url: domain ? \`https://\${domain}\` : '',
domain,
};
};`;
const IS_PERSONAL_EMAIL_LOGIC_FUNCTION_SOURCE = `const MULTI_PART_SUFFIXES = new Set([
'ac.uk',
'co.in',
'co.jp',
'co.kr',
'co.nz',
'co.uk',
'co.za',
'com.ar',
'com.au',
'com.br',
'com.cn',
'com.hk',
'com.mx',
'com.sg',
'com.tr',
'com.tw',
'com.ua',
'gov.uk',
'ne.jp',
'net.au',
'org.au',
'org.uk',
]);
const PERSONAL_EMAIL_DOMAINS = new Set([
'aol.com',
'att.net',
'btinternet.com',
'comcast.net',
'fastmail.com',
'free.fr',
'gmx.com',
'gmx.de',
'googlemail.com',
'hotmail.com',
'hotmail.fr',
'icloud.com',
'laposte.net',
'live.com',
'mac.com',
'mail.com',
'me.com',
'msn.com',
'orange.fr',
'outlook.com',
'outlook.fr',
'pm.me',
'proton.me',
'protonmail.com',
'sfr.fr',
'verizon.net',
'wanadoo.fr',
'yahoo.co.jp',
'yahoo.co.uk',
'yahoo.com',
'yandex.com',
'yandex.ru',
'zoho.com',
'gmail.com',
]);
const getRegistrableDomain = (host) => {
const normalizedHost = host.toLowerCase().replace(/^www\\./, '');
const parts = normalizedHost.split('.').filter(Boolean);
if (parts.length <= 2) {
return normalizedHost;
}
const lastTwo = parts.slice(-2).join('.');
if (MULTI_PART_SUFFIXES.has(lastTwo) && parts.length >= 3) {
return parts.slice(-3).join('.');
}
return lastTwo;
};
export const main = async (params) => {
const email =
typeof params?.primaryEmail === 'string'
? params.primaryEmail.trim().toLowerCase()
: '';
if (!email || !email.includes('@')) {
return { isPersonal: true };
}
const host = email.split('@')[1] ?? '';
const registrableDomain = host ? getRegistrableDomain(host) : '';
return {
isPersonal:
!registrableDomain ||
PERSONAL_EMAIL_DOMAINS.has(host) ||
PERSONAL_EMAIL_DOMAINS.has(registrableDomain),
};
};`;
const FIND_MATCHING_COMPANY_BY_DOMAIN_LOGIC_FUNCTION_SOURCE = `const MULTI_PART_SUFFIXES = new Set([
'ac.uk',
'co.in',
'co.jp',
'co.kr',
'co.nz',
'co.uk',
'co.za',
'com.ar',
'com.au',
'com.br',
'com.cn',
'com.hk',
'com.mx',
'com.sg',
'com.tr',
'com.tw',
'com.ua',
'gov.uk',
'ne.jp',
'net.au',
'org.au',
'org.uk',
]);
const normalizeHost = (value) => {
if (typeof value !== 'string') {
return '';
}
return value
.trim()
.toLowerCase()
.replace(/^https?:\\/\\//, '')
.replace(/^www\\./, '')
.split('/')[0]
.split('?')[0]
.split('#')[0]
.split(':')[0];
};
const getRegistrableDomain = (value) => {
const normalizedHost = normalizeHost(value);
const parts = normalizedHost.split('.').filter(Boolean);
if (parts.length <= 2) {
return normalizedHost;
}
const lastTwo = parts.slice(-2).join('.');
if (MULTI_PART_SUFFIXES.has(lastTwo) && parts.length >= 3) {
return parts.slice(-3).join('.');
}
return lastTwo;
};
export const main = async (params) => {
const domain = getRegistrableDomain(params?.domain);
const companies = Array.isArray(params?.companies) ? params.companies : [];
const matchingCompany = companies.find((company) => {
const companyDomain = getRegistrableDomain(
company?.domainName?.primaryLinkUrl,
);
return companyDomain !== '' && companyDomain === domain;
});
const companyId =
typeof matchingCompany?.id === 'string' ? matchingCompany.id : '';
return {
companyId,
hasMatch: companyId !== '',
};
};`;
export type PrefilledWorkflowCodeStepLogicFunctionDefinition = {
id: string;
name: string;
description: string;
sourceHandlerCode: string;
toolInputSchema: object;
};
export const getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions =
(workspaceId: string): PrefilledWorkflowCodeStepLogicFunctionDefinition[] => {
const {
extractDomainLogicFunctionId,
findMatchingCompanyByDomainLogicFunctionId,
isPersonalEmailLogicFunctionId,
} =
getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds(workspaceId);
return [
{
id: extractDomainLogicFunctionId,
name: 'Extract domain from email',
description:
'Extracts a normalized company domain and URL from a person email address.',
sourceHandlerCode: EXTRACT_DOMAIN_LOGIC_FUNCTION_SOURCE,
toolInputSchema: EXTRACT_DOMAIN_TOOL_INPUT_SCHEMA,
},
{
id: findMatchingCompanyByDomainLogicFunctionId,
name: 'Find matching company by domain',
description:
'Finds an existing company whose website matches a normalized registrable domain.',
sourceHandlerCode:
FIND_MATCHING_COMPANY_BY_DOMAIN_LOGIC_FUNCTION_SOURCE,
toolInputSchema: FIND_MATCHING_COMPANY_BY_DOMAIN_TOOL_INPUT_SCHEMA,
},
{
id: isPersonalEmailLogicFunctionId,
name: 'Is this a personal email?',
description:
'Detects whether an email address belongs to a common personal email provider.',
sourceHandlerCode: IS_PERSONAL_EMAIL_LOGIC_FUNCTION_SOURCE,
toolInputSchema: IS_PERSONAL_EMAIL_TOOL_INPUT_SCHEMA,
},
];
};
@@ -2,7 +2,7 @@ import { type EntityManager } from 'typeorm';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
import { QUICK_LEAD_WORKFLOW_VERSION_ID } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
import { QUICK_LEAD_WORKFLOW_VERSION_ID } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
const QUICK_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER =
@@ -0,0 +1,762 @@
import { FieldActorSource } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type EntityManager } from 'typeorm';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event';
import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields';
import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
export const QUICK_LEAD_WORKFLOW_ID = '8b213cac-a68b-4ffe-817a-3ec994e9932d';
export const QUICK_LEAD_WORKFLOW_VERSION_ID =
'ac67974f-c524-4288-9d88-af8515400b68';
export const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID =
'887c6c06-fbc5-4b45-8d6b-f7b6b0f40b12';
export const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_VERSION_ID =
'0f276d7e-a950-41ab-ad98-35e80753dc58';
export const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_AUTOMATED_TRIGGER_ID =
'c54f5990-13a3-4c3b-b75d-df09e7843036';
export const prefillWorkflows = async (
entityManager: EntityManager,
workspaceId: string,
schemaName: string,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) => {
const {
extractDomainLogicFunctionId,
findMatchingCompanyByDomainLogicFunctionId,
isPersonalEmailLogicFunctionId,
} = getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds(workspaceId);
const { idByNameSingular: objectIdByNameSingular } = buildObjectIdByNameMaps(
flatObjectMetadataMaps,
);
const companyObjectMetadataId = objectIdByNameSingular['company'];
const personObjectMetadataId = objectIdByNameSingular['person'];
if (
!isDefined(companyObjectMetadataId) ||
!isDefined(personObjectMetadataId)
) {
throw new Error('Company or person object metadata not found');
}
const companyObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: companyObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
const personObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: personObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
if (!isDefined(companyObjectMetadata) || !isDefined(personObjectMetadata)) {
throw new Error('Company or person object metadata not found');
}
const companyDomainNameFieldMetadata = Object.values(
flatFieldMetadataMaps.byUniversalIdentifier,
).find(
(fieldMetadata) =>
fieldMetadata?.objectMetadataId === companyObjectMetadataId &&
fieldMetadata?.name === 'domainName',
);
if (!isDefined(companyDomainNameFieldMetadata)) {
throw new Error('Company domainName field metadata not found');
}
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.workflow`, [
'id',
'name',
'lastPublishedVersionId',
'statuses',
'position',
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
'createdByContext',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
])
.orIgnore()
.values([
{
id: QUICK_LEAD_WORKFLOW_ID,
name: 'Quick Lead',
lastPublishedVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID,
statuses: ['ACTIVE'],
position: 1,
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
createdByContext: {},
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
{
id: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID,
name: 'Create company when adding a new person',
lastPublishedVersionId:
CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_VERSION_ID,
statuses: ['ACTIVE'],
position: 2,
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
createdByContext: {},
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
])
.returning('*')
.execute();
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.workflowVersion`, [
'id',
'name',
'trigger',
'steps',
'status',
'position',
'workflowId',
])
.orIgnore()
.values([
{
id: QUICK_LEAD_WORKFLOW_VERSION_ID,
name: 'v1',
trigger: JSON.stringify({
name: 'Launch manually',
type: 'MANUAL',
settings: {
outputSchema: {},
icon: 'IconUserPlus',
availability: { type: 'GLOBAL', locations: undefined },
},
nextStepIds: ['6e089bc9-aabd-435f-865f-f31c01c8f4a7'],
}),
steps: JSON.stringify([
{
id: '6e089bc9-aabd-435f-865f-f31c01c8f4a7',
name: 'Quick Lead Form',
type: 'FORM',
valid: false,
settings: {
input: [
{
id: '14d669f0-5249-4fa4-b0bb-f8bd408328d5',
name: 'firstName',
type: 'TEXT',
label: 'First name',
placeholder: 'Tim',
},
{
id: '4eb6ce85-d231-4aef-9837-744490c026d0',
name: 'lastName',
type: 'TEXT',
label: 'Last Name',
placeholder: 'Apple',
},
{
id: 'adbf0e9f-1427-49be-b4fb-092b34d97350',
name: 'email',
type: 'TEXT',
label: 'Email',
placeholder: 'timapple@apple.com',
},
{
id: '4ffc7992-9e65-4a4d-9baf-b52e62f2c273',
name: 'jobTitle',
type: 'TEXT',
label: 'Job title',
placeholder: 'CEO',
},
{
id: '42f11926-04ea-4924-94a4-2293cc748362',
name: 'companyName',
type: 'TEXT',
label: 'Company name',
placeholder: 'Apple',
},
{
id: 'd6ca80ee-26cd-466d-91bf-984d7205451c',
name: 'companyDomain',
type: 'TEXT',
label: 'Company domain',
placeholder: 'https://www.apple.com',
},
],
outputSchema: {
email: {
type: 'TEXT',
label: 'Email',
value: 'My text',
isLeaf: true,
},
jobTitle: {
type: 'TEXT',
label: 'Job title',
value: 'My text',
isLeaf: true,
},
lastName: {
type: 'TEXT',
label: 'Last Name',
value: 'My text',
isLeaf: true,
},
firstName: {
type: 'TEXT',
label: 'First name',
value: 'My text',
isLeaf: true,
},
companyName: {
type: 'TEXT',
label: 'Company name',
value: 'My text',
isLeaf: true,
},
companyDomain: {
type: 'TEXT',
label: 'Company domain',
value: 'My text',
isLeaf: true,
},
},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
__typename: 'WorkflowAction',
nextStepIds: ['0715b6cd-7cc1-4b98-971b-00f54dfe643b'],
},
{
id: '0715b6cd-7cc1-4b98-971b-00f54dfe643b',
name: 'Create Company',
type: 'CREATE_RECORD',
valid: false,
settings: {
input: {
objectName: 'company',
objectRecord: {
name: '{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.companyName}}',
domainName: {
primaryLinkUrl:
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.companyDomain}}',
primaryLinkLabel: '',
},
},
},
outputSchema: {
object: {
icon: 'IconBuildingSkyscraper',
label: 'Company',
value: 'A company',
isLeaf: true,
fieldIdName: 'id',
nameSingular: 'company',
},
_outputSchemaType: 'RECORD',
fields: generateObjectRecordFields({
objectMetadataInfo: {
flatObjectMetadata: companyObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
},
}),
},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
__typename: 'WorkflowAction',
nextStepIds: ['6f553ea7-b00e-4371-9d88-d8298568a246'],
},
{
id: '6f553ea7-b00e-4371-9d88-d8298568a246',
name: 'Create Person',
type: 'CREATE_RECORD',
valid: false,
settings: {
input: {
objectName: 'person',
objectRecord: {
name: {
lastName:
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.lastName}}',
firstName:
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.firstName}}',
},
emails: {
primaryEmail:
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.email}}',
additionalEmails: [],
},
companyId: '{{0715b6cd-7cc1-4b98-971b-00f54dfe643b.id}}',
},
},
outputSchema: {
fields: generateObjectRecordFields({
objectMetadataInfo: {
flatObjectMetadata: personObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
},
}),
},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
__typename: 'WorkflowAction',
nextStepIds: null,
},
]),
status: 'ACTIVE',
position: 1,
workflowId: QUICK_LEAD_WORKFLOW_ID,
},
{
id: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_VERSION_ID,
name: 'v1',
trigger: JSON.stringify({
name: 'Record is created or updated',
type: 'DATABASE_EVENT',
settings: {
eventName: 'person.upserted',
fields: ['emails'],
outputSchema: generateFakeObjectRecordEvent(
{
flatObjectMetadata: personObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
},
DatabaseEventAction.UPSERTED,
),
},
nextStepIds: ['c30d7cbe-00e0-4966-bc1a-99b0a11a2cca'],
}),
steps: JSON.stringify([
{
id: 'c30d7cbe-00e0-4966-bc1a-99b0a11a2cca',
name: 'Is this a personal email?',
type: 'CODE',
valid: false,
position: {
x: 227.25,
y: 130,
},
settings: {
input: {
logicFunctionId: isPersonalEmailLogicFunctionId,
logicFunctionInput: {
primaryEmail:
'{{trigger.properties.after.emails.primaryEmail}}',
},
},
outputSchema: {
isPersonal: {
type: 'boolean',
label: 'isPersonal',
value: true,
isLeaf: true,
},
},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
nextStepIds: ['01f3db05-aae5-4e4b-b361-96684f09c704'],
},
{
id: '01f3db05-aae5-4e4b-b361-96684f09c704',
name: 'If business email',
type: 'FILTER',
valid: false,
position: {
x: 249.25,
y: 260,
},
settings: {
input: {
stepFilters: [
{
id: '0e595385-9e18-4869-abfd-cf72952b124c',
type: 'boolean',
value: 'false',
operand: 'IS',
isFullRecord: false,
stepOutputKey:
'{{c30d7cbe-00e0-4966-bc1a-99b0a11a2cca.isPersonal}}',
stepFilterGroupId: '5204d5f5-7b23-428c-9f84-c37971d497d3',
positionInStepFilterGroup: 0,
},
],
stepFilterGroups: [
{
id: '5204d5f5-7b23-428c-9f84-c37971d497d3',
logicalOperator: 'AND',
},
],
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
nextStepIds: ['1b01193b-8300-4d79-940b-44464bf45505'],
},
{
id: '1b01193b-8300-4d79-940b-44464bf45505',
name: 'Extract domain from email',
type: 'CODE',
valid: false,
position: {
x: 219.75,
y: 390,
},
settings: {
input: {
logicFunctionId: extractDomainLogicFunctionId,
logicFunctionInput: {
email: '{{trigger.properties.after.emails.primaryEmail}}',
},
},
outputSchema: {
url: {
icon: 'IconVariable',
type: 'string',
label: 'url',
value: 'https://twenty.com',
isLeaf: true,
},
domain: {
icon: 'IconVariable',
type: 'string',
label: 'domain',
value: 'twenty.com',
isLeaf: true,
},
},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
nextStepIds: ['becb3acf-79bb-4672-8a42-3696e94957b5'],
},
{
id: 'becb3acf-79bb-4672-8a42-3696e94957b5',
name: 'Search Company',
type: 'FIND_RECORDS',
valid: false,
position: {
x: 247.75,
y: 520,
},
settings: {
input: {
limit: 25,
filter: {
recordFilters: [
{
id: 'a9b917a0-5c4c-4e8f-bf91-160d0b888693',
type: 'LINKS',
label: 'Domain Name',
value: '{{1b01193b-8300-4d79-940b-44464bf45505.domain}}',
operand: 'CONTAINS',
displayValue:
'{{1b01193b-8300-4d79-940b-44464bf45505.domain}}',
subFieldName: 'primaryLinkUrl',
fieldMetadataId: companyDomainNameFieldMetadata.id,
recordFilterGroupId:
'194e151c-cf46-4e8f-a48b-649c36082dfa',
},
],
recordFilterGroups: [
{
id: '194e151c-cf46-4e8f-a48b-649c36082dfa',
logicalOperator: 'AND',
},
],
},
objectName: 'company',
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
nextStepIds: ['9d0b6ef2-aad2-4853-92e1-95f2abf10d5b'],
},
{
id: '9d0b6ef2-aad2-4853-92e1-95f2abf10d5b',
name: 'Find exact company match',
type: 'CODE',
valid: false,
position: {
x: 247.75,
y: 650,
},
settings: {
input: {
logicFunctionId: findMatchingCompanyByDomainLogicFunctionId,
logicFunctionInput: {
companies: '{{becb3acf-79bb-4672-8a42-3696e94957b5.all}}',
domain: '{{1b01193b-8300-4d79-940b-44464bf45505.domain}}',
},
},
outputSchema: {
companyId: {
type: 'string',
label: 'companyId',
value: '00000000-0000-0000-0000-000000000000',
isLeaf: true,
},
hasMatch: {
type: 'boolean',
label: 'hasMatch',
value: true,
isLeaf: true,
},
},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
nextStepIds: ['0c99a900-656a-40e8-977e-5a7357be33b9'],
},
{
id: '0c99a900-656a-40e8-977e-5a7357be33b9',
name: 'If a company already exists',
type: 'IF_ELSE',
valid: false,
position: {
x: 216.25,
y: 780,
},
settings: {
input: {
branches: [
{
id: '1344c151-15ff-40e2-a1a3-925fabaf5b1c',
nextStepIds: ['ffdd4271-75d4-4805-b1f8-2167a113c3b2'],
filterGroupId: 'f5c41047-2a6e-49fb-968a-fa7789a90ee5',
},
{
id: 'fe6dd152-1103-4324-af51-3ff994d1f8a7',
nextStepIds: ['ddafb9db-a94f-40b9-a5c9-becce857edf7'],
},
],
stepFilters: [
{
id: '290cc6a3-08fd-4be5-b42e-966d0bb90ff7',
type: 'boolean',
value: 'true',
operand: 'IS',
isFullRecord: false,
stepOutputKey:
'{{9d0b6ef2-aad2-4853-92e1-95f2abf10d5b.hasMatch}}',
stepFilterGroupId: 'f5c41047-2a6e-49fb-968a-fa7789a90ee5',
positionInStepFilterGroup: 0,
},
],
stepFilterGroups: [
{
id: 'f5c41047-2a6e-49fb-968a-fa7789a90ee5',
logicalOperator: 'AND',
},
],
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
},
{
id: 'ffdd4271-75d4-4805-b1f8-2167a113c3b2',
name: 'Attach person to existing company',
type: 'UPDATE_RECORD',
valid: false,
position: {
x: 0,
y: 910,
},
settings: {
input: {
objectName: 'person',
objectRecord: {
companyId:
'{{9d0b6ef2-aad2-4853-92e1-95f2abf10d5b.companyId}}',
},
fieldsToUpdate: ['companyId'],
objectRecordId: '{{trigger.properties.after.id}}',
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
},
{
id: 'ddafb9db-a94f-40b9-a5c9-becce857edf7',
name: 'Create a new company',
type: 'CREATE_RECORD',
valid: false,
position: {
x: 440,
y: 910,
},
settings: {
input: {
objectName: 'company',
objectRecord: {
name: '{{1b01193b-8300-4d79-940b-44464bf45505.domain}}',
domainName: {
primaryLinkUrl:
'{{1b01193b-8300-4d79-940b-44464bf45505.url}}',
primaryLinkLabel:
'{{1b01193b-8300-4d79-940b-44464bf45505.domain}}',
},
},
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
nextStepIds: ['d5d5d6e1-391f-4142-83c1-670f7087f079'],
},
{
id: 'd5d5d6e1-391f-4142-83c1-670f7087f079',
name: 'Attach person to this company',
type: 'UPDATE_RECORD',
valid: false,
position: {
x: 420.5,
y: 1040,
},
settings: {
input: {
objectName: 'person',
objectRecord: {
companyId: '{{ddafb9db-a94f-40b9-a5c9-becce857edf7.id}}',
},
fieldsToUpdate: ['companyId'],
objectRecordId: '{{trigger.properties.after.id}}',
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
},
]),
status: 'ACTIVE',
position: 2,
workflowId: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID,
},
])
.returning('*')
.execute();
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.workflowAutomatedTrigger`, [
'id',
'workflowId',
'type',
'settings',
])
.orIgnore()
.values([
{
id: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_AUTOMATED_TRIGGER_ID,
workflowId: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID,
type: 'DATABASE_EVENT',
settings: {
eventName: 'person.upserted',
fields: ['emails'],
},
},
])
.execute();
};