diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 36b19284e8..85019daa19 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -1389,44 +1389,6 @@ type ApplicationConnectionProvider { oauth: ApplicationConnectionProviderOAuthConfig } -type EnterpriseLicenseInfoDTO { - isValid: Boolean! - licensee: String - expiresAt: DateTime - subscriptionId: String -} - -type EnterpriseSubscriptionStatusDTO { - status: String! - licensee: String - expiresAt: DateTime - cancelAt: DateTime - currentPeriodEnd: DateTime - isCancellationScheduled: Boolean! -} - -type ApprovedAccessDomain { - id: UUID! - domain: String! - isValidated: Boolean! - createdAt: DateTime! -} - -type FileWithSignedUrl { - id: UUID! - path: String! - size: Float! - createdAt: DateTime! - url: String! -} - -type FileUploadTarget { - fileId: UUID! - uploadUrl: String! - contentType: String! - expiresAt: DateTime! -} - type BillingSubscriptionSchedulePhaseItem { price: String! quantity: Float @@ -1518,11 +1480,6 @@ type BillingSubscriptionItem { billingProduct: BillingProductDTO! } -type BillingCustomer { - id: UUID! - hasPaymentMethod: Boolean -} - type BillingSubscription { id: UUID! status: SubscriptionStatus! @@ -1545,6 +1502,73 @@ enum SubscriptionStatus { Unpaid } +type BillingCustomer { + id: UUID! + hasPaymentMethod: Boolean +} + +type LogicFunctionExecutionResult { + """Execution result in JSON format""" + data: JSON + + """Execution Logs""" + logs: String! + + """Execution duration in milliseconds""" + duration: Float! + + """Execution status""" + status: LogicFunctionExecutionStatus! + + """Execution error in JSON format""" + error: JSON +} + +"""Status of the logic function execution""" +enum LogicFunctionExecutionStatus { + IDLE + SUCCESS + ERROR +} + +type EnterpriseLicenseInfoDTO { + isValid: Boolean! + licensee: String + expiresAt: DateTime + subscriptionId: String +} + +type EnterpriseSubscriptionStatusDTO { + status: String! + licensee: String + expiresAt: DateTime + cancelAt: DateTime + currentPeriodEnd: DateTime + isCancellationScheduled: Boolean! +} + +type ApprovedAccessDomain { + id: UUID! + domain: String! + isValidated: Boolean! + createdAt: DateTime! +} + +type FileWithSignedUrl { + id: UUID! + path: String! + size: Float! + createdAt: DateTime! + url: String! +} + +type FileUploadTarget { + fileId: UUID! + uploadUrl: String! + contentType: String! + expiresAt: DateTime! +} + type BillingEndTrialPeriod { """Updated subscription status""" status: SubscriptionStatus @@ -1704,30 +1728,6 @@ type EventSubscription { metadataEvents: [MetadataEvent!]! } -type LogicFunctionExecutionResult { - """Execution result in JSON format""" - data: JSON - - """Execution Logs""" - logs: String! - - """Execution duration in milliseconds""" - duration: Float! - - """Execution status""" - status: LogicFunctionExecutionStatus! - - """Execution error in JSON format""" - error: JSON -} - -"""Status of the logic function execution""" -enum LogicFunctionExecutionStatus { - IDLE - SUCCESS - ERROR -} - type FeatureFlag { key: FeatureFlagKey! value: Boolean! diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index c80777afeb..3cbb2e5d96 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -1063,49 +1063,6 @@ export interface ApplicationConnectionProvider { __typename: 'ApplicationConnectionProvider' } -export interface EnterpriseLicenseInfoDTO { - isValid: Scalars['Boolean'] - licensee?: Scalars['String'] - expiresAt?: Scalars['DateTime'] - subscriptionId?: Scalars['String'] - __typename: 'EnterpriseLicenseInfoDTO' -} - -export interface EnterpriseSubscriptionStatusDTO { - status: Scalars['String'] - licensee?: Scalars['String'] - expiresAt?: Scalars['DateTime'] - cancelAt?: Scalars['DateTime'] - currentPeriodEnd?: Scalars['DateTime'] - isCancellationScheduled: Scalars['Boolean'] - __typename: 'EnterpriseSubscriptionStatusDTO' -} - -export interface ApprovedAccessDomain { - id: Scalars['UUID'] - domain: Scalars['String'] - isValidated: Scalars['Boolean'] - createdAt: Scalars['DateTime'] - __typename: 'ApprovedAccessDomain' -} - -export interface FileWithSignedUrl { - id: Scalars['UUID'] - path: Scalars['String'] - size: Scalars['Float'] - createdAt: Scalars['DateTime'] - url: Scalars['String'] - __typename: 'FileWithSignedUrl' -} - -export interface FileUploadTarget { - fileId: Scalars['UUID'] - uploadUrl: Scalars['String'] - contentType: Scalars['String'] - expiresAt: Scalars['DateTime'] - __typename: 'FileUploadTarget' -} - export interface BillingSubscriptionSchedulePhaseItem { price: Scalars['String'] quantity?: Scalars['Float'] @@ -1197,12 +1154,6 @@ export interface BillingSubscriptionItem { __typename: 'BillingSubscriptionItem' } -export interface BillingCustomer { - id: Scalars['UUID'] - hasPaymentMethod?: Scalars['Boolean'] - __typename: 'BillingCustomer' -} - export interface BillingSubscription { id: Scalars['UUID'] status: SubscriptionStatus @@ -1217,6 +1168,73 @@ export interface BillingSubscription { export type SubscriptionStatus = 'Active' | 'Canceled' | 'Incomplete' | 'IncompleteExpired' | 'PastDue' | 'Paused' | 'Trialing' | 'Unpaid' +export interface BillingCustomer { + id: Scalars['UUID'] + hasPaymentMethod?: Scalars['Boolean'] + __typename: 'BillingCustomer' +} + +export interface LogicFunctionExecutionResult { + /** Execution result in JSON format */ + data?: Scalars['JSON'] + /** Execution Logs */ + logs: Scalars['String'] + /** Execution duration in milliseconds */ + duration: Scalars['Float'] + /** Execution status */ + status: LogicFunctionExecutionStatus + /** Execution error in JSON format */ + error?: Scalars['JSON'] + __typename: 'LogicFunctionExecutionResult' +} + + +/** Status of the logic function execution */ +export type LogicFunctionExecutionStatus = 'IDLE' | 'SUCCESS' | 'ERROR' + +export interface EnterpriseLicenseInfoDTO { + isValid: Scalars['Boolean'] + licensee?: Scalars['String'] + expiresAt?: Scalars['DateTime'] + subscriptionId?: Scalars['String'] + __typename: 'EnterpriseLicenseInfoDTO' +} + +export interface EnterpriseSubscriptionStatusDTO { + status: Scalars['String'] + licensee?: Scalars['String'] + expiresAt?: Scalars['DateTime'] + cancelAt?: Scalars['DateTime'] + currentPeriodEnd?: Scalars['DateTime'] + isCancellationScheduled: Scalars['Boolean'] + __typename: 'EnterpriseSubscriptionStatusDTO' +} + +export interface ApprovedAccessDomain { + id: Scalars['UUID'] + domain: Scalars['String'] + isValidated: Scalars['Boolean'] + createdAt: Scalars['DateTime'] + __typename: 'ApprovedAccessDomain' +} + +export interface FileWithSignedUrl { + id: Scalars['UUID'] + path: Scalars['String'] + size: Scalars['Float'] + createdAt: Scalars['DateTime'] + url: Scalars['String'] + __typename: 'FileWithSignedUrl' +} + +export interface FileUploadTarget { + fileId: Scalars['UUID'] + uploadUrl: Scalars['String'] + contentType: Scalars['String'] + expiresAt: Scalars['DateTime'] + __typename: 'FileUploadTarget' +} + export interface BillingEndTrialPeriod { /** Updated subscription status */ status?: SubscriptionStatus @@ -1372,24 +1390,6 @@ export interface EventSubscription { __typename: 'EventSubscription' } -export interface LogicFunctionExecutionResult { - /** Execution result in JSON format */ - data?: Scalars['JSON'] - /** Execution Logs */ - logs: Scalars['String'] - /** Execution duration in milliseconds */ - duration: Scalars['Float'] - /** Execution status */ - status: LogicFunctionExecutionStatus - /** Execution error in JSON format */ - error?: Scalars['JSON'] - __typename: 'LogicFunctionExecutionResult' -} - - -/** Status of the logic function execution */ -export type LogicFunctionExecutionStatus = 'IDLE' | 'SUCCESS' | 'ERROR' - export interface FeatureFlag { key: FeatureFlagKey value: Scalars['Boolean'] @@ -4177,54 +4177,6 @@ export interface ApplicationConnectionProviderGenqlSelection{ __scalar?: boolean | number } -export interface EnterpriseLicenseInfoDTOGenqlSelection{ - isValid?: boolean | number - licensee?: boolean | number - expiresAt?: boolean | number - subscriptionId?: boolean | number - __typename?: boolean | number - __scalar?: boolean | number -} - -export interface EnterpriseSubscriptionStatusDTOGenqlSelection{ - status?: boolean | number - licensee?: boolean | number - expiresAt?: boolean | number - cancelAt?: boolean | number - currentPeriodEnd?: boolean | number - isCancellationScheduled?: boolean | number - __typename?: boolean | number - __scalar?: boolean | number -} - -export interface ApprovedAccessDomainGenqlSelection{ - id?: boolean | number - domain?: boolean | number - isValidated?: boolean | number - createdAt?: boolean | number - __typename?: boolean | number - __scalar?: boolean | number -} - -export interface FileWithSignedUrlGenqlSelection{ - id?: boolean | number - path?: boolean | number - size?: boolean | number - createdAt?: boolean | number - url?: boolean | number - __typename?: boolean | number - __scalar?: boolean | number -} - -export interface FileUploadTargetGenqlSelection{ - fileId?: boolean | number - uploadUrl?: boolean | number - contentType?: boolean | number - expiresAt?: boolean | number - __typename?: boolean | number - __scalar?: boolean | number -} - export interface BillingSubscriptionSchedulePhaseItemGenqlSelection{ price?: boolean | number quantity?: boolean | number @@ -4314,13 +4266,6 @@ export interface BillingSubscriptionItemGenqlSelection{ __scalar?: boolean | number } -export interface BillingCustomerGenqlSelection{ - id?: boolean | number - hasPaymentMethod?: boolean | number - __typename?: boolean | number - __scalar?: boolean | number -} - export interface BillingSubscriptionGenqlSelection{ id?: boolean | number status?: boolean | number @@ -4334,6 +4279,76 @@ export interface BillingSubscriptionGenqlSelection{ __scalar?: boolean | number } +export interface BillingCustomerGenqlSelection{ + id?: boolean | number + hasPaymentMethod?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + +export interface LogicFunctionExecutionResultGenqlSelection{ + /** Execution result in JSON format */ + data?: boolean | number + /** Execution Logs */ + logs?: boolean | number + /** Execution duration in milliseconds */ + duration?: boolean | number + /** Execution status */ + status?: boolean | number + /** Execution error in JSON format */ + error?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + +export interface EnterpriseLicenseInfoDTOGenqlSelection{ + isValid?: boolean | number + licensee?: boolean | number + expiresAt?: boolean | number + subscriptionId?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + +export interface EnterpriseSubscriptionStatusDTOGenqlSelection{ + status?: boolean | number + licensee?: boolean | number + expiresAt?: boolean | number + cancelAt?: boolean | number + currentPeriodEnd?: boolean | number + isCancellationScheduled?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + +export interface ApprovedAccessDomainGenqlSelection{ + id?: boolean | number + domain?: boolean | number + isValidated?: boolean | number + createdAt?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + +export interface FileWithSignedUrlGenqlSelection{ + id?: boolean | number + path?: boolean | number + size?: boolean | number + createdAt?: boolean | number + url?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + +export interface FileUploadTargetGenqlSelection{ + fileId?: boolean | number + uploadUrl?: boolean | number + contentType?: boolean | number + expiresAt?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + export interface BillingEndTrialPeriodGenqlSelection{ /** Updated subscription status */ status?: boolean | number @@ -4496,21 +4511,6 @@ export interface EventSubscriptionGenqlSelection{ __scalar?: boolean | number } -export interface LogicFunctionExecutionResultGenqlSelection{ - /** Execution result in JSON format */ - data?: boolean | number - /** Execution Logs */ - logs?: boolean | number - /** Execution duration in milliseconds */ - duration?: boolean | number - /** Execution status */ - status?: boolean | number - /** Execution error in JSON format */ - error?: boolean | number - __typename?: boolean | number - __scalar?: boolean | number -} - export interface FeatureFlagGenqlSelection{ key?: boolean | number value?: boolean | number @@ -7325,46 +7325,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null - const EnterpriseLicenseInfoDTO_possibleTypes: string[] = ['EnterpriseLicenseInfoDTO'] - export const isEnterpriseLicenseInfoDTO = (obj?: { __typename?: any } | null): obj is EnterpriseLicenseInfoDTO => { - if (!obj?.__typename) throw new Error('__typename is missing in "isEnterpriseLicenseInfoDTO"') - return EnterpriseLicenseInfoDTO_possibleTypes.includes(obj.__typename) - } - - - - const EnterpriseSubscriptionStatusDTO_possibleTypes: string[] = ['EnterpriseSubscriptionStatusDTO'] - export const isEnterpriseSubscriptionStatusDTO = (obj?: { __typename?: any } | null): obj is EnterpriseSubscriptionStatusDTO => { - if (!obj?.__typename) throw new Error('__typename is missing in "isEnterpriseSubscriptionStatusDTO"') - return EnterpriseSubscriptionStatusDTO_possibleTypes.includes(obj.__typename) - } - - - - const ApprovedAccessDomain_possibleTypes: string[] = ['ApprovedAccessDomain'] - export const isApprovedAccessDomain = (obj?: { __typename?: any } | null): obj is ApprovedAccessDomain => { - if (!obj?.__typename) throw new Error('__typename is missing in "isApprovedAccessDomain"') - return ApprovedAccessDomain_possibleTypes.includes(obj.__typename) - } - - - - const FileWithSignedUrl_possibleTypes: string[] = ['FileWithSignedUrl'] - export const isFileWithSignedUrl = (obj?: { __typename?: any } | null): obj is FileWithSignedUrl => { - if (!obj?.__typename) throw new Error('__typename is missing in "isFileWithSignedUrl"') - return FileWithSignedUrl_possibleTypes.includes(obj.__typename) - } - - - - const FileUploadTarget_possibleTypes: string[] = ['FileUploadTarget'] - export const isFileUploadTarget = (obj?: { __typename?: any } | null): obj is FileUploadTarget => { - if (!obj?.__typename) throw new Error('__typename is missing in "isFileUploadTarget"') - return FileUploadTarget_possibleTypes.includes(obj.__typename) - } - - - const BillingSubscriptionSchedulePhaseItem_possibleTypes: string[] = ['BillingSubscriptionSchedulePhaseItem'] export const isBillingSubscriptionSchedulePhaseItem = (obj?: { __typename?: any } | null): obj is BillingSubscriptionSchedulePhaseItem => { if (!obj?.__typename) throw new Error('__typename is missing in "isBillingSubscriptionSchedulePhaseItem"') @@ -7445,6 +7405,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null + const BillingSubscription_possibleTypes: string[] = ['BillingSubscription'] + export const isBillingSubscription = (obj?: { __typename?: any } | null): obj is BillingSubscription => { + if (!obj?.__typename) throw new Error('__typename is missing in "isBillingSubscription"') + return BillingSubscription_possibleTypes.includes(obj.__typename) + } + + + const BillingCustomer_possibleTypes: string[] = ['BillingCustomer'] export const isBillingCustomer = (obj?: { __typename?: any } | null): obj is BillingCustomer => { if (!obj?.__typename) throw new Error('__typename is missing in "isBillingCustomer"') @@ -7453,10 +7421,50 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null - const BillingSubscription_possibleTypes: string[] = ['BillingSubscription'] - export const isBillingSubscription = (obj?: { __typename?: any } | null): obj is BillingSubscription => { - if (!obj?.__typename) throw new Error('__typename is missing in "isBillingSubscription"') - return BillingSubscription_possibleTypes.includes(obj.__typename) + const LogicFunctionExecutionResult_possibleTypes: string[] = ['LogicFunctionExecutionResult'] + export const isLogicFunctionExecutionResult = (obj?: { __typename?: any } | null): obj is LogicFunctionExecutionResult => { + if (!obj?.__typename) throw new Error('__typename is missing in "isLogicFunctionExecutionResult"') + return LogicFunctionExecutionResult_possibleTypes.includes(obj.__typename) + } + + + + const EnterpriseLicenseInfoDTO_possibleTypes: string[] = ['EnterpriseLicenseInfoDTO'] + export const isEnterpriseLicenseInfoDTO = (obj?: { __typename?: any } | null): obj is EnterpriseLicenseInfoDTO => { + if (!obj?.__typename) throw new Error('__typename is missing in "isEnterpriseLicenseInfoDTO"') + return EnterpriseLicenseInfoDTO_possibleTypes.includes(obj.__typename) + } + + + + const EnterpriseSubscriptionStatusDTO_possibleTypes: string[] = ['EnterpriseSubscriptionStatusDTO'] + export const isEnterpriseSubscriptionStatusDTO = (obj?: { __typename?: any } | null): obj is EnterpriseSubscriptionStatusDTO => { + if (!obj?.__typename) throw new Error('__typename is missing in "isEnterpriseSubscriptionStatusDTO"') + return EnterpriseSubscriptionStatusDTO_possibleTypes.includes(obj.__typename) + } + + + + const ApprovedAccessDomain_possibleTypes: string[] = ['ApprovedAccessDomain'] + export const isApprovedAccessDomain = (obj?: { __typename?: any } | null): obj is ApprovedAccessDomain => { + if (!obj?.__typename) throw new Error('__typename is missing in "isApprovedAccessDomain"') + return ApprovedAccessDomain_possibleTypes.includes(obj.__typename) + } + + + + const FileWithSignedUrl_possibleTypes: string[] = ['FileWithSignedUrl'] + export const isFileWithSignedUrl = (obj?: { __typename?: any } | null): obj is FileWithSignedUrl => { + if (!obj?.__typename) throw new Error('__typename is missing in "isFileWithSignedUrl"') + return FileWithSignedUrl_possibleTypes.includes(obj.__typename) + } + + + + const FileUploadTarget_possibleTypes: string[] = ['FileUploadTarget'] + export const isFileUploadTarget = (obj?: { __typename?: any } | null): obj is FileUploadTarget => { + if (!obj?.__typename) throw new Error('__typename is missing in "isFileUploadTarget"') + return FileUploadTarget_possibleTypes.includes(obj.__typename) } @@ -7597,14 +7605,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null - const LogicFunctionExecutionResult_possibleTypes: string[] = ['LogicFunctionExecutionResult'] - export const isLogicFunctionExecutionResult = (obj?: { __typename?: any } | null): obj is LogicFunctionExecutionResult => { - if (!obj?.__typename) throw new Error('__typename is missing in "isLogicFunctionExecutionResult"') - return LogicFunctionExecutionResult_possibleTypes.includes(obj.__typename) - } - - - const FeatureFlag_possibleTypes: string[] = ['FeatureFlag'] export const isFeatureFlag = (obj?: { __typename?: any } | null): obj is FeatureFlag => { if (!obj?.__typename) throw new Error('__typename is missing in "isFeatureFlag"') @@ -9236,6 +9236,12 @@ export const enumSubscriptionStatus = { Unpaid: 'Unpaid' as const } +export const enumLogicFunctionExecutionStatus = { + IDLE: 'IDLE' as const, + SUCCESS: 'SUCCESS' as const, + ERROR: 'ERROR' as const +} + export const enumNavigationMenuItemType = { VIEW: 'VIEW' as const, FOLDER: 'FOLDER' as const, @@ -9260,12 +9266,6 @@ export const enumDatabaseEventAction = { UPSERTED: 'UPSERTED' as const } -export const enumLogicFunctionExecutionStatus = { - IDLE: 'IDLE' as const, - SUCCESS: 'SUCCESS' as const, - ERROR: 'ERROR' as const -} - export const enumFeatureFlagKey = { IS_APP_CLAIMING_ENABLED: 'IS_APP_CLAIMING_ENABLED' as const, IS_UNIQUE_INDEXES_ENABLED: 'IS_UNIQUE_INDEXES_ENABLED' as const, diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index abeb242460..05f466f274 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -44,15 +44,15 @@ export default { 101, 107, 121, - 132, - 133, - 134, - 136, - 145, - 158, - 161, + 127, + 128, + 129, + 131, + 139, + 142, + 160, 163, - 167, + 165, 169, 176, 177, @@ -97,13 +97,13 @@ export default { 1 ], "metadata": [ - 131 + 126 ], "on_BillingLicensedProduct": [ - 140 + 135 ], "on_BillingMeteredProduct": [ - 141 + 136 ], "__typename": [ 1 @@ -1731,16 +1731,16 @@ export default { 168 ], "billingSubscriptions": [ - 144 + 138 ], "installedApplications": [ 50 ], "currentBillingSubscription": [ - 144 + 138 ], "billingCustomer": [ - 143 + 140 ], "billingEntitlements": [ 231 @@ -2707,6 +2707,239 @@ export default { 1 ] }, + "BillingSubscriptionSchedulePhaseItem": { + "price": [ + 1 + ], + "quantity": [ + 16 + ], + "__typename": [ + 1 + ] + }, + "BillingSubscriptionSchedulePhase": { + "start_date": [ + 16 + ], + "end_date": [ + 16 + ], + "items": [ + 124 + ], + "__typename": [ + 1 + ] + }, + "BillingProductMetadata": { + "planKey": [ + 127 + ], + "priceUsageBased": [ + 128 + ], + "productKey": [ + 129 + ], + "__typename": [ + 1 + ] + }, + "BillingPlanKey": {}, + "BillingUsageType": {}, + "BillingProductKey": {}, + "BillingPriceLicensed": { + "recurringInterval": [ + 131 + ], + "unitAmount": [ + 16 + ], + "stripePriceId": [ + 1 + ], + "priceUsageType": [ + 128 + ], + "creditAmount": [ + 16 + ], + "__typename": [ + 1 + ] + }, + "SubscriptionInterval": {}, + "BillingPriceTier": { + "upTo": [ + 16 + ], + "flatAmount": [ + 16 + ], + "unitAmount": [ + 16 + ], + "__typename": [ + 1 + ] + }, + "BillingPriceMetered": { + "tiers": [ + 132 + ], + "recurringInterval": [ + 131 + ], + "stripePriceId": [ + 1 + ], + "priceUsageType": [ + 128 + ], + "__typename": [ + 1 + ] + }, + "BillingProduct": { + "name": [ + 1 + ], + "description": [ + 1 + ], + "images": [ + 1 + ], + "metadata": [ + 126 + ], + "__typename": [ + 1 + ] + }, + "BillingLicensedProduct": { + "name": [ + 1 + ], + "description": [ + 1 + ], + "images": [ + 1 + ], + "metadata": [ + 126 + ], + "prices": [ + 130 + ], + "__typename": [ + 1 + ] + }, + "BillingMeteredProduct": { + "name": [ + 1 + ], + "description": [ + 1 + ], + "images": [ + 1 + ], + "metadata": [ + 126 + ], + "prices": [ + 133 + ], + "__typename": [ + 1 + ] + }, + "BillingSubscriptionItem": { + "id": [ + 4 + ], + "hasReachedCurrentPeriodCap": [ + 3 + ], + "quantity": [ + 16 + ], + "stripePriceId": [ + 1 + ], + "billingProduct": [ + 0 + ], + "__typename": [ + 1 + ] + }, + "BillingSubscription": { + "id": [ + 4 + ], + "status": [ + 139 + ], + "interval": [ + 131 + ], + "billingSubscriptionItems": [ + 137 + ], + "currentPeriodEnd": [ + 6 + ], + "metadata": [ + 5 + ], + "phases": [ + 125 + ], + "cancelAt": [ + 6 + ], + "__typename": [ + 1 + ] + }, + "SubscriptionStatus": {}, + "BillingCustomer": { + "id": [ + 4 + ], + "hasPaymentMethod": [ + 3 + ], + "__typename": [ + 1 + ] + }, + "LogicFunctionExecutionResult": { + "data": [ + 5 + ], + "logs": [ + 1 + ], + "duration": [ + 16 + ], + "status": [ + 142 + ], + "error": [ + 5 + ], + "__typename": [ + 1 + ] + }, + "LogicFunctionExecutionStatus": {}, "EnterpriseLicenseInfoDTO": { "isValid": [ 3 @@ -2801,221 +3034,9 @@ export default { 1 ] }, - "BillingSubscriptionSchedulePhaseItem": { - "price": [ - 1 - ], - "quantity": [ - 16 - ], - "__typename": [ - 1 - ] - }, - "BillingSubscriptionSchedulePhase": { - "start_date": [ - 16 - ], - "end_date": [ - 16 - ], - "items": [ - 129 - ], - "__typename": [ - 1 - ] - }, - "BillingProductMetadata": { - "planKey": [ - 132 - ], - "priceUsageBased": [ - 133 - ], - "productKey": [ - 134 - ], - "__typename": [ - 1 - ] - }, - "BillingPlanKey": {}, - "BillingUsageType": {}, - "BillingProductKey": {}, - "BillingPriceLicensed": { - "recurringInterval": [ - 136 - ], - "unitAmount": [ - 16 - ], - "stripePriceId": [ - 1 - ], - "priceUsageType": [ - 133 - ], - "creditAmount": [ - 16 - ], - "__typename": [ - 1 - ] - }, - "SubscriptionInterval": {}, - "BillingPriceTier": { - "upTo": [ - 16 - ], - "flatAmount": [ - 16 - ], - "unitAmount": [ - 16 - ], - "__typename": [ - 1 - ] - }, - "BillingPriceMetered": { - "tiers": [ - 137 - ], - "recurringInterval": [ - 136 - ], - "stripePriceId": [ - 1 - ], - "priceUsageType": [ - 133 - ], - "__typename": [ - 1 - ] - }, - "BillingProduct": { - "name": [ - 1 - ], - "description": [ - 1 - ], - "images": [ - 1 - ], - "metadata": [ - 131 - ], - "__typename": [ - 1 - ] - }, - "BillingLicensedProduct": { - "name": [ - 1 - ], - "description": [ - 1 - ], - "images": [ - 1 - ], - "metadata": [ - 131 - ], - "prices": [ - 135 - ], - "__typename": [ - 1 - ] - }, - "BillingMeteredProduct": { - "name": [ - 1 - ], - "description": [ - 1 - ], - "images": [ - 1 - ], - "metadata": [ - 131 - ], - "prices": [ - 138 - ], - "__typename": [ - 1 - ] - }, - "BillingSubscriptionItem": { - "id": [ - 4 - ], - "hasReachedCurrentPeriodCap": [ - 3 - ], - "quantity": [ - 16 - ], - "stripePriceId": [ - 1 - ], - "billingProduct": [ - 0 - ], - "__typename": [ - 1 - ] - }, - "BillingCustomer": { - "id": [ - 4 - ], - "hasPaymentMethod": [ - 3 - ], - "__typename": [ - 1 - ] - }, - "BillingSubscription": { - "id": [ - 4 - ], - "status": [ - 145 - ], - "interval": [ - 136 - ], - "billingSubscriptionItems": [ - 142 - ], - "currentPeriodEnd": [ - 6 - ], - "metadata": [ - 5 - ], - "phases": [ - 130 - ], - "cancelAt": [ - 6 - ], - "__typename": [ - 1 - ] - }, - "SubscriptionStatus": {}, "BillingEndTrialPeriod": { "status": [ - 145 + 139 ], "hasPaymentMethod": [ 3 @@ -3029,7 +3050,7 @@ export default { }, "BillingResourceCreditUsage": { "productKey": [ - 134 + 129 ], "periodStart": [ 6 @@ -3058,16 +3079,16 @@ export default { }, "BillingPlan": { "planKey": [ - 132 + 127 ], "baseProducts": [ - 140 + 135 ], "resourceCreditProducts": [ - 140 + 135 ], "meteredProducts": [ - 141 + 136 ], "__typename": [ 1 @@ -3094,10 +3115,10 @@ export default { }, "BillingUpdate": { "currentBillingSubscription": [ - 144 + 138 ], "billingSubscriptions": [ - 144 + 138 ], "__typename": [ 1 @@ -3147,7 +3168,7 @@ export default { 1 ], "result": [ - 154 + 156 ], "__typename": [ 1 @@ -3184,7 +3205,7 @@ export default { 4 ], "type": [ - 158 + 160 ], "name": [ 1 @@ -3217,7 +3238,7 @@ export default { 6 ], "targetRecordIdentifier": [ - 156 + 158 ], "__typename": [ 1 @@ -3243,7 +3264,7 @@ export default { }, "MetadataEvent": { "type": [ - 161 + 163 ], "metadataName": [ 1 @@ -3252,7 +3273,7 @@ export default { 1 ], "properties": [ - 159 + 161 ], "updatedCollectionHash": [ 1 @@ -3264,7 +3285,7 @@ export default { "MetadataEventAction": {}, "ObjectRecordEvent": { "action": [ - 163 + 165 ], "objectNameSingular": [ 1 @@ -3279,7 +3300,7 @@ export default { 1 ], "properties": [ - 159 + 161 ], "__typename": [ 1 @@ -3291,7 +3312,7 @@ export default { 1 ], "objectRecordEvent": [ - 162 + 164 ], "__typename": [ 1 @@ -3302,36 +3323,15 @@ export default { 1 ], "objectRecordEventsWithQueryIds": [ - 164 + 166 ], "metadataEvents": [ - 160 + 162 ], "__typename": [ 1 ] }, - "LogicFunctionExecutionResult": { - "data": [ - 5 - ], - "logs": [ - 1 - ], - "duration": [ - 16 - ], - "status": [ - 167 - ], - "error": [ - 5 - ], - "__typename": [ - 1 - ] - }, - "LogicFunctionExecutionStatus": {}, "FeatureFlag": { "key": [ 169 @@ -6085,10 +6085,10 @@ export default { }, "Query": { "navigationMenuItems": [ - 157 + 159 ], "navigationMenuItem": [ - 157, + 159, { "id": [ 4, @@ -6122,7 +6122,7 @@ export default { } ], "enterpriseSubscriptionStatus": [ - 125 + 144 ], "getViewFilterGroups": [ 56, @@ -6248,7 +6248,7 @@ export default { } ], "getInviteSuggestions": [ - 152 + 154 ], "applicationConnectionProviders": [ 123, @@ -6260,7 +6260,7 @@ export default { } ], "billingPortalSession": [ - 150, + 152, { "returnUrlPath": [ 1 @@ -6271,16 +6271,16 @@ export default { } ], "listPlans": [ - 148 + 150 ], "getResourceCreditUsage": [ - 147 + 149 ], "findWorkspaceInvitations": [ - 154 + 156 ], "getApprovedAccessDomains": [ - 126 + 145 ], "getPageLayoutTabs": [ 119, @@ -7138,7 +7138,7 @@ export default { } ], "createManyNavigationMenuItems": [ - 157, + 159, { "inputs": [ 359, @@ -7147,7 +7147,7 @@ export default { } ], "createNavigationMenuItem": [ - 157, + 159, { "input": [ 359, @@ -7156,7 +7156,7 @@ export default { } ], "updateManyNavigationMenuItems": [ - 157, + 159, { "inputs": [ 360, @@ -7165,7 +7165,7 @@ export default { } ], "updateNavigationMenuItem": [ - 157, + 159, { "input": [ 360, @@ -7174,7 +7174,7 @@ export default { } ], "deleteManyNavigationMenuItems": [ - 157, + 159, { "ids": [ 4, @@ -7183,7 +7183,7 @@ export default { } ], "deleteNavigationMenuItem": [ - 157, + 159, { "id": [ 4, @@ -7192,7 +7192,7 @@ export default { } ], "createFileUpload": [ - 128, + 147, { "filename": [ 1, @@ -7215,7 +7215,7 @@ export default { } ], "completeFileUpload": [ - 127, + 146, { "fileId": [ 1, @@ -7227,10 +7227,10 @@ export default { 3 ], "releaseEnterpriseServerBinding": [ - 124 + 143 ], "setEnterpriseKey": [ - 124, + 143, { "enterpriseKey": [ 1, @@ -7239,7 +7239,7 @@ export default { } ], "uploadEmailAttachmentFile": [ - 127, + 146, { "file": [ 363, @@ -7248,7 +7248,7 @@ export default { } ], "uploadAiChatFile": [ - 127, + 146, { "file": [ 363, @@ -7257,7 +7257,7 @@ export default { } ], "uploadWorkflowFile": [ - 127, + 146, { "file": [ 363, @@ -7266,7 +7266,7 @@ export default { } ], "uploadWorkspaceLogo": [ - 127, + 146, { "file": [ 363, @@ -7275,7 +7275,7 @@ export default { } ], "uploadWorkspaceMemberProfilePicture": [ - 127, + 146, { "file": [ 363, @@ -7284,7 +7284,7 @@ export default { } ], "uploadFilesFieldFile": [ - 127, + 146, { "file": [ 363, @@ -7297,7 +7297,7 @@ export default { } ], "uploadFilesFieldFileByUniversalIdentifier": [ - 127, + 146, { "file": [ 363, @@ -7610,10 +7610,10 @@ export default { } ], "skipSyncEmailOnboardingStep": [ - 153 + 155 ], "triggerInstallAppsOnboardingStep": [ - 153, + 155, { "universalIdentifiers": [ 1, @@ -7639,14 +7639,14 @@ export default { } ], "checkoutSession": [ - 150, + 152, { "recurringInterval": [ - 136, + 131, "SubscriptionInterval!" ], "plan": [ - 132, + 127, "BillingPlanKey!" ], "requirePaymentMethod": [ @@ -7659,14 +7659,14 @@ export default { } ], "createSubscriptionPaymentIntent": [ - 149, + 151, { "recurringInterval": [ - 136, + 131, "SubscriptionInterval!" ], "plan": [ - 132, + 127, "BillingPlanKey!" ], "requirePaymentMethod": [ @@ -7683,22 +7683,22 @@ export default { } ], "createBillingPaymentMethodSetupIntent": [ - 149 + 151 ], "switchSubscriptionInterval": [ - 151 + 153 ], "switchBillingPlan": [ - 151 + 153 ], "cancelSwitchBillingPlan": [ - 151 + 153 ], "cancelSwitchBillingInterval": [ - 151 + 153 ], "setResourceCreditSubscriptionPrice": [ - 151, + 153, { "priceId": [ 1, @@ -7707,10 +7707,10 @@ export default { } ], "endSubscriptionTrialPeriod": [ - 146 + 148 ], "cancelSwitchResourceCreditPrice": [ - 151 + 153 ], "deleteWorkspaceInvitation": [ 1, @@ -7722,7 +7722,7 @@ export default { } ], "resendWorkspaceInvitation": [ - 155, + 157, { "appTokenId": [ 1, @@ -7731,7 +7731,7 @@ export default { } ], "sendInvitations": [ - 155, + 157, { "emails": [ 1, @@ -7743,7 +7743,7 @@ export default { } ], "createApprovedAccessDomain": [ - 126, + 145, { "input": [ 400, @@ -7761,7 +7761,7 @@ export default { } ], "validateApprovedAccessDomain": [ - 126, + 145, { "input": [ 402, @@ -7993,7 +7993,7 @@ export default { } ], "executeOneLogicFunction": [ - 166, + 141, { "input": [ 425, @@ -8989,7 +8989,7 @@ export default { } ], "uploadNewWorkspaceLogo": [ - 127, + 146, { "workspaceId": [ 1, @@ -9458,7 +9458,7 @@ export default { 4 ], "type": [ - 158 + 160 ], "name": [ 1 @@ -12102,7 +12102,7 @@ export default { }, "Subscription": { "onEventSubscription": [ - 165, + 167, { "eventStreamId": [ 1, diff --git a/packages/twenty-docs/developers/extend/apps/logic/connections.mdx b/packages/twenty-docs/developers/extend/apps/logic/connections.mdx index b12f1ff1ad..2d7093d393 100644 --- a/packages/twenty-docs/developers/extend/apps/logic/connections.mdx +++ b/packages/twenty-docs/developers/extend/apps/logic/connections.mdx @@ -42,6 +42,9 @@ export default defineConnectionProvider({ // Optional: provider's RFC 7009 token revocation endpoint, called on disconnect. // revokeEndpoint: 'https://example.com/oauth/revoke', }, + // Optional: a logic function in this app to run right after a connection is + // established. See "Run a logic function on connect". + // onConnectLogicFunction: { universalIdentifier: '3a2b1c0d-...-...' }, }); ``` @@ -87,6 +90,40 @@ https:///auth/apps/callback + + +Some providers hand you data at connect time that you need to persist before the connection is usable — the classic example is Slack, where the OAuth response identifies the workspace's `team_id` that inbound events will be keyed by. Set `onConnectLogicFunction` to reference a logic function in the same app (by its `universalIdentifier`), and it runs right after the `ConnectedAccount` is created. + +```ts src/connection-providers/slack-connection.ts +export default defineConnectionProvider({ + universalIdentifier: '...', + name: 'slack', + displayName: 'Slack', + type: 'oauth', + oauth: { + /* ... */ + }, + // Runs claimSlackTeam after every successful Slack connection. + onConnectLogicFunction: { + universalIdentifier: '3a2b1c0d-1111-4222-8333-444455556666', + }, +}); +``` + +The hook runs **asynchronously in the connecting workspace** (it is enqueued, not awaited), so a slow or failing hook never blocks or breaks the OAuth callback — make it idempotent and handle its own retries. The handler receives: + +```ts +type OnConnectPayload = { + connectionProviderId: string; + connectionProviderName: string; // e.g. 'slack' + connectedAccountId: string; +}; +``` + +From there use `getConnection(connectedAccountId)` to read the fresh access token and call the provider's API (e.g. Slack `auth.test`) or persist a mapping with the [key-value store](/developers/extend/apps/logic/key-value-store). + + + Inside a logic function handler, `listConnections({ providerName })` returns this app's `ConnectedAccount` rows for the given provider, with refreshed access tokens. diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts index 3e396d356b..0252f51d47 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts @@ -196,6 +196,37 @@ describe('manifestValidate', () => { 'Duplicate universal identifiers: 550e8400-e29b-41d4-a716-446655440001', ); }); + + it('should not flag a connection provider referencing a logic function via onConnectLogicFunction as a duplicate', () => { + const logicFunctionId = '550e8400-e29b-41d4-a716-446655440040'; + + const logicFunction = { + universalIdentifier: logicFunctionId, + name: 'onConnect', + sourceHandlerPath: 'src/logic-functions/on-connect.ts', + builtHandlerPath: 'dist/on-connect.js', + builtHandlerChecksum: '00000000-0000-4000-8000-000000000000', + handlerName: 'handler', + } as unknown as Manifest['logicFunctions'][number]; + + const connectionProvider = { + universalIdentifier: '550e8400-e29b-41d4-a716-446655440041', + name: 'slack', + displayName: 'Slack', + type: 'oauth', + oauth: {}, + onConnectLogicFunction: { universalIdentifier: logicFunctionId }, + } as unknown as NonNullable[number]; + + const result = manifestValidate({ + ...validManifest, + logicFunctions: [logicFunction], + connectionProviders: [connectionProvider], + }); + + expect(result.isValid).toBe(true); + expect(result.errors).toHaveLength(0); + }); }); describe('relation field validation', () => { diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts index 08c6231d27..35d822744e 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts @@ -72,7 +72,8 @@ const findUniversalIdentifiers = (obj: object): string[] => { if ( key === 'postInstallLogicFunction' || - key === 'preInstallLogicFunction' + key === 'preInstallLogicFunction' || + key === 'onConnectLogicFunction' ) { continue; } diff --git a/packages/twenty-sdk/src/sdk/define/connection-providers/__tests__/define-connection-provider.spec.ts b/packages/twenty-sdk/src/sdk/define/connection-providers/__tests__/define-connection-provider.spec.ts index b376597f17..af72cf1fee 100644 --- a/packages/twenty-sdk/src/sdk/define/connection-providers/__tests__/define-connection-provider.spec.ts +++ b/packages/twenty-sdk/src/sdk/define/connection-providers/__tests__/define-connection-provider.spec.ts @@ -82,4 +82,28 @@ describe('defineConnectionProvider', () => { expect(result.success).toBe(false); }); + + it('accepts a valid onConnectLogicFunction', () => { + const result = defineConnectionProvider({ + ...baseValidConfig, + onConnectLogicFunction: { + universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061', + }, + }); + + expect(result.success).toBe(true); + expect(result.errors).toEqual([]); + }); + + it('rejects a non-UUID onConnectLogicFunction universalIdentifier', () => { + const result = defineConnectionProvider({ + ...baseValidConfig, + onConnectLogicFunction: { universalIdentifier: 'claim-team-id' }, + }); + + expect(result.success).toBe(false); + expect( + result.errors.some((error) => error.includes('onConnectLogicFunction')), + ).toBe(true); + }); }); diff --git a/packages/twenty-sdk/src/sdk/define/connection-providers/define-connection-provider.ts b/packages/twenty-sdk/src/sdk/define/connection-providers/define-connection-provider.ts index fd9af5f1c3..9d760c31e4 100644 --- a/packages/twenty-sdk/src/sdk/define/connection-providers/define-connection-provider.ts +++ b/packages/twenty-sdk/src/sdk/define/connection-providers/define-connection-provider.ts @@ -36,6 +36,15 @@ export const defineConnectionProvider: DefineEntity< errors.push('Connection provider must have a displayName'); } + if ( + config.onConnectLogicFunction && + !UUID_PATTERN.test(config.onConnectLogicFunction.universalIdentifier) + ) { + errors.push( + `Connection provider onConnectLogicFunction.universalIdentifier "${config.onConnectLogicFunction.universalIdentifier}" must be the UUID universalIdentifier of a logic function in this app.`, + ); + } + if (!config.type) { errors.push("Connection provider must declare a `type` (e.g. 'oauth')"); } else if (!(SUPPORTED_TYPES as readonly string[]).includes(config.type)) { diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-24/2-24-instance-command-fast-1784712843602-add-on-connect-logic-function-to-connection-provider.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-24/2-24-instance-command-fast-1784712843602-add-on-connect-logic-function-to-connection-provider.ts new file mode 100644 index 0000000000..37ab6254e2 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-24/2-24-instance-command-fast-1784712843602-add-on-connect-logic-function-to-connection-provider.ts @@ -0,0 +1,21 @@ +import { type QueryRunner } from 'typeorm'; + +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +import { type FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface'; + +@RegisteredInstanceCommand('2.24.0', 1784712843602) +export class AddOnConnectLogicFunctionToConnectionProviderFastInstanceCommand + implements FastInstanceCommand +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE "core"."connectionProvider" ADD COLUMN IF NOT EXISTS "onConnectLogicFunctionUniversalIdentifier" uuid', + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE "core"."connectionProvider" DROP COLUMN IF EXISTS "onConnectLogicFunctionUniversalIdentifier"', + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts index 6c80fa2a4e..5d18bc6e74 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts @@ -120,6 +120,7 @@ import { AddApplicationIdToKeyValuePairFastInstanceCommand } from './2-23/2-23-i import { BackfillCreatedWorkspaceActivationStatusSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784286705000-backfill-created-workspace-activation-status'; import { UnlistUnclaimedNpmApplicationRegistrationsSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784322591746-unlist-unclaimed-npm-application-registrations'; import { AddStatusesToBillingSubscriptionIndexSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784650048045-add-statuses-to-billing-subscription-index'; +import { AddOnConnectLogicFunctionToConnectionProviderFastInstanceCommand } from './2-24/2-24-instance-command-fast-1784712843602-add-on-connect-logic-function-to-connection-provider'; export const INSTANCE_COMMANDS = [ AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand, @@ -242,4 +243,5 @@ export const INSTANCE_COMMANDS = [ AddApplicationIdToKeyValuePairFastInstanceCommand, AddSdkClientCoreChecksumToApplicationFastInstanceCommand, AddStatusesToBillingSubscriptionIndexSlowInstanceCommand, + AddOnConnectLogicFunctionToConnectionProviderFastInstanceCommand, ]; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/__tests__/from-connection-provider-manifest-to-universal-flat-connection-provider.util.spec.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/__tests__/from-connection-provider-manifest-to-universal-flat-connection-provider.util.spec.ts index 112e656784..c9d7768571 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/__tests__/from-connection-provider-manifest-to-universal-flat-connection-provider.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/__tests__/from-connection-provider-manifest-to-universal-flat-connection-provider.util.spec.ts @@ -50,11 +50,32 @@ describe('fromConnectionProviderManifestToUniversalFlatConnectionProvider', () = tokenRequestContentType: 'json', usePkce: true, }, + onConnectLogicFunctionUniversalIdentifier: null, createdAt: NOW, updatedAt: NOW, }); }); + it('resolves the onConnectLogicFunction universalIdentifier into the flat field when provided', () => { + const onConnectLogicFunctionUniversalIdentifier = + 'c1c1c1c1-c1c1-4c1c-c1c1-c1c1c1c1c1c1'; + + const result = + fromConnectionProviderManifestToUniversalFlatConnectionProvider({ + connectionProviderManifest: buildManifest({ + onConnectLogicFunction: { + universalIdentifier: onConnectLogicFunctionUniversalIdentifier, + }, + }), + applicationUniversalIdentifier: APP_UID, + now: NOW, + }); + + expect(result.onConnectLogicFunctionUniversalIdentifier).toBe( + onConnectLogicFunctionUniversalIdentifier, + ); + }); + it('passes through optional oauth config when provided', () => { const result = fromConnectionProviderManifestToUniversalFlatConnectionProvider({ diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-connection-provider-manifest-to-universal-flat-connection-provider.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-connection-provider-manifest-to-universal-flat-connection-provider.util.ts index 4d0f280a02..599592a564 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-connection-provider-manifest-to-universal-flat-connection-provider.util.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-connection-provider-manifest-to-universal-flat-connection-provider.util.ts @@ -43,6 +43,9 @@ export const fromConnectionProviderManifestToUniversalFlatConnectionProvider = displayName: connectionProviderManifest.displayName, type: connectionProviderManifest.type, oauthConfig, + onConnectLogicFunctionUniversalIdentifier: + connectionProviderManifest.onConnectLogicFunction + ?.universalIdentifier ?? null, createdAt: now, updatedAt: now, }; diff --git a/packages/twenty-server/src/engine/core-modules/application/connection-provider/__tests__/connection-provider-oauth-flow.service.spec.ts b/packages/twenty-server/src/engine/core-modules/application/connection-provider/__tests__/connection-provider-oauth-flow.service.spec.ts index 942844f18b..05c4d3ea48 100644 --- a/packages/twenty-server/src/engine/core-modules/application/connection-provider/__tests__/connection-provider-oauth-flow.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/application/connection-provider/__tests__/connection-provider-oauth-flow.service.spec.ts @@ -19,12 +19,16 @@ import { type ConnectionProviderEntity } from 'src/engine/core-modules/applicati import { ConnectionProviderOAuthFlowService } from 'src/engine/core-modules/application/connection-provider/connection-provider-oauth-flow.service'; import { ConnectionProviderService } from 'src/engine/core-modules/application/connection-provider/connection-provider.service'; import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/jwt-token-type.enum'; +import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util'; import { SECRET_ENCRYPTION_ENVELOPE_V2_PREFIX } from 'src/engine/core-modules/secret-encryption/constants/secret-encryption.constant'; import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; import { ConnectedAccountTokenEncryptionService } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; const FAKE_CIPHER_PREFIX = `${SECRET_ENCRYPTION_ENVELOPE_V2_PREFIX}keyid:`; @@ -47,6 +51,9 @@ describe('ConnectionProviderOAuthFlowService', () => { findOne: jest.Mock; findOneByOrFail: jest.Mock; }; + let workspaceCacheService: { getOrRecompute: jest.Mock }; + let messageQueueService: { add: jest.Mock }; + let exceptionHandlerService: { captureExceptions: jest.Mock }; const baseProvider: ConnectionProviderEntity = { id: 'provider-1', @@ -95,6 +102,13 @@ describe('ConnectionProviderOAuthFlowService', () => { provider: ConnectedAccountProvider.APP, })), }; + workspaceCacheService = { + getOrRecompute: jest.fn(async () => ({ + flatLogicFunctionMaps: { byUniversalIdentifier: {} }, + })), + }; + messageQueueService = { add: jest.fn() }; + exceptionHandlerService = { captureExceptions: jest.fn() }; const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -113,6 +127,18 @@ describe('ConnectionProviderOAuthFlowService', () => { provide: getRepositoryToken(ConnectedAccountEntity), useValue: connectedAccountRepository, }, + { + provide: WorkspaceCacheService, + useValue: workspaceCacheService, + }, + { + provide: getQueueToken(MessageQueue.logicFunctionQueue), + useValue: messageQueueService, + }, + { + provide: ExceptionHandlerService, + useValue: exceptionHandlerService, + }, { // Real prefix/round-trip behavior is asserted in // connected-account-token-encryption.service.spec.ts; here we use a @@ -437,5 +463,107 @@ describe('ConnectionProviderOAuthFlowService', () => { }), ).rejects.toThrow(/state/); }); + + describe('on-connect hook', () => { + const ON_CONNECT_UID = 'c1c1c1c1-c1c1-4c1c-c1c1-c1c1c1c1c1c1'; + + it('does not dispatch a hook when the provider declares none', async () => { + await service.completeAuthorizationFlow({ + code: 'auth_code', + state: 'signed-state', + }); + + expect(workspaceCacheService.getOrRecompute).not.toHaveBeenCalled(); + expect(messageQueueService.add).not.toHaveBeenCalled(); + }); + + it('enqueues the declared on-connect logic function in the connecting workspace', async () => { + connectionProviderService.findOneByIdOrThrow.mockResolvedValue({ + ...baseProvider, + onConnectLogicFunctionUniversalIdentifier: ON_CONNECT_UID, + }); + workspaceCacheService.getOrRecompute.mockResolvedValue({ + flatLogicFunctionMaps: { + byUniversalIdentifier: { + [ON_CONNECT_UID]: { id: 'logic-function-1' }, + }, + }, + }); + + const result = await service.completeAuthorizationFlow({ + code: 'auth_code', + state: 'signed-state', + }); + + expect(workspaceCacheService.getOrRecompute).toHaveBeenCalledWith( + 'workspace-1', + ['flatLogicFunctionMaps'], + ); + expect(messageQueueService.add).toHaveBeenCalledWith( + 'LogicFunctionTriggerJob', + [ + { + logicFunctionId: 'logic-function-1', + workspaceId: 'workspace-1', + payload: { + connectionProviderId: 'provider-1', + connectionProviderName: 'linear', + connectedAccountId: result.connectedAccountId, + }, + }, + ], + { retryLimit: 3 }, + ); + }); + + it('reports to Sentry without failing the connection when the hook function is missing', async () => { + connectionProviderService.findOneByIdOrThrow.mockResolvedValue({ + ...baseProvider, + onConnectLogicFunctionUniversalIdentifier: ON_CONNECT_UID, + }); + workspaceCacheService.getOrRecompute.mockResolvedValue({ + flatLogicFunctionMaps: { byUniversalIdentifier: {} }, + }); + + const result = await service.completeAuthorizationFlow({ + code: 'auth_code', + state: 'signed-state', + }); + + expect(result.connectedAccountId).toBe('new-account-id'); + expect(messageQueueService.add).not.toHaveBeenCalled(); + expect(exceptionHandlerService.captureExceptions).toHaveBeenCalledTimes( + 1, + ); + }); + + it('treats a soft-deleted hook function as missing instead of enqueuing it', async () => { + connectionProviderService.findOneByIdOrThrow.mockResolvedValue({ + ...baseProvider, + onConnectLogicFunctionUniversalIdentifier: ON_CONNECT_UID, + }); + workspaceCacheService.getOrRecompute.mockResolvedValue({ + flatLogicFunctionMaps: { + byUniversalIdentifier: { + [ON_CONNECT_UID]: { + id: 'logic-function-1', + deletedAt: new Date().toISOString(), + }, + }, + }, + }); + + const result = await service.completeAuthorizationFlow({ + code: 'auth_code', + state: 'signed-state', + }); + + expect(result.connectedAccountId).toBe('new-account-id'); + expect(messageQueueService.add).not.toHaveBeenCalled(); + expect(exceptionHandlerService.captureExceptions).toHaveBeenCalledTimes( + 1, + ); + }); + }); }); }); diff --git a/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider-exception-code.enum.ts b/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider-exception-code.enum.ts index 52828cadcc..1279f15a39 100644 --- a/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider-exception-code.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider-exception-code.enum.ts @@ -9,4 +9,5 @@ export enum ConnectionProviderExceptionCode { INVALID_CONNECTION_PROVIDER_INPUT = 'INVALID_CONNECTION_PROVIDER_INPUT', CONNECTION_PROVIDER_NOT_FOUND = 'CONNECTION_PROVIDER_NOT_FOUND', CONNECTION_PROVIDER_NAME_ALREADY_EXISTS = 'CONNECTION_PROVIDER_NAME_ALREADY_EXISTS', + ON_CONNECT_LOGIC_FUNCTION_NOT_FOUND = 'ON_CONNECT_LOGIC_FUNCTION_NOT_FOUND', } diff --git a/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider-oauth-flow.service.ts b/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider-oauth-flow.service.ts index abf176efbf..48a548a952 100644 --- a/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider-oauth-flow.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider-oauth-flow.service.ts @@ -21,11 +21,20 @@ import { exchangeCodeForToken } from 'src/engine/core-modules/application/connec import { generatePkceVerifier } from 'src/engine/core-modules/application/connection-provider/utils/generate-pkce-verifier.util'; import { type AppOAuthStateJwtPayload } from 'src/engine/core-modules/auth/types/app-oauth-state-jwt-payload.type'; import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/jwt-token-type.enum'; +import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; +import { + LogicFunctionTriggerJob, + type LogicFunctionTriggerJobData, +} from 'src/engine/core-modules/logic-function/logic-function-trigger/jobs/logic-function-trigger.job'; +import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; import { ConnectedAccountTokenEncryptionService } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; const STATE_JWT_EXPIRES_IN = '10m'; @@ -63,6 +72,10 @@ export class ConnectionProviderOAuthFlowService { private readonly connectedAccountTokenEncryptionService: ConnectedAccountTokenEncryptionService, @InjectRepository(ConnectedAccountEntity) private readonly connectedAccountRepository: Repository, + @InjectMessageQueue(MessageQueue.logicFunctionQueue) + private readonly messageQueueService: MessageQueueService, + private readonly workspaceCacheService: WorkspaceCacheService, + private readonly exceptionHandlerService: ExceptionHandlerService, ) {} async startAuthorizationFlow( @@ -186,6 +199,12 @@ export class ConnectionProviderOAuthFlowService { statePayload.reconnectingConnectedAccountId, }); + await this.dispatchOnConnectHook({ + provider, + workspaceId: statePayload.workspaceId, + connectedAccountId: connectedAccount.id, + }); + return { connectedAccountId: connectedAccount.id, workspaceId: statePayload.workspaceId, @@ -194,6 +213,67 @@ export class ConnectionProviderOAuthFlowService { }; } + private async dispatchOnConnectHook({ + provider, + workspaceId, + connectedAccountId, + }: { + provider: OAuthConnectionProvider; + workspaceId: string; + connectedAccountId: string; + }): Promise { + const { onConnectLogicFunctionUniversalIdentifier } = provider; + + if (!isDefined(onConnectLogicFunctionUniversalIdentifier)) { + return; + } + + // The on-connect hook is best-effort: the ConnectedAccount is already + // persisted, so a misconfigured or failing hook must not break the OAuth + // callback. We still report failures to Sentry so they don't go unnoticed. + try { + const { flatLogicFunctionMaps } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatLogicFunctionMaps', + ]); + + const flatLogicFunction = + flatLogicFunctionMaps.byUniversalIdentifier[ + onConnectLogicFunctionUniversalIdentifier + ]; + + if ( + !isDefined(flatLogicFunction) || + isDefined(flatLogicFunction.deletedAt) + ) { + throw new ConnectionProviderException( + `Connection provider ${provider.id} references on-connect logic function ${onConnectLogicFunctionUniversalIdentifier}, which was not found in workspace ${workspaceId}.`, + ConnectionProviderExceptionCode.ON_CONNECT_LOGIC_FUNCTION_NOT_FOUND, + ); + } + + await this.messageQueueService.add( + LogicFunctionTriggerJob.name, + [ + { + logicFunctionId: flatLogicFunction.id, + workspaceId, + payload: { + connectionProviderId: provider.id, + connectionProviderName: provider.name, + connectedAccountId, + }, + }, + ], + { retryLimit: 3 }, + ); + } catch (error) { + this.exceptionHandlerService.captureExceptions([error], { + workspace: { id: workspaceId }, + }); + } + } + private async signState(payload: AppOAuthStateJwtPayload): Promise { return this.jwtWrapperService.signAsyncOrThrow(payload, { expiresIn: STATE_JWT_EXPIRES_IN, diff --git a/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider.entity.ts b/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider.entity.ts index a20c09e94e..6d6f06a284 100644 --- a/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider.entity.ts @@ -12,6 +12,7 @@ import { UpdateDateColumn, } from 'typeorm'; +import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator'; import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; @Entity({ name: 'connectionProvider', schema: 'core' }) @@ -39,6 +40,13 @@ export class ConnectionProviderEntity @Column({ nullable: true, type: 'jsonb' }) oauthConfig: StoredOAuthConnectionProviderConfig | null; + @Column({ nullable: true, type: 'uuid' }) + @WasIntroducedInUpgrade({ + upgradeCommandName: + '2.24.0_AddOnConnectLogicFunctionToConnectionProviderFastInstanceCommand_1784712843602', + }) + onConnectLogicFunctionUniversalIdentifier: string | null; + @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; diff --git a/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider.exception.ts b/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider.exception.ts index e71dec49c9..0081737744 100644 --- a/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider.exception.ts @@ -29,6 +29,8 @@ const getConnectionProviderExceptionUserFriendlyMessage = ( return msg`Connection provider not found.`; case ConnectionProviderExceptionCode.CONNECTION_PROVIDER_NAME_ALREADY_EXISTS: return msg`A connection provider with this name already exists for this application.`; + case ConnectionProviderExceptionCode.ON_CONNECT_LOGIC_FUNCTION_NOT_FOUND: + return msg`The logic function to run on connect was not found.`; default: assertUnreachable(code); } diff --git a/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider.module.ts b/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider.module.ts index dd7ff50c7b..fe15e96d0b 100644 --- a/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider.module.ts +++ b/packages/twenty-server/src/engine/core-modules/application/connection-provider/connection-provider.module.ts @@ -14,6 +14,7 @@ import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; import { ConnectedAccountTokenEncryptionModule } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.module'; import { FlatConnectionProviderModule } from 'src/engine/metadata-modules/flat-connection-provider/flat-connection-provider.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @Module({ imports: [ @@ -29,6 +30,7 @@ import { FlatConnectionProviderModule } from 'src/engine/metadata-modules/flat-c TwentyConfigModule, FlatConnectionProviderModule, ConnectedAccountTokenEncryptionModule, + WorkspaceCacheModule, ], providers: [ ConnectionProviderService, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap index 986963deb7..995a1f3710 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap @@ -60,6 +60,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma "displayName", "type", "oauthConfig", + "onConnectLogicFunctionUniversalIdentifier", ], "propertiesToStringify": [ "oauthConfig", diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts index a2a8e17658..d62737d54a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts @@ -1802,6 +1802,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = { toStringify: true, universalProperty: undefined, }, + onConnectLogicFunctionUniversalIdentifier: { + toCompare: true, + toStringify: false, + universalProperty: undefined, + }, createdAt: { toCompare: false, toStringify: false, diff --git a/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-connection-provider.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-connection-provider.integration-spec.ts index 7a31deafea..610f2ba413 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-connection-provider.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-connection-provider.integration-spec.ts @@ -83,6 +83,7 @@ describe('Manifest update - connection providers', () => { tokenRequestContentType: 'json', usePkce: true, }, + onConnectLogicFunctionUniversalIdentifier: null, }); }, 60000); diff --git a/packages/twenty-server/test/integration/metadata/suites/connection-provider/utils/find-connection-providers-by-application.util.ts b/packages/twenty-server/test/integration/metadata/suites/connection-provider/utils/find-connection-providers-by-application.util.ts index e5ddcb39ba..b5d3a5925d 100644 --- a/packages/twenty-server/test/integration/metadata/suites/connection-provider/utils/find-connection-providers-by-application.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/connection-provider/utils/find-connection-providers-by-application.util.ts @@ -9,6 +9,7 @@ type ConnectionProviderRow = { displayName: string; type: string; oauthConfig: StoredOAuthConnectionProviderConfig | null; + onConnectLogicFunctionUniversalIdentifier: string | null; }; export const findConnectionProvidersByApplication = async ( @@ -17,7 +18,7 @@ export const findConnectionProvidersByApplication = async ( return globalThis.testDataSource.query( `SELECT cp.id, cp."universalIdentifier", cp."applicationId", cp."workspaceId", cp.name, cp."displayName", cp.type, - cp."oauthConfig" + cp."oauthConfig", cp."onConnectLogicFunctionUniversalIdentifier" FROM core."connectionProvider" cp JOIN core."application" app ON app.id = cp."applicationId" WHERE app."universalIdentifier" = $1 diff --git a/packages/twenty-shared/src/application/connectionProviderManifestType.ts b/packages/twenty-shared/src/application/connectionProviderManifestType.ts index e45d44efa0..d7a37be78a 100644 --- a/packages/twenty-shared/src/application/connectionProviderManifestType.ts +++ b/packages/twenty-shared/src/application/connectionProviderManifestType.ts @@ -6,4 +6,5 @@ export type ConnectionProviderManifest = SyncableEntityOptions & { displayName: string; type: 'oauth'; oauth: OAuthConnectionProviderConfig; + onConnectLogicFunction?: SyncableEntityOptions; };