From 04d1c2035c56146a44ae8bcc78f21844db444b63 Mon Sep 17 00:00:00 2001
From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com>
Date: Wed, 22 Jul 2026 21:08:01 +0530
Subject: [PATCH] feat(connections): run a logic function on connection
provider connect (#23167)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## What
Adds an optional `onConnectLogicFunctionUniversalIdentifier` field to
the connection provider manifest. When set, the referenced logic
function is dispatched right after an OAuth connection is successfully
established for that provider.
This gives apps a first-class "on connect" hook — e.g. the Slack app can
resolve the workspace's `team_id` via `auth.test` and claim the `team_id
-> workspaceId` mapping in the SERVER key-value store immediately on
connect, instead of racing against later events.
Follow-up to the app key-value store PR (#23089).
## How
- **twenty-shared**: add `onConnectLogicFunctionUniversalIdentifier` to
`ConnectionProviderManifest`.
- **twenty-sdk**: expose the field in `defineConnectionProvider` and
validate it is a UUID `universalIdentifier`.
- **twenty-server**:
- add a nullable `onConnectLogicFunctionUniversalIdentifier` column to
`ConnectionProviderEntity` (+ fast instance command / migration).
- map the field through the
---
.../src/metadata/generated/schema.graphql | 134 ++--
.../src/metadata/generated/schema.ts | 390 +++++------
.../src/metadata/generated/types.ts | 630 +++++++++---------
.../extend/apps/logic/connections.mdx | 37 +
.../__tests__/manifest-validate.spec.ts | 31 +
.../build/manifest/manifest-validate.ts | 3 +-
.../define-connection-provider.spec.ts | 24 +
.../define-connection-provider.ts | 9 +
...t-logic-function-to-connection-provider.ts | 21 +
.../instance-commands.constant.ts | 2 +
...rsal-flat-connection-provider.util.spec.ts | 21 +
...universal-flat-connection-provider.util.ts | 3 +
...ection-provider-oauth-flow.service.spec.ts | 128 ++++
...connection-provider-exception-code.enum.ts | 1 +
.../connection-provider-oauth-flow.service.ts | 80 +++
.../connection-provider.entity.ts | 8 +
.../connection-provider.exception.ts | 2 +
.../connection-provider.module.ts | 2 +
...ompare-and-stringify.constant.spec.ts.snap | 1 +
...configuration-by-metadata-name.constant.ts | 5 +
...te-connection-provider.integration-spec.ts | 1 +
...onnection-providers-by-application.util.ts | 3 +-
.../connectionProviderManifestType.ts | 1 +
23 files changed, 958 insertions(+), 579 deletions(-)
create mode 100644 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
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;
};