diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 51a4c0851..8fa9f7601 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1054,7 +1054,7 @@ export type ServerlessFunctionExecutionResult = { status: ServerlessFunctionExecutionStatus; }; -/** Status of the table */ +/** Status of the serverless function execution */ export enum ServerlessFunctionExecutionStatus { Error = 'ERROR', Success = 'SUCCESS' @@ -1397,7 +1397,8 @@ export type WorkspaceFeatureFlagsArgs = { export enum WorkspaceActivationStatus { Active = 'ACTIVE', - Inactive = 'INACTIVE' + Inactive = 'INACTIVE', + PendingCreation = 'PENDING_CREATION' } export type WorkspaceEdge = { diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 10fe470cf..f72707004 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -162,6 +162,17 @@ export type ClientConfig = { telemetry: Telemetry; }; +export type CreateServerlessFunctionFromFileInput = { + description?: InputMaybe; + name: Scalars['String']; +}; + +export type CreateServerlessFunctionInput = { + code: Scalars['String']; + description?: InputMaybe; + name: Scalars['String']; +}; + export type CursorPaging = { /** Paginate after opaque cursor */ after?: InputMaybe; @@ -178,6 +189,11 @@ export type DeleteOneObjectInput = { id: Scalars['UUID']; }; +export type DeleteServerlessFunctionInput = { + /** The id of the function. */ + id: Scalars['ID']; +}; + /** Schema update on a table */ export enum DistantTableUpdate { ColumnsAdded = 'COLUMNS_ADDED', @@ -310,14 +326,18 @@ export type Mutation = { checkoutSession: SessionEntity; createOneAppToken: AppToken; createOneObject: Object; + createOneServerlessFunction: ServerlessFunction; + createOneServerlessFunctionFromFile: ServerlessFunction; deleteCurrentWorkspace: Workspace; deleteOneObject: Object; + deleteOneServerlessFunction: ServerlessFunction; deleteUser: User; disablePostgresProxy: PostgresCredentials; emailPasswordResetLink: EmailPasswordResetLink; enablePostgresProxy: PostgresCredentials; enableWorkflowTrigger: Scalars['Boolean']; exchangeAuthorizationCode: ExchangeAuthCode; + executeOneServerlessFunction: ServerlessFunctionExecutionResult; generateApiKeyToken: ApiKeyToken; generateJWT: AuthTokens; generateTransientToken: TransientToken; @@ -327,8 +347,10 @@ export type Mutation = { signUp: LoginToken; skipSyncEmailOnboardingStep: OnboardingStepSuccess; track: Analytics; + triggerWorkflow: WorkflowTriggerResult; updateBillingSubscription: UpdateBillingEntity; updateOneObject: Object; + updateOneServerlessFunction: ServerlessFunction; updatePasswordViaResetToken: InvalidatePassword; updateWorkspace: Workspace; uploadFile: Scalars['String']; @@ -369,11 +391,27 @@ export type MutationCheckoutSessionArgs = { }; +export type MutationCreateOneServerlessFunctionArgs = { + input: CreateServerlessFunctionInput; +}; + + +export type MutationCreateOneServerlessFunctionFromFileArgs = { + file: Scalars['Upload']; + input: CreateServerlessFunctionFromFileInput; +}; + + export type MutationDeleteOneObjectArgs = { input: DeleteOneObjectInput; }; +export type MutationDeleteOneServerlessFunctionArgs = { + input: DeleteServerlessFunctionInput; +}; + + export type MutationEmailPasswordResetLinkArgs = { email: Scalars['String']; }; @@ -391,6 +429,12 @@ export type MutationExchangeAuthorizationCodeArgs = { }; +export type MutationExecuteOneServerlessFunctionArgs = { + id: Scalars['UUID']; + payload?: InputMaybe; +}; + + export type MutationGenerateApiKeyTokenArgs = { apiKeyId: Scalars['String']; expiresAt: Scalars['String']; @@ -431,11 +475,21 @@ export type MutationTrackArgs = { }; +export type MutationTriggerWorkflowArgs = { + workflowVersionId: Scalars['String']; +}; + + export type MutationUpdateOneObjectArgs = { input: UpdateOneObjectInput; }; +export type MutationUpdateOneServerlessFunctionArgs = { + input: UpdateServerlessFunctionInput; +}; + + export type MutationUpdatePasswordViaResetTokenArgs = { newPassword: Scalars['String']; passwordResetToken: Scalars['String']; @@ -557,6 +611,8 @@ export type Query = { getTimelineThreadsFromPersonId: TimelineThreadsWithTotal; object: Object; objects: ObjectConnection; + serverlessFunction: ServerlessFunction; + serverlessFunctions: ServerlessFunctionConnection; validatePasswordResetToken: ValidatePasswordResetToken; }; @@ -731,9 +787,21 @@ export type ServerlessFunctionEdge = { export type ServerlessFunctionExecutionResult = { __typename?: 'ServerlessFunctionExecutionResult'; /** Execution result in JSON format */ - result: Scalars['JSON']; + data?: Maybe; + /** Execution duration in milliseconds */ + duration: Scalars['Float']; + /** Execution error in JSON format */ + error?: Maybe; + /** Execution status */ + status: ServerlessFunctionExecutionStatus; }; +/** Status of the serverless function execution */ +export enum ServerlessFunctionExecutionStatus { + Error = 'ERROR', + Success = 'SUCCESS' +} + /** SyncStatus of the serverlessFunction */ export enum ServerlessFunctionSyncStatus { NotReady = 'NOT_READY', @@ -896,6 +964,14 @@ export type UpdateOneObjectInput = { update: UpdateObjectPayload; }; +export type UpdateServerlessFunctionInput = { + code: Scalars['String']; + description?: InputMaybe; + /** Id of the serverless function to execute */ + id: Scalars['UUID']; + name: Scalars['String']; +}; + export type UpdateWorkspaceInput = { allowImpersonation?: InputMaybe; displayName?: InputMaybe; @@ -969,6 +1045,12 @@ export type Verify = { user: User; }; +export type WorkflowTriggerResult = { + __typename?: 'WorkflowTriggerResult'; + /** Execution result in JSON format */ + result?: Maybe; +}; + export type Workspace = { __typename?: 'Workspace'; activationStatus: WorkspaceActivationStatus; @@ -1002,7 +1084,8 @@ export type WorkspaceFeatureFlagsArgs = { export enum WorkspaceActivationStatus { Active = 'ACTIVE', - Inactive = 'INACTIVE' + Inactive = 'INACTIVE', + PendingCreation = 'PENDING_CREATION' } export type WorkspaceEdge = { diff --git a/packages/twenty-server/project.json b/packages/twenty-server/project.json index c7139d0a8..2a993b848 100644 --- a/packages/twenty-server/project.json +++ b/packages/twenty-server/project.json @@ -128,6 +128,18 @@ "parallel": false } }, + "database:migrate:revert": { + "executor": "nx:run-commands", + "dependsOn": ["build"], + "options": { + "cwd": "packages/twenty-server", + "commands": [ + "nx typeorm -- migration:revert -d src/database/typeorm/metadata/metadata.datasource", + "nx typeorm -- migration:revert -d src/database/typeorm/core/core.datasource" + ], + "parallel": false + } + }, "database:reset": { "executor": "nx:run-commands", "dependsOn": ["build"], diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1722256203540-updateActivationStatusEnum.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1722256203540-updateActivationStatusEnum.ts new file mode 100644 index 000000000..bb24e40a3 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/1722256203540-updateActivationStatusEnum.ts @@ -0,0 +1,67 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateActivationStatus1722256203540 implements MigrationInterface { + name = 'UpdateActivationStatus1722256203540'; + + public async up(queryRunner: QueryRunner): Promise { + // Set current column as text + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" SET DATA TYPE text USING "activationStatus"::text`, + ); + + // Drop default value + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" DROP DEFAULT`, + ); + + // Drop the old enum type + await queryRunner.query( + `DROP TYPE "core"."workspace_activationstatus_enum"`, + ); + + await queryRunner.query( + `CREATE TYPE "core"."workspace_activationStatus_enum" AS ENUM('PENDING_CREATION', 'ACTIVE', 'INACTIVE')`, + ); + + // Re-apply the enum type + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" SET DATA TYPE "core"."workspace_activationStatus_enum" USING "activationStatus"::"core"."workspace_activationStatus_enum"`, + ); + + // Update default value + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" SET DEFAULT 'INACTIVE'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // Set current column as text + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" SET DATA TYPE text USING "activationStatus"::text`, + ); + + // Drop default value + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" DROP DEFAULT`, + ); + + // Drop the old enum type + await queryRunner.query( + `DROP TYPE "core"."workspace_activationStatus_enum"`, + ); + + await queryRunner.query( + `CREATE TYPE "core"."workspace_activationstatus_enum" AS ENUM('ACTIVE', 'INACTIVE')`, + ); + + // Re-apply the enum type + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" SET DATA TYPE "core"."workspace_activationstatus_enum" USING "activationStatus"::"core"."workspace_activationstatus_enum"`, + ); + + // Update default value + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" SET DEFAULT 'INACTIVE'`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1721309629608-addRuntimeColumnToServerlessFunction.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1721309629608-addRuntimeColumnToServerlessFunction.ts index c1ac579fc..f7d5c40c8 100644 --- a/packages/twenty-server/src/database/typeorm/metadata/migrations/1721309629608-addRuntimeColumnToServerlessFunction.ts +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1721309629608-addRuntimeColumnToServerlessFunction.ts @@ -21,5 +21,13 @@ export class AddRuntimeColumnToServerlessFunction1721309629608 await queryRunner.query( `ALTER TABLE "metadata"."serverlessFunction" DROP COLUMN "runtime"`, ); + + await queryRunner.query( + `ALTER TABLE "metadata"."serverlessFunction" DROP COLUMN "description"`, + ); + + await queryRunner.query( + `ALTER TABLE "metadata"."serverlessFunction" DROP COLUMN "sourceCodeFullPath"`, + ); } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts index 4dafcf126..90bfd8f6e 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts @@ -1,30 +1,33 @@ +import { HttpService } from '@nestjs/axios'; import { BadRequestException, ForbiddenException, Injectable, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { HttpService } from '@nestjs/axios'; +import FileType from 'file-type'; import { Repository } from 'typeorm'; import { v4 } from 'uuid'; -import FileType from 'file-type'; import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; -import { assert } from 'src/utils/assert'; import { PASSWORD_REGEX, - hashPassword, compareHash, + hashPassword, } from 'src/engine/core-modules/auth/auth.util'; -import { User } from 'src/engine/core-modules/user/user.entity'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; -import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; -import { getImageBufferFromUrl } from 'src/utils/image'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; +import { User } from 'src/engine/core-modules/user/user.entity'; import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; +import { + Workspace, + WorkspaceActivationStatus, +} from 'src/engine/core-modules/workspace/workspace.entity'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { assert } from 'src/utils/assert'; +import { getImageBufferFromUrl } from 'src/utils/image'; export type SignInUpServiceInput = { email: string; @@ -144,11 +147,8 @@ export class SignInUpService { ForbiddenException, ); - const isWorkspaceActivated = - await this.workspaceService.isWorkspaceActivated(workspace.id); - assert( - isWorkspaceActivated, + workspace.activationStatus === WorkspaceActivationStatus.ACTIVE, 'Workspace is not ready to welcome new members', ForbiddenException, ); @@ -203,6 +203,7 @@ export class SignInUpService { displayName: '', domainName: '', inviteHash: v4(), + activationStatus: WorkspaceActivationStatus.PENDING_CREATION, }); const workspace = await this.workspaceRepository.save(workspaceToCreate); diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.workspace-service.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.workspace-service.ts index 169f24811..be0ecb355 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.workspace-service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.workspace-service.ts @@ -284,7 +284,7 @@ export class BillingWorkspaceService { | Stripe.CustomerSubscriptionCreatedEvent.Data | Stripe.CustomerSubscriptionDeletedEvent.Data, ) { - const workspace = this.workspaceRepository.find({ + const workspace = await this.workspaceRepository.findOne({ where: { id: workspaceId }, }); @@ -341,9 +341,10 @@ export class BillingWorkspaceService { } if ( - data.object.status === SubscriptionStatus.Active || - data.object.status === SubscriptionStatus.Trialing || - data.object.status === SubscriptionStatus.PastDue + (data.object.status === SubscriptionStatus.Active || + data.object.status === SubscriptionStatus.Trialing || + data.object.status === SubscriptionStatus.PastDue) && + workspace.activationStatus == WorkspaceActivationStatus.INACTIVE ) { await this.workspaceRepository.update(workspaceId, { activationStatus: WorkspaceActivationStatus.ACTIVE, diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts index 8b176e554..28a24059b 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts @@ -45,23 +45,20 @@ export class WorkspaceService extends TypeOrmQueryService { if (!data.displayName || !data.displayName.length) { throw new BadRequestException("'displayName' not provided"); } - await this.workspaceRepository.update(user.defaultWorkspace.id, { - displayName: data.displayName, - activationStatus: WorkspaceActivationStatus.ACTIVE, - }); + await this.workspaceManagerService.init(user.defaultWorkspace.id); await this.userWorkspaceService.createWorkspaceMember( user.defaultWorkspace.id, user, ); + await this.workspaceRepository.update(user.defaultWorkspace.id, { + displayName: data.displayName, + activationStatus: WorkspaceActivationStatus.ACTIVE, + }); return user.defaultWorkspace; } - async isWorkspaceActivated(id: string): Promise { - return await this.workspaceManagerService.doesDataSourceExist(id); - } - async softDeleteWorkspace(id: string) { const workspace = await this.workspaceRepository.findOneBy({ id }); diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts index 5d54ec231..88a3bf7c8 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts @@ -21,6 +21,7 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works import { User } from 'src/engine/core-modules/user/user.entity'; export enum WorkspaceActivationStatus { + PENDING_CREATION = 'PENDING_CREATION', ACTIVE = 'ACTIVE', INACTIVE = 'INACTIVE', } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index e1ae3e27e..43542a22b 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -29,7 +29,7 @@ import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/worksp import { assert } from 'src/utils/assert'; import { streamToBuffer } from 'src/utils/stream-to-buffer'; -import { Workspace, WorkspaceActivationStatus } from './workspace.entity'; +import { Workspace } from './workspace.entity'; import { WorkspaceService } from './services/workspace.service'; @@ -100,21 +100,6 @@ export class WorkspaceResolver { return this.workspaceService.deleteWorkspace(id); } - @ResolveField(() => WorkspaceActivationStatus) - async activationStatus( - @Parent() workspace: Workspace, - ): Promise { - if (workspace.activationStatus) { - return workspace.activationStatus; - } - - if (await this.workspaceService.isWorkspaceActivated(workspace.id)) { - return WorkspaceActivationStatus.ACTIVE; - } - - return WorkspaceActivationStatus.INACTIVE; - } - @ResolveField(() => String, { nullable: true }) async currentCacheVersion( @Parent() workspace: Workspace, diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module.ts index 793c50ef8..4de8e6f5e 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module.ts @@ -1,10 +1,12 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; + +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { CalendarCreateCompanyAndContactAfterSyncJob } from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-create-company-and-contact-after-sync.job'; @@ -17,18 +19,17 @@ import { CalendarEventParticipantService } from 'src/modules/calendar/calendar-e import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; import { ContactCreationManagerModule } from 'src/modules/contact-creation-manager/contact-creation-manager.module'; import { MatchParticipantModule } from 'src/modules/match-participant/match-participant.module'; -import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; @Module({ imports: [ WorkspaceDataSourceModule, WorkspaceModule, TwentyORMModule.forFeature([CalendarEventParticipantWorkspaceEntity]), - ObjectMetadataRepositoryModule.forFeature([PersonWorkspaceEntity]), TypeOrmModule.forFeature( [ObjectMetadataEntity, FieldMetadataEntity], 'metadata', ), + NestjsQueryTypeOrmModule.forFeature([Workspace], 'core'), ContactCreationManagerModule, MatchParticipantModule, ], diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-match-participant.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-match-participant.job.ts index e91ff2300..be60d5e65 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-match-participant.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-match-participant.job.ts @@ -1,6 +1,9 @@ import { Scope } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; -import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; +import { Repository } from 'typeorm'; + +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; @@ -20,7 +23,8 @@ export type CalendarEventParticipantMatchParticipantJobData = { }) export class CalendarEventParticipantMatchParticipantJob { constructor( - private readonly workspaceService: WorkspaceService, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, private readonly matchParticipantService: MatchParticipantService, ) {} @@ -30,7 +34,13 @@ export class CalendarEventParticipantMatchParticipantJob { ): Promise { const { workspaceId, email, personId, workspaceMemberId } = data; - if (!this.workspaceService.isWorkspaceActivated(workspaceId)) { + const workspace = await this.workspaceRepository.findOne({ + where: { + id: workspaceId, + }, + }); + + if (workspace?.activationStatus !== 'ACTIVE') { return; }