diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 9ea567c252..0529fedc91 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -982,6 +982,11 @@ The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404]( """ scalar JSONObject +type SdkClientChecksums { + core: String + metadata: String! +} + type RatioAggregateConfig { fieldMetadataId: UUID! optionValue: String! @@ -3048,6 +3053,7 @@ type MinimalMetadata { type Query { navigationMenuItems: [NavigationMenuItem!]! navigationMenuItem(id: UUID!): NavigationMenuItem + applicationSdkClientChecksums(applicationId: UUID!): SdkClientChecksums enterprisePortalSession(returnUrlPath: String): String enterpriseCheckoutSession(billingInterval: String): String enterpriseSubscriptionStatus: EnterpriseSubscriptionStatusDTO diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 7222517b14..59fb478c1b 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -704,6 +704,12 @@ export interface User { /** Onboarding status */ export type OnboardingStatus = 'PLAN_REQUIRED' | 'WORKSPACE_ACTIVATION' | 'PROFILE_CREATION' | 'SYNC_EMAIL' | 'APPS_INSTALLATION' | 'INVITE_TEAM' | 'COMPLETED' +export interface SdkClientChecksums { + core?: Scalars['String'] + metadata: Scalars['String'] + __typename: 'SdkClientChecksums' +} + export interface RatioAggregateConfig { fieldMetadataId: Scalars['UUID'] optionValue: Scalars['String'] @@ -2704,6 +2710,7 @@ export interface MinimalMetadata { export interface Query { navigationMenuItems: NavigationMenuItem[] navigationMenuItem?: NavigationMenuItem + applicationSdkClientChecksums?: SdkClientChecksums enterprisePortalSession?: Scalars['String'] enterpriseCheckoutSession?: Scalars['String'] enterpriseSubscriptionStatus?: EnterpriseSubscriptionStatusDTO @@ -3770,6 +3777,13 @@ export interface UserGenqlSelection{ __scalar?: boolean | number } +export interface SdkClientChecksumsGenqlSelection{ + core?: boolean | number + metadata?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + export interface RatioAggregateConfigGenqlSelection{ fieldMetadataId?: boolean | number optionValue?: boolean | number @@ -5898,6 +5912,7 @@ export interface MinimalMetadataGenqlSelection{ export interface QueryGenqlSelection{ navigationMenuItems?: NavigationMenuItemGenqlSelection navigationMenuItem?: (NavigationMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} }) + applicationSdkClientChecksums?: (SdkClientChecksumsGenqlSelection & { __args: {applicationId: Scalars['UUID']} }) enterprisePortalSession?: { __args: {returnUrlPath?: (Scalars['String'] | null)} } | boolean | number enterpriseCheckoutSession?: { __args: {billingInterval?: (Scalars['String'] | null)} } | boolean | number enterpriseSubscriptionStatus?: EnterpriseSubscriptionStatusDTOGenqlSelection @@ -6995,6 +7010,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null + const SdkClientChecksums_possibleTypes: string[] = ['SdkClientChecksums'] + export const isSdkClientChecksums = (obj?: { __typename?: any } | null): obj is SdkClientChecksums => { + if (!obj?.__typename) throw new Error('__typename is missing in "isSdkClientChecksums"') + return SdkClientChecksums_possibleTypes.includes(obj.__typename) + } + + + const RatioAggregateConfig_possibleTypes: string[] = ['RatioAggregateConfig'] export const isRatioAggregateConfig = (obj?: { __typename?: any } | null): obj is RatioAggregateConfig => { if (!obj?.__typename) throw new Error('__typename is missing in "isRatioAggregateConfig"') diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index 136e3b66b1..3b7bf97bbe 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -33,56 +33,56 @@ export default { 73, 75, 76, - 81, - 84, - 89, + 82, + 85, 90, - 93, + 91, 94, - 96, - 99, + 95, + 97, 100, - 106, - 120, - 131, + 101, + 107, + 121, 132, 133, - 135, - 144, - 157, - 160, - 162, - 166, - 168, - 175, + 134, + 136, + 145, + 158, + 161, + 163, + 167, + 169, 176, - 183, - 186, - 189, - 201, - 218, - 231, - 237, - 271, - 273, + 177, + 184, + 187, + 190, + 202, + 219, + 232, + 238, + 272, 274, 275, 276, 277, 278, 279, - 286, - 326, + 280, + 287, 327, 328, 329, - 331, - 333, - 345, - 352, - 359, + 330, + 332, + 334, + 346, + 353, 360, - 488 + 361, + 489 ], "types": { "BillingProductDTO": { @@ -96,13 +96,13 @@ export default { 1 ], "metadata": [ - 130 + 131 ], "on_BillingLicensedProduct": [ - 139 + 140 ], "on_BillingMeteredProduct": [ - 140 + 141 ], "__typename": [ 1 @@ -954,10 +954,10 @@ export default { 3 ], "relation": [ - 217 + 218 ], "morphRelations": [ - 217 + 218 ], "object": [ 47 @@ -993,7 +993,7 @@ export default { 4 ], "indexFieldMetadataList": [ - 219 + 220 ], "__typename": [ 1 @@ -1080,10 +1080,10 @@ export default { 45 ], "searchFieldMetadataList": [ - 221 + 222 ], "fields": [ - 228, + 229, { "paging": [ 48, @@ -1096,7 +1096,7 @@ export default { } ], "indexMetadatas": [ - 226, + 227, { "paging": [ 48, @@ -1786,22 +1786,22 @@ export default { 54 ], "featureFlags": [ - 167 + 168 ], "billingSubscriptions": [ - 143 + 144 ], "installedApplications": [ 54 ], "currentBillingSubscription": [ - 143 + 144 ], "billingCustomer": [ - 142 + 143 ], "billingEntitlements": [ - 230 + 231 ], "hasValidSignedEnterpriseKey": [ 6 @@ -1810,7 +1810,7 @@ export default { 6 ], "workspaceUrls": [ - 169 + 170 ], "workspaceCustomApplicationId": [ 1 @@ -1880,7 +1880,7 @@ export default { 20 ], "deletedWorkspaceMembers": [ - 211 + 212 ], "hasPassword": [ 6 @@ -1892,7 +1892,7 @@ export default { 17 ], "availableWorkspaces": [ - 210 + 211 ], "__typename": [ 1 @@ -1900,6 +1900,17 @@ export default { }, "OnboardingStatus": {}, "JSONObject": {}, + "SdkClientChecksums": { + "core": [ + 1 + ], + "metadata": [ + 1 + ], + "__typename": [ + 1 + ] + }, "RatioAggregateConfig": { "fieldMetadataId": [ 3 @@ -1953,19 +1964,19 @@ export default { 1 ], "type": [ - 81 + 82 ], "objectMetadataId": [ 3 ], "gridPosition": [ - 79 + 80 ], "position": [ - 82 + 83 ], "configuration": [ - 87 + 88 ], "conditionalDisplay": [ 7 @@ -1995,13 +2006,13 @@ export default { "WidgetType": {}, "PageLayoutWidgetPosition": { "on_PageLayoutWidgetGridPosition": [ - 83 + 84 ], "on_PageLayoutWidgetVerticalListPosition": [ - 85 + 86 ], "on_PageLayoutWidgetCanvasPosition": [ - 86 + 87 ], "__typename": [ 1 @@ -2009,7 +2020,7 @@ export default { }, "PageLayoutWidgetGridPosition": { "layoutMode": [ - 84 + 85 ], "row": [ 21 @@ -2030,7 +2041,7 @@ export default { "PageLayoutTabLayoutMode": {}, "PageLayoutWidgetVerticalListPosition": { "layoutMode": [ - 84 + 85 ], "index": [ 21 @@ -2041,7 +2052,7 @@ export default { }, "PageLayoutWidgetCanvasPosition": { "layoutMode": [ - 84 + 85 ], "__typename": [ 1 @@ -2049,78 +2060,78 @@ export default { }, "WidgetConfiguration": { "on_AggregateChartConfiguration": [ - 88 + 89 ], "on_StandaloneRichTextConfiguration": [ - 91 - ], - "on_PieChartConfiguration": [ 92 ], + "on_PieChartConfiguration": [ + 93 + ], "on_LineChartConfiguration": [ - 95 + 96 ], "on_IframeConfiguration": [ - 97 - ], - "on_BarChartConfiguration": [ 98 ], - "on_CalendarConfiguration": [ - 101 + "on_BarChartConfiguration": [ + 99 ], - "on_FrontComponentConfiguration": [ + "on_CalendarConfiguration": [ 102 ], - "on_EmailsConfiguration": [ + "on_FrontComponentConfiguration": [ 103 ], - "on_EmailThreadConfiguration": [ + "on_EmailsConfiguration": [ 104 ], - "on_FieldConfiguration": [ + "on_EmailThreadConfiguration": [ 105 ], - "on_FieldRichTextConfiguration": [ - 107 + "on_FieldConfiguration": [ + 106 ], - "on_FieldsConfiguration": [ + "on_FieldRichTextConfiguration": [ 108 ], - "on_FilesConfiguration": [ + "on_FieldsConfiguration": [ 109 ], - "on_NotesConfiguration": [ + "on_FilesConfiguration": [ 110 ], - "on_TasksConfiguration": [ + "on_NotesConfiguration": [ 111 ], - "on_TimelineConfiguration": [ + "on_TasksConfiguration": [ 112 ], - "on_ViewConfiguration": [ + "on_TimelineConfiguration": [ 113 ], - "on_RecordTableConfiguration": [ + "on_ViewConfiguration": [ 114 ], - "on_WorkflowConfiguration": [ + "on_RecordTableConfiguration": [ 115 ], - "on_WorkflowRunConfiguration": [ + "on_WorkflowConfiguration": [ 116 ], - "on_WorkflowVersionConfiguration": [ + "on_WorkflowRunConfiguration": [ 117 ], + "on_WorkflowVersionConfiguration": [ + 118 + ], "__typename": [ 1 ] }, "AggregateChartConfiguration": { "configurationType": [ - 89 + 90 ], "aggregateFieldMetadataId": [ 3 @@ -2135,7 +2146,7 @@ export default { 6 ], "numberFormat": [ - 90 + 91 ], "description": [ 1 @@ -2156,7 +2167,7 @@ export default { 1 ], "ratioAggregateConfig": [ - 77 + 78 ], "__typename": [ 1 @@ -2166,10 +2177,10 @@ export default { "ChartNumberFormat": {}, "StandaloneRichTextConfiguration": { "configurationType": [ - 89 + 90 ], "body": [ - 78 + 79 ], "__typename": [ 1 @@ -2177,7 +2188,7 @@ export default { }, "PieChartConfiguration": { "configurationType": [ - 89 + 90 ], "aggregateFieldMetadataId": [ 3 @@ -2192,10 +2203,10 @@ export default { 1 ], "dateGranularity": [ - 93 + 94 ], "orderBy": [ - 94 + 95 ], "manualSortOrder": [ 1 @@ -2238,7 +2249,7 @@ export default { "GraphOrderBy": {}, "LineChartConfiguration": { "configurationType": [ - 89 + 90 ], "aggregateFieldMetadataId": [ 3 @@ -2253,10 +2264,10 @@ export default { 1 ], "primaryAxisDateGranularity": [ - 93 + 94 ], "primaryAxisOrderBy": [ - 94 + 95 ], "primaryAxisManualSortOrder": [ 1 @@ -2268,10 +2279,10 @@ export default { 1 ], "secondaryAxisGroupByDateGranularity": [ - 93 + 94 ], "secondaryAxisOrderBy": [ - 94 + 95 ], "secondaryAxisManualSortOrder": [ 1 @@ -2283,7 +2294,7 @@ export default { 6 ], "axisNameDisplay": [ - 96 + 97 ], "displayDataLabel": [ 6 @@ -2325,7 +2336,7 @@ export default { "AxisNameDisplay": {}, "IframeConfiguration": { "configurationType": [ - 89 + 90 ], "url": [ 1 @@ -2336,7 +2347,7 @@ export default { }, "BarChartConfiguration": { "configurationType": [ - 89 + 90 ], "aggregateFieldMetadataId": [ 3 @@ -2351,10 +2362,10 @@ export default { 1 ], "primaryAxisDateGranularity": [ - 93 + 94 ], "primaryAxisOrderBy": [ - 94 + 95 ], "primaryAxisManualSortOrder": [ 1 @@ -2366,10 +2377,10 @@ export default { 1 ], "secondaryAxisGroupByDateGranularity": [ - 93 + 94 ], "secondaryAxisOrderBy": [ - 94 + 95 ], "secondaryAxisManualSortOrder": [ 1 @@ -2381,7 +2392,7 @@ export default { 6 ], "axisNameDisplay": [ - 96 + 97 ], "displayDataLabel": [ 6 @@ -2405,10 +2416,10 @@ export default { 7 ], "groupMode": [ - 99 + 100 ], "layout": [ - 100 + 101 ], "isCumulative": [ 6 @@ -2427,7 +2438,7 @@ export default { "BarChartLayout": {}, "CalendarConfiguration": { "configurationType": [ - 89 + 90 ], "__typename": [ 1 @@ -2435,7 +2446,7 @@ export default { }, "FrontComponentConfiguration": { "configurationType": [ - 89 + 90 ], "frontComponentId": [ 3 @@ -2446,7 +2457,7 @@ export default { }, "EmailsConfiguration": { "configurationType": [ - 89 + 90 ], "__typename": [ 1 @@ -2454,7 +2465,7 @@ export default { }, "EmailThreadConfiguration": { "configurationType": [ - 89 + 90 ], "__typename": [ 1 @@ -2462,13 +2473,13 @@ export default { }, "FieldConfiguration": { "configurationType": [ - 89 + 90 ], "fieldMetadataId": [ 1 ], "fieldDisplayMode": [ - 106 + 107 ], "viewId": [ 1 @@ -2480,7 +2491,7 @@ export default { "FieldDisplayMode": {}, "FieldRichTextConfiguration": { "configurationType": [ - 89 + 90 ], "__typename": [ 1 @@ -2488,7 +2499,7 @@ export default { }, "FieldsConfiguration": { "configurationType": [ - 89 + 90 ], "viewId": [ 1 @@ -2505,7 +2516,7 @@ export default { }, "FilesConfiguration": { "configurationType": [ - 89 + 90 ], "__typename": [ 1 @@ -2513,7 +2524,7 @@ export default { }, "NotesConfiguration": { "configurationType": [ - 89 + 90 ], "__typename": [ 1 @@ -2521,7 +2532,7 @@ export default { }, "TasksConfiguration": { "configurationType": [ - 89 + 90 ], "__typename": [ 1 @@ -2529,7 +2540,7 @@ export default { }, "TimelineConfiguration": { "configurationType": [ - 89 + 90 ], "__typename": [ 1 @@ -2537,7 +2548,7 @@ export default { }, "ViewConfiguration": { "configurationType": [ - 89 + 90 ], "__typename": [ 1 @@ -2545,7 +2556,7 @@ export default { }, "RecordTableConfiguration": { "configurationType": [ - 89 + 90 ], "viewId": [ 1 @@ -2559,7 +2570,7 @@ export default { }, "WorkflowConfiguration": { "configurationType": [ - 89 + 90 ], "__typename": [ 1 @@ -2567,7 +2578,7 @@ export default { }, "WorkflowRunConfiguration": { "configurationType": [ - 89 + 90 ], "__typename": [ 1 @@ -2575,7 +2586,7 @@ export default { }, "WorkflowVersionConfiguration": { "configurationType": [ - 89 + 90 ], "__typename": [ 1 @@ -2598,13 +2609,13 @@ export default { 3 ], "widgets": [ - 80 + 81 ], "icon": [ 1 ], "layoutMode": [ - 84 + 85 ], "createdAt": [ 4 @@ -2633,13 +2644,13 @@ export default { 1 ], "type": [ - 120 + 121 ], "objectMetadataId": [ 3 ], "tabs": [ - 118 + 119 ], "defaultTabToFocusOnMobileAndSidePanelId": [ 3 @@ -2689,7 +2700,7 @@ export default { 1 ], "oauth": [ - 121 + 122 ], "__typename": [ 1 @@ -2808,7 +2819,7 @@ export default { 12 ], "items": [ - 128 + 129 ], "__typename": [ 1 @@ -2816,14 +2827,14 @@ export default { }, "BillingProductMetadata": { "planKey": [ - 131 - ], - "priceUsageBased": [ 132 ], - "productKey": [ + "priceUsageBased": [ 133 ], + "productKey": [ + 134 + ], "__typename": [ 1 ] @@ -2833,7 +2844,7 @@ export default { "BillingProductKey": {}, "BillingPriceLicensed": { "recurringInterval": [ - 135 + 136 ], "unitAmount": [ 12 @@ -2842,7 +2853,7 @@ export default { 1 ], "priceUsageType": [ - 132 + 133 ], "creditAmount": [ 12 @@ -2868,16 +2879,16 @@ export default { }, "BillingPriceMetered": { "tiers": [ - 136 + 137 ], "recurringInterval": [ - 135 + 136 ], "stripePriceId": [ 1 ], "priceUsageType": [ - 132 + 133 ], "__typename": [ 1 @@ -2894,7 +2905,7 @@ export default { 1 ], "metadata": [ - 130 + 131 ], "__typename": [ 1 @@ -2911,10 +2922,10 @@ export default { 1 ], "metadata": [ - 130 + 131 ], "prices": [ - 134 + 135 ], "__typename": [ 1 @@ -2931,10 +2942,10 @@ export default { 1 ], "metadata": [ - 130 + 131 ], "prices": [ - 137 + 138 ], "__typename": [ 1 @@ -2976,13 +2987,13 @@ export default { 3 ], "status": [ - 144 + 145 ], "interval": [ - 135 + 136 ], "billingSubscriptionItems": [ - 141 + 142 ], "currentPeriodEnd": [ 4 @@ -2991,7 +3002,7 @@ export default { 7 ], "phases": [ - 129 + 130 ], "cancelAt": [ 4 @@ -3003,7 +3014,7 @@ export default { "SubscriptionStatus": {}, "BillingEndTrialPeriod": { "status": [ - 144 + 145 ], "hasPaymentMethod": [ 6 @@ -3017,7 +3028,7 @@ export default { }, "BillingResourceCreditUsage": { "productKey": [ - 133 + 134 ], "periodStart": [ 4 @@ -3046,16 +3057,16 @@ export default { }, "BillingPlan": { "planKey": [ - 131 + 132 ], "baseProducts": [ - 139 + 140 ], "resourceCreditProducts": [ - 139 + 140 ], "meteredProducts": [ - 140 + 141 ], "__typename": [ 1 @@ -3082,10 +3093,10 @@ export default { }, "BillingUpdate": { "currentBillingSubscription": [ - 143 + 144 ], "billingSubscriptions": [ - 143 + 144 ], "__typename": [ 1 @@ -3135,7 +3146,7 @@ export default { 1 ], "result": [ - 153 + 154 ], "__typename": [ 1 @@ -3172,7 +3183,7 @@ export default { 3 ], "type": [ - 157 + 158 ], "name": [ 1 @@ -3205,7 +3216,7 @@ export default { 4 ], "targetRecordIdentifier": [ - 155 + 156 ], "__typename": [ 1 @@ -3231,7 +3242,7 @@ export default { }, "MetadataEvent": { "type": [ - 160 + 161 ], "metadataName": [ 1 @@ -3240,7 +3251,7 @@ export default { 1 ], "properties": [ - 158 + 159 ], "updatedCollectionHash": [ 1 @@ -3252,7 +3263,7 @@ export default { "MetadataEventAction": {}, "ObjectRecordEvent": { "action": [ - 162 + 163 ], "objectNameSingular": [ 1 @@ -3267,7 +3278,7 @@ export default { 1 ], "properties": [ - 158 + 159 ], "__typename": [ 1 @@ -3279,7 +3290,7 @@ export default { 1 ], "objectRecordEvent": [ - 161 + 162 ], "__typename": [ 1 @@ -3290,10 +3301,10 @@ export default { 1 ], "objectRecordEventsWithQueryIds": [ - 163 + 164 ], "metadataEvents": [ - 159 + 160 ], "__typename": [ 1 @@ -3310,7 +3321,7 @@ export default { 12 ], "status": [ - 166 + 167 ], "error": [ 7 @@ -3322,7 +3333,7 @@ export default { "LogicFunctionExecutionStatus": {}, "FeatureFlag": { "key": [ - 168 + 169 ], "value": [ 6 @@ -3400,7 +3411,7 @@ export default { 1 ], "versionDistribution": [ - 171 + 172 ], "__typename": [ 1 @@ -3425,10 +3436,10 @@ export default { 1 ], "type": [ - 175 + 176 ], "status": [ - 176 + 177 ], "issuer": [ 1 @@ -3441,7 +3452,7 @@ export default { "SSOIdentityProviderStatus": {}, "AuthProviders": { "sso": [ - 174 + 175 ], "google": [ 6 @@ -3478,10 +3489,10 @@ export default { 3 ], "authProviders": [ - 177 + 178 ], "authBypassProviders": [ - 178 + 179 ], "logo": [ 1 @@ -3490,7 +3501,7 @@ export default { 1 ], "workspaceUrls": [ - 169 + 170 ], "__typename": [ 1 @@ -3529,7 +3540,7 @@ export default { 1 ], "modelFamily": [ - 183 + 184 ], "modelFamilyLabel": [ 1 @@ -3544,7 +3555,7 @@ export default { 12 ], "nativeCapabilities": [ - 181 + 182 ], "isDeprecated": [ 6 @@ -3583,7 +3594,7 @@ export default { 1 ], "trialPeriods": [ - 173 + 174 ], "__typename": [ 1 @@ -3591,7 +3602,7 @@ export default { }, "Support": { "supportDriver": [ - 186 + 187 ], "supportFrontChatId": [ 1 @@ -3617,7 +3628,7 @@ export default { }, "Captcha": { "provider": [ - 189 + 190 ], "siteKey": [ 1 @@ -3651,10 +3662,10 @@ export default { }, "PublicFeatureFlag": { "key": [ - 168 + 169 ], "metadata": [ - 191 + 192 ], "__typename": [ 1 @@ -3679,13 +3690,13 @@ export default { 1 ], "authProviders": [ - 177 + 178 ], "billing": [ - 184 + 185 ], "aiModels": [ - 182 + 183 ], "signInPrefilled": [ 6 @@ -3709,25 +3720,25 @@ export default { 6 ], "support": [ - 185 + 186 ], "isAttachmentPreviewEnabled": [ 6 ], "sentry": [ - 187 - ], - "captcha": [ 188 ], + "captcha": [ + 189 + ], "api": [ - 190 + 191 ], "canManageFeatureFlags": [ 6 ], "publicFeatureFlags": [ - 192 + 193 ], "isMicrosoftMessagingEnabled": [ 6 @@ -3769,7 +3780,7 @@ export default { 1 ], "maintenance": [ - 193 + 194 ], "__typename": [ 1 @@ -3859,7 +3870,7 @@ export default { }, "AppConnection": { "id": [ - 201 + 202 ], "providerName": [ 1 @@ -3911,7 +3922,7 @@ export default { 3 ], "type": [ - 175 + 176 ], "issuer": [ 1 @@ -3920,7 +3931,7 @@ export default { 1 ], "status": [ - 176 + 177 ], "__typename": [ 1 @@ -3939,7 +3950,7 @@ export default { }, "FindAvailableSSOIDP": { "type": [ - 175 + 176 ], "id": [ 3 @@ -3951,10 +3962,10 @@ export default { 1 ], "status": [ - 176 + 177 ], "workspace": [ - 205 + 206 ], "__typename": [ 1 @@ -3965,7 +3976,7 @@ export default { 3 ], "type": [ - 175 + 176 ], "issuer": [ 1 @@ -3974,7 +3985,7 @@ export default { 1 ], "status": [ - 176 + 177 ], "__typename": [ 1 @@ -3982,7 +3993,7 @@ export default { }, "SSOConnection": { "type": [ - 175 + 176 ], "id": [ 3 @@ -3994,7 +4005,7 @@ export default { 1 ], "status": [ - 176 + 177 ], "__typename": [ 1 @@ -4017,13 +4028,13 @@ export default { 1 ], "workspaceUrls": [ - 169 + 170 ], "logo": [ 1 ], "sso": [ - 208 + 209 ], "__typename": [ 1 @@ -4031,10 +4042,10 @@ export default { }, "AvailableWorkspaces": { "availableWorkspacesForSignIn": [ - 209 + 210 ], "availableWorkspacesForSignUp": [ - 209 + 210 ], "__typename": [ 1 @@ -4167,10 +4178,10 @@ export default { 1 ], "objectPermissions": [ - 213 + 214 ], "fieldPermissions": [ - 214 + 215 ], "__typename": [ 1 @@ -4238,7 +4249,7 @@ export default { 1 ], "roles": [ - 215 + 216 ], "manifest": [ 7 @@ -4249,7 +4260,7 @@ export default { }, "Relation": { "type": [ - 218 + 219 ], "sourceObjectMetadata": [ 47 @@ -4355,10 +4366,10 @@ export default { }, "ObjectConnection": { "pageInfo": [ - 223 + 224 ], "edges": [ - 222 + 223 ], "__typename": [ 1 @@ -4377,10 +4388,10 @@ export default { }, "ObjectIndexMetadatasConnection": { "pageInfo": [ - 223 + 224 ], "edges": [ - 225 + 226 ], "__typename": [ 1 @@ -4399,10 +4410,10 @@ export default { }, "ObjectFieldsConnection": { "pageInfo": [ - 223 + 224 ], "edges": [ - 227 + 228 ], "__typename": [ 1 @@ -4410,10 +4421,10 @@ export default { }, "FieldConnection": { "pageInfo": [ - 223 + 224 ], "edges": [ - 227 + 228 ], "__typename": [ 1 @@ -4421,7 +4432,7 @@ export default { }, "BillingEntitlement": { "key": [ - 231 + 232 ], "value": [ 6 @@ -4459,7 +4470,7 @@ export default { 1 ], "records": [ - 232 + 233 ], "isCustomDomainEnabled": [ 6 @@ -4498,7 +4509,7 @@ export default { 1 ], "connectionSecurity": [ - 237 + 238 ], "__typename": [ 1 @@ -4507,13 +4518,13 @@ export default { "EmailConnectionSecurity": {}, "PublicImapSmtpCaldavConnectionParameters": { "IMAP": [ - 236 + 237 ], "SMTP": [ - 236 + 237 ], "CALDAV": [ - 236 + 237 ], "__typename": [ 1 @@ -4569,7 +4580,7 @@ export default { 4 ], "connectionParameters": [ - 238 + 239 ], "__typename": [ 1 @@ -4620,10 +4631,10 @@ export default { }, "AvailableWorkspacesAndAccessTokens": { "tokens": [ - 244 + 245 ], "availableWorkspaces": [ - 210 + 211 ], "__typename": [ 1 @@ -4661,7 +4672,7 @@ export default { }, "WorkspaceUrlsAndId": { "workspaceUrls": [ - 169 + 170 ], "id": [ 3 @@ -4675,7 +4686,7 @@ export default { 32 ], "workspace": [ - 249 + 250 ], "__typename": [ 1 @@ -4708,7 +4719,7 @@ export default { 32 ], "workspaceUrls": [ - 169 + 170 ], "__typename": [ 1 @@ -4752,7 +4763,7 @@ export default { }, "AuthTokens": { "tokens": [ - 244 + 245 ], "__typename": [ 1 @@ -4793,7 +4804,7 @@ export default { 32 ], "workspace": [ - 249 + 250 ], "__typename": [ 1 @@ -4815,7 +4826,7 @@ export default { 1 ], "dailyUsage": [ - 262 + 263 ], "__typename": [ 1 @@ -4823,16 +4834,16 @@ export default { }, "UsageAnalytics": { "usageByUser": [ - 195 + 196 ], "usageByOperationType": [ - 195 + 196 ], "usageByModel": [ - 195 + 196 ], "timeSeries": [ - 262 + 263 ], "periodStart": [ 4 @@ -4841,7 +4852,7 @@ export default { 4 ], "userDailyUsage": [ - 263 + 264 ], "__typename": [ 1 @@ -4940,10 +4951,10 @@ export default { 1 ], "status": [ - 271 + 272 ], "verificationRecords": [ - 269 + 270 ], "verifiedAt": [ 4 @@ -4958,22 +4969,22 @@ export default { 3 ], "visibility": [ - 273 + 274 ], "handle": [ 1 ], "type": [ - 274 + 275 ], "isContactAutoCreationEnabled": [ 6 ], "contactAutoCreationPolicy": [ - 275 + 276 ], "messageFolderImportPolicy": [ - 276 + 277 ], "excludeNonProfessionalEmails": [ 6 @@ -4982,7 +4993,7 @@ export default { 6 ], "pendingGroupEmailsAction": [ - 277 + 278 ], "isSyncEnabled": [ 6 @@ -4991,10 +5002,10 @@ export default { 4 ], "syncStatus": [ - 278 + 279 ], "syncStage": [ - 279 + 280 ], "syncStageStartedAt": [ 4 @@ -5015,7 +5026,7 @@ export default { 4 ], "connectedAccount": [ - 239 + 240 ], "__typename": [ 1 @@ -5030,7 +5041,7 @@ export default { "MessageChannelSyncStage": {}, "CreateEmailGroupChannelOutput": { "messageChannel": [ - 272 + 273 ], "forwardingAddress": [ 1 @@ -5092,7 +5103,7 @@ export default { 21 ], "skipped": [ - 283 + 284 ], "__typename": [ 1 @@ -5115,7 +5126,7 @@ export default { 1 ], "visibility": [ - 286 + 287 ], "__typename": [ 1 @@ -5161,7 +5172,7 @@ export default { 1 ], "location": [ - 288 + 289 ], "__typename": [ 1 @@ -5178,7 +5189,7 @@ export default { 1 ], "connectionSecurity": [ - 237 + 238 ], "__typename": [ 1 @@ -5189,13 +5200,13 @@ export default { 1 ], "IMAP": [ - 290 + 291 ], "SMTP": [ - 290 + 291 ], "CALDAV": [ - 290 + 291 ], "__typename": [ 1 @@ -5215,7 +5226,7 @@ export default { 3 ], "connectionParameters": [ - 291 + 292 ], "__typename": [ 1 @@ -5437,7 +5448,7 @@ export default { 1 ], "series": [ - 300 + 301 ], "xAxisLabel": [ 1 @@ -5452,10 +5463,10 @@ export default { 6 ], "layout": [ - 100 + 101 ], "groupMode": [ - 99 + 100 ], "hasTooManyGroups": [ 6 @@ -5486,7 +5497,7 @@ export default { 1 ], "data": [ - 302 + 303 ], "__typename": [ 1 @@ -5494,7 +5505,7 @@ export default { }, "LineChartData": { "series": [ - 303 + 304 ], "xAxisLabel": [ 1 @@ -5531,7 +5542,7 @@ export default { }, "PieChartData": { "data": [ - 305 + 306 ], "showLegend": [ 6 @@ -5636,13 +5647,13 @@ export default { }, "EventLogQueryResult": { "records": [ - 310 + 311 ], "totalCount": [ 21 ], "pageInfo": [ - 311 + 312 ], "__typename": [ 1 @@ -5706,7 +5717,7 @@ export default { 1 ], "parts": [ - 296 + 297 ], "processedAt": [ 4 @@ -5720,7 +5731,7 @@ export default { }, "AgentChatThread": { "id": [ - 201 + 202 ], "title": [ 1 @@ -5775,7 +5786,7 @@ export default { }, "AiSystemPromptPreview": { "sections": [ - 316 + 317 ], "estimatedTokenCount": [ 21 @@ -5803,7 +5814,7 @@ export default { 21 ], "error": [ - 318 + 319 ], "__typename": [ 1 @@ -5865,10 +5876,10 @@ export default { 3 ], "evaluations": [ - 322 + 323 ], "messages": [ - 314 + 315 ], "createdAt": [ 4 @@ -5899,19 +5910,19 @@ export default { 1 ], "syncStatus": [ - 326 - ], - "syncStage": [ 327 ], - "visibility": [ + "syncStage": [ 328 ], + "visibility": [ + 329 + ], "isContactAutoCreationEnabled": [ 6 ], "contactAutoCreationPolicy": [ - 329 + 330 ], "isSyncEnabled": [ 6 @@ -5962,7 +5973,7 @@ export default { 1 ], "pendingSyncAction": [ - 331 + 332 ], "messageChannelId": [ 3 @@ -5980,7 +5991,7 @@ export default { "MessageFolderPendingSyncAction": {}, "CollectionHash": { "collectionName": [ - 333 + 334 ], "hash": [ 1 @@ -6044,13 +6055,13 @@ export default { }, "MinimalMetadata": { "objectMetadataItems": [ - 334 - ], - "views": [ 335 ], + "views": [ + 336 + ], "collectionHashes": [ - 332 + 333 ], "__typename": [ 1 @@ -6058,10 +6069,10 @@ export default { }, "Query": { "navigationMenuItems": [ - 156 + 157 ], "navigationMenuItem": [ - 156, + 157, { "id": [ 3, @@ -6069,6 +6080,15 @@ export default { ] } ], + "applicationSdkClientChecksums": [ + 77, + { + "applicationId": [ + 3, + "UUID!" + ] + } + ], "enterprisePortalSession": [ 1, { @@ -6086,7 +6106,7 @@ export default { } ], "enterpriseSubscriptionStatus": [ - 124 + 125 ], "getViewFilterGroups": [ 57, @@ -6206,16 +6226,16 @@ export default { 2, { "input": [ - 338, + 339, "GetApiKeyInput!" ] } ], "getInviteSuggestions": [ - 151 + 152 ], "applicationConnectionProviders": [ - 122, + 123, { "applicationId": [ 3, @@ -6224,7 +6244,7 @@ export default { } ], "billingPortalSession": [ - 149, + 150, { "returnUrlPath": [ 1 @@ -6235,19 +6255,19 @@ export default { } ], "listPlans": [ - 147 + 148 ], "getResourceCreditUsage": [ - 146 + 147 ], "findWorkspaceInvitations": [ - 153 + 154 ], "getApprovedAccessDomains": [ - 125 + 126 ], "getPageLayoutTabs": [ - 118, + 119, { "pageLayoutId": [ 1, @@ -6256,7 +6276,7 @@ export default { } ], "getPageLayoutTab": [ - 118, + 119, { "id": [ 1, @@ -6265,18 +6285,18 @@ export default { } ], "getPageLayouts": [ - 119, + 120, { "objectMetadataId": [ 1 ], "pageLayoutType": [ - 120 + 121 ] } ], "getPageLayout": [ - 119, + 120, { "id": [ 1, @@ -6285,7 +6305,7 @@ export default { } ], "getPageLayoutWidgets": [ - 80, + 81, { "pageLayoutTabId": [ 1, @@ -6294,7 +6314,7 @@ export default { } ], "getPageLayoutWidget": [ - 80, + 81, { "id": [ 1, @@ -6309,13 +6329,13 @@ export default { 25, { "input": [ - 339, + 340, "AgentIdInput!" ] } ], "objectRecordCounts": [ - 220 + 221 ], "mostlyEmptyFieldMetadataIds": [ 3, @@ -6336,14 +6356,14 @@ export default { } ], "objects": [ - 224, + 225, { "paging": [ 48, "CursorPaging!" ], "filter": [ - 340, + 341, "ObjectFilter!" ] } @@ -6352,7 +6372,7 @@ export default { 41, { "input": [ - 341, + 342, "LogicFunctionIdInput!" ] } @@ -6364,7 +6384,7 @@ export default { 7, { "input": [ - 341, + 342, "LogicFunctionIdInput!" ] } @@ -6373,7 +6393,7 @@ export default { 1, { "input": [ - 341, + 342, "LogicFunctionIdInput!" ] } @@ -6406,7 +6426,7 @@ export default { 71 ], "getPublicWorkspaceDataByDomain": [ - 179, + 180, { "origin": [ 1 @@ -6414,7 +6434,7 @@ export default { } ], "getPublicWorkspaceDataById": [ - 180, + 181, { "id": [ 3, @@ -6423,7 +6443,7 @@ export default { } ], "findApplicationRegistrationByClientId": [ - 198, + 199, { "clientId": [ 1, @@ -6453,7 +6473,7 @@ export default { } ], "findApplicationRegistrationStats": [ - 172, + 173, { "id": [ 1, @@ -6462,7 +6482,7 @@ export default { } ], "findApplicationRegistrationVariables": [ - 170, + 171, { "applicationRegistrationId": [ 1, @@ -6480,7 +6500,7 @@ export default { } ], "findClaimableApplicationRegistration": [ - 196, + 197, { "sourcePackage": [ 1 @@ -6514,7 +6534,7 @@ export default { } ], "findManyMarketplaceApps": [ - 212, + 213, { "universalIdentifiers": [ 1, @@ -6523,7 +6543,7 @@ export default { } ], "findMarketplaceAppDetail": [ - 216, + 217, { "universalIdentifier": [ 1, @@ -6532,7 +6552,7 @@ export default { } ], "publicMarketplaceApps": [ - 212, + 213, { "isVetted": [ 6, @@ -6541,7 +6561,7 @@ export default { } ], "publicMarketplaceAppDetail": [ - 216, + 217, { "universalIdentifier": [ 1, @@ -6559,7 +6579,7 @@ export default { } ], "fields": [ - 229, + 230, { "paging": [ 48, @@ -6592,22 +6612,22 @@ export default { 29 ], "previewMessageCampaignAudience": [ - 281, + 282, { "input": [ - 342, + 343, "PreviewMessageCampaignAudienceInput!" ] } ], "unsubscribeTopics": [ - 285 + 286 ], "unsubscribePagePreviewUrl": [ 1 ], "myMessageChannels": [ - 272, + 273, { "connectedAccountId": [ 3 @@ -6615,13 +6635,13 @@ export default { } ], "getEmailingDomains": [ - 270 + 271 ], "myConnectedAccounts": [ - 239 + 240 ], "getToolIndex": [ - 295 + 296 ], "getToolInputSchema": [ 7, @@ -6633,10 +6653,10 @@ export default { } ], "webhooks": [ - 294 + 295 ], "webhook": [ - 294, + 295, { "id": [ 3, @@ -6645,7 +6665,7 @@ export default { } ], "myMessageFolders": [ - 330, + 331, { "messageChannelId": [ 3 @@ -6653,7 +6673,7 @@ export default { } ], "myCalendarChannels": [ - 325, + 326, { "connectedAccountId": [ 3 @@ -6661,33 +6681,33 @@ export default { } ], "minimalMetadata": [ - 336 + 337 ], "appConnections": [ - 200, + 201, { "filter": [ - 343 + 344 ] } ], "appConnection": [ - 200, + 201, { "id": [ - 201, + 202, "ID!" ] } ], "findWorkspaceAiStats": [ - 324 + 325 ], "chatThreads": [ - 315 + 316 ], "chatThread": [ - 315, + 316, { "id": [ 3, @@ -6696,7 +6716,7 @@ export default { } ], "chatMessages": [ - 314, + 315, { "threadId": [ 3, @@ -6705,7 +6725,7 @@ export default { } ], "chatStreamCatchupChunks": [ - 319, + 320, { "threadId": [ 3, @@ -6714,13 +6734,13 @@ export default { } ], "getAiSystemPromptPreview": [ - 317 + 318 ], "skills": [ - 313 + 314 ], "skill": [ - 313, + 314, { "id": [ 3, @@ -6729,7 +6749,7 @@ export default { } ], "agentTurns": [ - 323, + 324, { "agentId": [ 3, @@ -6738,7 +6758,7 @@ export default { } ], "checkUserExists": [ - 259, + 260, { "email": [ 1, @@ -6750,7 +6770,7 @@ export default { } ], "checkWorkspaceInviteHashIsValid": [ - 260, + 261, { "inviteHash": [ 1, @@ -6768,7 +6788,7 @@ export default { } ], "checkWorkspaceSubdomainAvailability": [ - 254, + 255, { "subdomain": [ 1, @@ -6777,10 +6797,10 @@ export default { } ], "getWorkspaceCreationDefaults": [ - 255 + 256 ], "validatePasswordResetToken": [ - 252, + 253, { "passwordResetToken": [ 1, @@ -6792,46 +6812,46 @@ export default { 74 ], "getSSOIdentityProviders": [ - 206 + 207 ], "eventLogs": [ - 312, + 313, { "input": [ - 344, + 345, "EventLogQueryInput!" ] } ], "pieChartData": [ - 306, + 307, { "input": [ - 348, + 349, "PieChartDataInput!" ] } ], "lineChartData": [ - 304, + 305, { "input": [ - 349, + 350, "LineChartDataInput!" ] } ], "barChartData": [ - 301, + 302, { "input": [ - 350, + 351, "BarChartDataInput!" ] } ], "getConnectedImapSmtpCaldavAccount": [ - 292, + 293, { "id": [ 3, @@ -6840,7 +6860,7 @@ export default { } ], "getAutoCompleteAddress": [ - 287, + 288, { "address": [ 1, @@ -6859,7 +6879,7 @@ export default { } ], "getAddressDetails": [ - 289, + 290, { "placeId": [ 1, @@ -6872,15 +6892,15 @@ export default { } ], "getUsageAnalytics": [ - 264, + 265, { "input": [ - 351 + 352 ] } ], "findManyPublicDomains": [ - 268 + 269 ], "__typename": [ 1 @@ -6904,10 +6924,10 @@ export default { }, "ObjectFilter": { "and": [ - 340 + 341 ], "or": [ - 340 + 341 ], "id": [ 51 @@ -6939,7 +6959,7 @@ export default { }, "LogicFunctionIdInput": { "id": [ - 201 + 202 ], "__typename": [ 1 @@ -6972,10 +6992,10 @@ export default { }, "EventLogQueryInput": { "table": [ - 345 + 346 ], "filters": [ - 346 + 347 ], "first": [ 21 @@ -6996,7 +7016,7 @@ export default { 1 ], "dateRange": [ - 347 + 348 ], "recordId": [ 1 @@ -7063,7 +7083,7 @@ export default { 1 ], "operationTypes": [ - 352 + 353 ], "__typename": [ 1 @@ -7075,7 +7095,7 @@ export default { 6, { "input": [ - 354, + 355, "AddQuerySubscriptionInput!" ] } @@ -7084,49 +7104,49 @@ export default { 6, { "input": [ - 355, + 356, "RemoveQueryFromEventStreamInput!" ] } ], "createManyNavigationMenuItems": [ - 156, + 157, { "inputs": [ - 356, + 357, "[CreateNavigationMenuItemInput!]!" ] } ], "createNavigationMenuItem": [ - 156, + 157, { "input": [ - 356, + 357, "CreateNavigationMenuItemInput!" ] } ], "updateManyNavigationMenuItems": [ - 156, + 157, { "inputs": [ - 357, + 358, "[UpdateOneNavigationMenuItemInput!]!" ] } ], "updateNavigationMenuItem": [ - 156, + 157, { "input": [ - 357, + 358, "UpdateOneNavigationMenuItemInput!" ] } ], "deleteManyNavigationMenuItems": [ - 156, + 157, { "ids": [ 3, @@ -7135,7 +7155,7 @@ export default { } ], "deleteNavigationMenuItem": [ - 156, + 157, { "id": [ 3, @@ -7144,7 +7164,7 @@ export default { } ], "createFileUpload": [ - 127, + 128, { "filename": [ 1, @@ -7155,7 +7175,7 @@ export default { "Float!" ], "fileFolder": [ - 359, + 360, "FileFolder!" ], "fieldMetadataId": [ @@ -7167,7 +7187,7 @@ export default { } ], "completeFileUpload": [ - 126, + 127, { "fileId": [ 1, @@ -7179,10 +7199,10 @@ export default { 6 ], "releaseEnterpriseServerBinding": [ - 123 + 124 ], "setEnterpriseKey": [ - 123, + 124, { "enterpriseKey": [ 1, @@ -7191,55 +7211,55 @@ export default { } ], "uploadEmailAttachmentFile": [ - 126, + 127, { "file": [ - 360, + 361, "Upload!" ] } ], "uploadAiChatFile": [ - 126, + 127, { "file": [ - 360, + 361, "Upload!" ] } ], "uploadWorkflowFile": [ - 126, + 127, { "file": [ - 360, + 361, "Upload!" ] } ], "uploadWorkspaceLogo": [ - 126, + 127, { "file": [ - 360, + 361, "Upload!" ] } ], "uploadWorkspaceMemberProfilePicture": [ - 126, + 127, { "file": [ - 360, + 361, "Upload!" ] } ], "uploadFilesFieldFile": [ - 126, + 127, { "file": [ - 360, + 361, "Upload!" ], "fieldMetadataId": [ @@ -7249,10 +7269,10 @@ export default { } ], "uploadFilesFieldFileByUniversalIdentifier": [ - 126, + 127, { "file": [ - 360, + 361, "Upload!" ], "fieldMetadataUniversalIdentifier": [ @@ -7265,7 +7285,7 @@ export default { 57, { "input": [ - 361, + 362, "CreateViewFilterGroupInput!" ] } @@ -7278,7 +7298,7 @@ export default { "String!" ], "input": [ - 362, + 363, "UpdateViewFilterGroupInput!" ] } @@ -7305,7 +7325,7 @@ export default { 59, { "input": [ - 363, + 364, "CreateViewFilterInput!" ] } @@ -7314,7 +7334,7 @@ export default { 59, { "input": [ - 364, + 365, "UpdateViewFilterInput!" ] } @@ -7323,7 +7343,7 @@ export default { 59, { "input": [ - 366, + 367, "DeleteViewFilterInput!" ] } @@ -7332,7 +7352,7 @@ export default { 59, { "input": [ - 367, + 368, "DestroyViewFilterInput!" ] } @@ -7341,7 +7361,7 @@ export default { 65, { "input": [ - 368, + 369, "CreateViewInput!" ] } @@ -7354,7 +7374,7 @@ export default { "String!" ], "input": [ - 369, + 370, "UpdateViewInput!" ] } @@ -7381,7 +7401,7 @@ export default { 65, { "input": [ - 370, + 371, "UpsertViewWidgetInput!" ] } @@ -7390,7 +7410,7 @@ export default { 62, { "input": [ - 376, + 377, "CreateViewSortInput!" ] } @@ -7399,7 +7419,7 @@ export default { 62, { "input": [ - 377, + 378, "UpdateViewSortInput!" ] } @@ -7408,7 +7428,7 @@ export default { 6, { "input": [ - 379, + 380, "DeleteViewSortInput!" ] } @@ -7417,7 +7437,7 @@ export default { 6, { "input": [ - 380, + 381, "DestroyViewSortInput!" ] } @@ -7426,7 +7446,7 @@ export default { 55, { "input": [ - 381, + 382, "UpdateViewFieldInput!" ] } @@ -7435,7 +7455,7 @@ export default { 55, { "input": [ - 383, + 384, "CreateViewFieldInput!" ] } @@ -7444,7 +7464,7 @@ export default { 55, { "inputs": [ - 383, + 384, "[CreateViewFieldInput!]!" ] } @@ -7453,7 +7473,7 @@ export default { 55, { "input": [ - 384, + 385, "DeleteViewFieldInput!" ] } @@ -7462,7 +7482,7 @@ export default { 55, { "input": [ - 385, + 386, "DestroyViewFieldInput!" ] } @@ -7471,7 +7491,7 @@ export default { 64, { "input": [ - 386, + 387, "UpdateViewFieldGroupInput!" ] } @@ -7480,7 +7500,7 @@ export default { 64, { "input": [ - 388, + 389, "CreateViewFieldGroupInput!" ] } @@ -7489,7 +7509,7 @@ export default { 64, { "inputs": [ - 388, + 389, "[CreateViewFieldGroupInput!]!" ] } @@ -7498,7 +7518,7 @@ export default { 64, { "input": [ - 389, + 390, "DeleteViewFieldGroupInput!" ] } @@ -7507,7 +7527,7 @@ export default { 64, { "input": [ - 390, + 391, "DestroyViewFieldGroupInput!" ] } @@ -7516,7 +7536,7 @@ export default { 65, { "input": [ - 391, + 392, "UpsertFieldsWidgetInput!" ] } @@ -7525,7 +7545,7 @@ export default { 2, { "input": [ - 394, + 395, "CreateApiKeyInput!" ] } @@ -7534,7 +7554,7 @@ export default { 2, { "input": [ - 395, + 396, "UpdateApiKeyInput!" ] } @@ -7543,7 +7563,7 @@ export default { 2, { "input": [ - 396, + 397, "RevokeApiKeyInput!" ] } @@ -7562,10 +7582,10 @@ export default { } ], "skipSyncEmailOnboardingStep": [ - 152 + 153 ], "triggerInstallAppsOnboardingStep": [ - 152, + 153, { "universalIdentifiers": [ 1, @@ -7591,14 +7611,14 @@ export default { } ], "checkoutSession": [ - 149, + 150, { "recurringInterval": [ - 135, + 136, "SubscriptionInterval!" ], "plan": [ - 131, + 132, "BillingPlanKey!" ], "requirePaymentMethod": [ @@ -7611,14 +7631,14 @@ export default { } ], "createSubscriptionPaymentIntent": [ - 148, + 149, { "recurringInterval": [ - 135, + 136, "SubscriptionInterval!" ], "plan": [ - 131, + 132, "BillingPlanKey!" ], "requirePaymentMethod": [ @@ -7635,22 +7655,22 @@ export default { } ], "createBillingPaymentMethodSetupIntent": [ - 148 + 149 ], "switchSubscriptionInterval": [ - 150 + 151 ], "switchBillingPlan": [ - 150 + 151 ], "cancelSwitchBillingPlan": [ - 150 + 151 ], "cancelSwitchBillingInterval": [ - 150 + 151 ], "setResourceCreditSubscriptionPrice": [ - 150, + 151, { "priceId": [ 1, @@ -7659,10 +7679,10 @@ export default { } ], "endSubscriptionTrialPeriod": [ - 145 + 146 ], "cancelSwitchResourceCreditPrice": [ - 150 + 151 ], "deleteWorkspaceInvitation": [ 1, @@ -7674,7 +7694,7 @@ export default { } ], "resendWorkspaceInvitation": [ - 154, + 155, { "appTokenId": [ 1, @@ -7683,7 +7703,7 @@ export default { } ], "sendInvitations": [ - 154, + 155, { "emails": [ 1, @@ -7695,10 +7715,10 @@ export default { } ], "createApprovedAccessDomain": [ - 125, + 126, { "input": [ - 397, + 398, "CreateApprovedAccessDomainInput!" ] } @@ -7707,38 +7727,38 @@ export default { 6, { "input": [ - 398, + 399, "DeleteApprovedAccessDomainInput!" ] } ], "validateApprovedAccessDomain": [ - 125, + 126, { "input": [ - 399, + 400, "ValidateApprovedAccessDomainInput!" ] } ], "createPageLayoutTab": [ - 118, + 119, { "input": [ - 400, + 401, "CreatePageLayoutTabInput!" ] } ], "updatePageLayoutTab": [ - 118, + 119, { "id": [ 1, "String!" ], "input": [ - 401, + 402, "UpdatePageLayoutTabInput!" ] } @@ -7753,23 +7773,23 @@ export default { } ], "createPageLayout": [ - 119, + 120, { "input": [ - 402, + 403, "CreatePageLayoutInput!" ] } ], "updatePageLayout": [ - 119, + 120, { "id": [ 1, "String!" ], "input": [ - 403, + 404, "UpdatePageLayoutInput!" ] } @@ -7784,20 +7804,20 @@ export default { } ], "updatePageLayoutWithTabsAndWidgets": [ - 119, + 120, { "id": [ 1, "String!" ], "input": [ - 404, + 405, "UpdatePageLayoutWithTabsInput!" ] } ], "resetPageLayoutToDefault": [ - 119, + 120, { "id": [ 1, @@ -7806,7 +7826,7 @@ export default { } ], "resetPageLayoutWidgetToDefault": [ - 80, + 81, { "id": [ 1, @@ -7815,7 +7835,7 @@ export default { } ], "resetPageLayoutTabToDefault": [ - 118, + 119, { "id": [ 1, @@ -7824,23 +7844,23 @@ export default { } ], "createPageLayoutWidget": [ - 80, + 81, { "input": [ - 408, + 409, "CreatePageLayoutWidgetInput!" ] } ], "updatePageLayoutWidget": [ - 80, + 81, { "id": [ 1, "String!" ], "input": [ - 409, + 410, "UpdatePageLayoutWidgetInput!" ] } @@ -7858,7 +7878,7 @@ export default { 25, { "input": [ - 410, + 411, "CreateAgentInput!" ] } @@ -7867,7 +7887,7 @@ export default { 25, { "input": [ - 411, + 412, "UpdateAgentInput!" ] } @@ -7876,7 +7896,7 @@ export default { 25, { "input": [ - 339, + 340, "AgentIdInput!" ] } @@ -7885,7 +7905,7 @@ export default { 47, { "input": [ - 412, + 413, "CreateOneObjectInput!" ] } @@ -7894,7 +7914,7 @@ export default { 47, { "input": [ - 414, + 415, "DeleteOneObjectInput!" ] } @@ -7903,7 +7923,7 @@ export default { 47, { "input": [ - 415, + 416, "UpdateOneObjectInput!" ] } @@ -7912,7 +7932,7 @@ export default { 45, { "input": [ - 417, + 418, "CreateOneIndexInput!" ] } @@ -7921,7 +7941,7 @@ export default { 45, { "input": [ - 420, + 421, "DeleteOneIndexInput!" ] } @@ -7930,7 +7950,7 @@ export default { 41, { "input": [ - 341, + 342, "LogicFunctionIdInput!" ] } @@ -7939,16 +7959,16 @@ export default { 41, { "input": [ - 421, + 422, "CreateLogicFunctionFromSourceInput!" ] } ], "executeOneLogicFunction": [ - 165, + 166, { "input": [ - 422, + 423, "ExecuteOneLogicFunctionInput!" ] } @@ -7957,7 +7977,7 @@ export default { 6, { "input": [ - 423, + 424, "UpdateLogicFunctionFromSourceInput!" ] } @@ -7966,7 +7986,7 @@ export default { 35, { "input": [ - 425, + 426, "CreateCommandMenuItemInput!" ] } @@ -7975,7 +7995,7 @@ export default { 35, { "input": [ - 426, + 427, "UpdateCommandMenuItemInput!" ] } @@ -8002,7 +8022,7 @@ export default { 34, { "input": [ - 427, + 428, "CreateFrontComponentInput!" ] } @@ -8011,7 +8031,7 @@ export default { 34, { "input": [ - 428, + 429, "UpdateFrontComponentInput!" ] } @@ -8029,7 +8049,7 @@ export default { 71, { "data": [ - 430, + 431, "ActivateWorkspaceInput!" ] } @@ -8038,7 +8058,7 @@ export default { 71, { "data": [ - 431, + 432, "UpdateWorkspaceInput!" ] } @@ -8047,7 +8067,7 @@ export default { 71 ], "checkCustomDomainValidRecords": [ - 233 + 234 ], "upgradeApplication": [ 6, @@ -8063,10 +8083,10 @@ export default { } ], "createApplicationRegistration": [ - 197, + 198, { "input": [ - 432, + 433, "CreateApplicationRegistrationInput!" ] } @@ -8075,7 +8095,7 @@ export default { 8, { "input": [ - 433, + 434, "UpdateApplicationRegistrationInput!" ] } @@ -8090,7 +8110,7 @@ export default { } ], "rotateApplicationRegistrationClientSecret": [ - 199, + 200, { "id": [ 1, @@ -8102,7 +8122,7 @@ export default { 5, { "input": [ - 435, + 436, "CreateApplicationRegistrationVariableInput!" ] } @@ -8111,7 +8131,7 @@ export default { 5, { "input": [ - 436, + 437, "UpdateApplicationRegistrationVariableInput!" ] } @@ -8129,7 +8149,7 @@ export default { 8, { "file": [ - 360, + 361, "Upload!" ], "universalIdentifier": [ @@ -8191,7 +8211,7 @@ export default { "UUID!" ], "input": [ - 438, + 439, "UpdateApplicationInput!" ] } @@ -8212,7 +8232,7 @@ export default { 43, { "input": [ - 439, + 440, "CreateOneFieldMetadataInput!" ] } @@ -8221,7 +8241,7 @@ export default { 43, { "input": [ - 441, + 442, "UpdateOneFieldMetadataInput!" ] } @@ -8230,7 +8250,7 @@ export default { 43, { "input": [ - 443, + 444, "DeleteOneFieldInput!" ] } @@ -8239,7 +8259,7 @@ export default { 61, { "input": [ - 444, + 445, "CreateViewGroupInput!" ] } @@ -8248,7 +8268,7 @@ export default { 61, { "inputs": [ - 444, + 445, "[CreateViewGroupInput!]!" ] } @@ -8257,7 +8277,7 @@ export default { 61, { "input": [ - 445, + 446, "UpdateViewGroupInput!" ] } @@ -8266,7 +8286,7 @@ export default { 61, { "inputs": [ - 445, + 446, "[UpdateViewGroupInput!]!" ] } @@ -8275,7 +8295,7 @@ export default { 61, { "input": [ - 447, + 448, "DeleteViewGroupInput!" ] } @@ -8284,7 +8304,7 @@ export default { 61, { "input": [ - 448, + 449, "DestroyViewGroupInput!" ] } @@ -8306,7 +8326,7 @@ export default { 29, { "createRoleInput": [ - 449, + 450, "CreateRoleInput!" ] } @@ -8315,7 +8335,7 @@ export default { 29, { "updateRoleInput": [ - 450, + 451, "UpdateRoleInput!" ] } @@ -8333,7 +8353,7 @@ export default { 16, { "upsertObjectPermissionsInput": [ - 452, + 453, "UpsertObjectPermissionsInput!" ] } @@ -8342,7 +8362,7 @@ export default { 27, { "upsertPermissionFlagsInput": [ - 454, + 455, "UpsertPermissionFlagsInput!" ] } @@ -8351,16 +8371,16 @@ export default { 26, { "upsertFieldPermissionsInput": [ - 455, + 456, "UpsertFieldPermissionsInput!" ] } ], "upsertRowLevelPermissionPredicates": [ - 234, + 235, { "input": [ - 457, + 458, "UpsertRowLevelPermissionPredicatesInput!" ] } @@ -8388,37 +8408,37 @@ export default { } ], "sendEmailViaEmailingDomain": [ - 282, + 283, { "input": [ - 460, + 461, "SendEmailViaDomainInput!" ] } ], "sendMessageCampaign": [ - 284, + 285, { "input": [ - 461, + 462, "SendMessageCampaignInput!" ] } ], "createUnsubscribeTopic": [ - 285, + 286, { "input": [ - 462, + 463, "CreateUnsubscribeTopicInput!" ] } ], "updateUnsubscribeTopic": [ - 285, + 286, { "input": [ - 463, + 464, "UpdateUnsubscribeTopicInput!" ] } @@ -8433,25 +8453,25 @@ export default { } ], "updateMessageChannel": [ - 272, + 273, { "input": [ - 464, + 465, "UpdateMessageChannelInput!" ] } ], "createEmailGroupChannel": [ - 280, + 281, { "input": [ - 466, + 467, "CreateEmailGroupChannelInput!" ] } ], "deleteEmailGroupChannel": [ - 272, + 273, { "id": [ 3, @@ -8460,10 +8480,10 @@ export default { } ], "createEmailingDomain": [ - 270, + 271, { "input": [ - 467, + 468, "CreateEmailingDomainInput!" ] } @@ -8478,7 +8498,7 @@ export default { } ], "verifyEmailingDomain": [ - 270, + 271, { "id": [ 1, @@ -8487,7 +8507,7 @@ export default { } ], "deleteConnectedAccount": [ - 239, + 240, { "id": [ 3, @@ -8496,34 +8516,34 @@ export default { } ], "runAgent": [ - 297, + 298, { "input": [ - 468, + 469, "RunAgentInput!" ] } ], "createWebhook": [ - 294, + 295, { "input": [ - 469, + 470, "CreateWebhookInput!" ] } ], "updateWebhook": [ - 294, + 295, { "input": [ - 470, + 471, "UpdateWebhookInput!" ] } ], "deleteWebhook": [ - 294, + 295, { "id": [ 3, @@ -8532,37 +8552,37 @@ export default { } ], "updateMessageFolder": [ - 330, + 331, { "input": [ - 472, + 473, "UpdateMessageFolderInput!" ] } ], "updateMessageFolders": [ - 330, + 331, { "input": [ - 474, + 475, "UpdateMessageFoldersInput!" ] } ], "updateCalendarChannel": [ - 325, + 326, { "input": [ - 475, + 476, "UpdateCalendarChannelInput!" ] } ], "createChatThread": [ - 315 + 316 ], "sendChatMessage": [ - 320, + 321, { "threadId": [ 3, @@ -8583,13 +8603,13 @@ export default { 1 ], "fileAttachments": [ - 477, + 478, "[FileAttachmentInput!]" ] } ], "retryChatMessage": [ - 320, + 321, { "threadId": [ 3, @@ -8601,7 +8621,7 @@ export default { } ], "answerAgentChatQuestion": [ - 320, + 321, { "threadId": [ 3, @@ -8612,7 +8632,7 @@ export default { "UUID!" ], "answers": [ - 478, + 479, "[AgentChatQuestionAnswerInput!]!" ], "modelId": [ @@ -8630,7 +8650,7 @@ export default { } ], "renameChatThread": [ - 315, + 316, { "id": [ 3, @@ -8643,7 +8663,7 @@ export default { } ], "archiveChatThread": [ - 315, + 316, { "id": [ 3, @@ -8652,7 +8672,7 @@ export default { } ], "unarchiveChatThread": [ - 315, + 316, { "id": [ 3, @@ -8679,25 +8699,25 @@ export default { } ], "createSkill": [ - 313, + 314, { "input": [ - 479, + 480, "CreateSkillInput!" ] } ], "updateSkill": [ - 313, + 314, { "input": [ - 480, + 481, "UpdateSkillInput!" ] } ], "deleteSkill": [ - 313, + 314, { "id": [ 3, @@ -8706,7 +8726,7 @@ export default { } ], "activateSkill": [ - 313, + 314, { "id": [ 3, @@ -8715,7 +8735,7 @@ export default { } ], "deactivateSkill": [ - 313, + 314, { "id": [ 3, @@ -8724,7 +8744,7 @@ export default { } ], "evaluateAgentTurn": [ - 322, + 323, { "turnId": [ 3, @@ -8733,7 +8753,7 @@ export default { } ], "runEvaluationInput": [ - 323, + 324, { "agentId": [ 3, @@ -8746,16 +8766,16 @@ export default { } ], "getAuthorizationUrlForSSO": [ - 247, + 248, { "input": [ - 481, + 482, "GetAuthorizationUrlForSSOInput!" ] } ], "getLoginTokenFromCredentials": [ - 258, + 259, { "email": [ 1, @@ -8781,7 +8801,7 @@ export default { } ], "signIn": [ - 245, + 246, { "email": [ 1, @@ -8803,7 +8823,7 @@ export default { } ], "verifyEmailAndGetLoginToken": [ - 253, + 254, { "emailVerificationToken": [ 1, @@ -8823,7 +8843,7 @@ export default { } ], "verifyEmailAndGetWorkspaceAgnosticToken": [ - 245, + 246, { "emailVerificationToken": [ 1, @@ -8839,7 +8859,7 @@ export default { } ], "getAuthTokensFromOTP": [ - 257, + 258, { "otp": [ 1, @@ -8859,7 +8879,7 @@ export default { } ], "signUp": [ - 245, + 246, { "email": [ 1, @@ -8881,7 +8901,7 @@ export default { } ], "signUpInWorkspace": [ - 250, + 251, { "email": [ 1, @@ -8912,31 +8932,31 @@ export default { } ], "signUpInNewWorkspace": [ - 250, + 251, { "input": [ - 482 + 483 ] } ], "uploadNewWorkspaceLogo": [ - 126, + 127, { "workspaceId": [ 1, "String!" ], "file": [ - 360, + 361, "Upload!" ] } ], "generateTransientToken": [ - 251 + 252 ], "getAuthTokensFromLoginToken": [ - 257, + 258, { "loginToken": [ 1, @@ -8949,7 +8969,7 @@ export default { } ], "authorizeApp": [ - 243, + 244, { "clientId": [ 1, @@ -8971,7 +8991,7 @@ export default { } ], "renewToken": [ - 257, + 258, { "appToken": [ 1, @@ -8980,7 +9000,7 @@ export default { } ], "generateApiKeyToken": [ - 256, + 257, { "apiKeyId": [ 3, @@ -8996,7 +9016,7 @@ export default { 32 ], "emailPasswordResetLink": [ - 246, + 247, { "email": [ 1, @@ -9008,7 +9028,7 @@ export default { } ], "updatePasswordViaResetToken": [ - 248, + 249, { "passwordResetToken": [ 1, @@ -9021,7 +9041,7 @@ export default { } ], "initiateOTPProvisioning": [ - 241, + 242, { "loginToken": [ 1, @@ -9034,10 +9054,10 @@ export default { } ], "initiateOTPProvisioningForAuthenticatedUser": [ - 241 + 242 ], "deleteTwoFactorAuthenticationMethod": [ - 240, + 241, { "twoFactorAuthenticationMethodId": [ 3, @@ -9046,7 +9066,7 @@ export default { } ], "verifyTwoFactorAuthenticationMethodForAuthenticatedUser": [ - 242, + 243, { "otp": [ 1, @@ -9070,7 +9090,7 @@ export default { 6, { "input": [ - 483, + 484, "UpdateWorkspaceMemberSettingsInput!" ] } @@ -9088,7 +9108,7 @@ export default { } ], "resendEmailVerificationToken": [ - 202, + 203, { "email": [ 1, @@ -9101,43 +9121,43 @@ export default { } ], "createOIDCIdentityProvider": [ - 207, + 208, { "input": [ - 484, + 485, "SetupOIDCSsoInput!" ] } ], "createSAMLIdentityProvider": [ - 207, + 208, { "input": [ - 485, + 486, "SetupSAMLSsoInput!" ] } ], "deleteSSOIdentityProvider": [ - 203, + 204, { "input": [ - 486, + 487, "DeleteSsoInput!" ] } ], "editSSOIdentityProvider": [ - 204, + 205, { "input": [ - 487, + 488, "EditSsoInput!" ] } ], "createObjectEvent": [ - 309, + 310, { "event": [ 1, @@ -9157,10 +9177,10 @@ export default { } ], "trackAnalytics": [ - 309, + 310, { "type": [ - 488, + 489, "AnalyticsType!" ], "name": [ @@ -9175,7 +9195,7 @@ export default { } ], "duplicateDashboard": [ - 307, + 308, { "id": [ 3, @@ -9184,7 +9204,7 @@ export default { } ], "impersonate": [ - 261, + 262, { "userId": [ 3, @@ -9197,25 +9217,25 @@ export default { } ], "createCalendarEvent": [ - 299, + 300, { "input": [ - 489, + 490, "CreateCalendarEventInput!" ] } ], "sendEmail": [ - 308, + 309, { "input": [ - 490, + 491, "SendEmailInput!" ] } ], "startChannelSync": [ - 298, + 299, { "connectedAccountId": [ 3, @@ -9224,14 +9244,14 @@ export default { } ], "saveImapSmtpCaldavAccount": [ - 293, + 294, { "handle": [ 1, "String!" ], "connectionParameters": [ - 492, + 493, "EmailAccountConnectionParameters!" ], "id": [ @@ -9240,16 +9260,16 @@ export default { } ], "updateLabPublicFeatureFlag": [ - 167, + 168, { "input": [ - 494, + 495, "UpdateLabPublicFeatureFlagInput!" ] } ], "createPublicDomain": [ - 268, + 269, { "domain": [ 1, @@ -9271,7 +9291,7 @@ export default { } ], "checkPublicDomainValidRecords": [ - 233, + 234, { "domain": [ 1, @@ -9280,7 +9300,7 @@ export default { } ], "createDevelopmentApplication": [ - 265, + 266, { "universalIdentifier": [ 1, @@ -9293,7 +9313,7 @@ export default { } ], "syncApplication": [ - 266, + 267, { "manifest": [ 7, @@ -9305,10 +9325,10 @@ export default { } ], "uploadApplicationFile": [ - 267, + 268, { "file": [ - 360, + 361, "Upload!" ], "applicationUniversalIdentifier": [ @@ -9316,7 +9336,7 @@ export default { "String!" ], "fileFolder": [ - 359, + 360, "FileFolder!" ], "filePath": [ @@ -9389,7 +9409,7 @@ export default { 3 ], "type": [ - 157 + 158 ], "name": [ 1 @@ -9421,7 +9441,7 @@ export default { 3 ], "update": [ - 358 + 359 ], "__typename": [ 1 @@ -9532,7 +9552,7 @@ export default { 3 ], "update": [ - 365 + 366 ], "__typename": [ 1 @@ -9703,20 +9723,20 @@ export default { 3 ], "view": [ - 371 - ], - "viewFields": [ 372 ], - "viewFilters": [ + "viewFields": [ 373 ], - "viewFilterGroups": [ + "viewFilters": [ 374 ], - "viewSorts": [ + "viewFilterGroups": [ 375 ], + "viewSorts": [ + 376 + ], "__typename": [ 1 ] @@ -9861,7 +9881,7 @@ export default { 3 ], "update": [ - 378 + 379 ], "__typename": [ 1 @@ -9899,7 +9919,7 @@ export default { 3 ], "update": [ - 382 + 383 ], "__typename": [ 1 @@ -9975,7 +9995,7 @@ export default { 3 ], "update": [ - 387 + 388 ], "__typename": [ 1 @@ -10039,10 +10059,10 @@ export default { 3 ], "groups": [ - 392 + 393 ], "fields": [ - 393 + 394 ], "__typename": [ 1 @@ -10062,7 +10082,7 @@ export default { 6 ], "fields": [ - 393 + 394 ], "__typename": [ 1 @@ -10168,7 +10188,7 @@ export default { 3 ], "layoutMode": [ - 84 + 85 ], "__typename": [ 1 @@ -10185,7 +10205,7 @@ export default { 1 ], "layoutMode": [ - 84 + 85 ], "__typename": [ 1 @@ -10196,7 +10216,7 @@ export default { 1 ], "type": [ - 120 + 121 ], "objectMetadataId": [ 3 @@ -10210,7 +10230,7 @@ export default { 1 ], "type": [ - 120 + 121 ], "objectMetadataId": [ 3 @@ -10224,13 +10244,13 @@ export default { 1 ], "type": [ - 120 + 121 ], "objectMetadataId": [ 3 ], "tabs": [ - 405 + 406 ], "__typename": [ 1 @@ -10250,10 +10270,10 @@ export default { 1 ], "layoutMode": [ - 84 + 85 ], "widgets": [ - 406 + 407 ], "__typename": [ 1 @@ -10270,13 +10290,13 @@ export default { 1 ], "type": [ - 81 + 82 ], "objectMetadataId": [ 3 ], "gridPosition": [ - 407 + 408 ], "position": [ 7 @@ -10319,13 +10339,13 @@ export default { 1 ], "type": [ - 81 + 82 ], "objectMetadataId": [ 3 ], "gridPosition": [ - 407 + 408 ], "position": [ 7 @@ -10345,13 +10365,13 @@ export default { 1 ], "type": [ - 81 + 82 ], "objectMetadataId": [ 3 ], "gridPosition": [ - 407 + 408 ], "position": [ 7 @@ -10444,7 +10464,7 @@ export default { }, "CreateOneObjectInput": { "object": [ - 413 + 414 ], "__typename": [ 1 @@ -10504,7 +10524,7 @@ export default { }, "UpdateOneObjectInput": { "update": [ - 416 + 417 ], "id": [ 3 @@ -10559,7 +10579,7 @@ export default { }, "CreateOneIndexInput": { "index": [ - 418 + 419 ], "__typename": [ 1 @@ -10570,7 +10590,7 @@ export default { 3 ], "fields": [ - 419 + 420 ], "indexType": [ 46 @@ -10655,7 +10675,7 @@ export default { 3 ], "update": [ - 424 + 425 ], "__typename": [ 1 @@ -10815,7 +10835,7 @@ export default { 3 ], "update": [ - 429 + 430 ], "__typename": [ 1 @@ -10942,7 +10962,7 @@ export default { 1 ], "update": [ - 434 + 435 ], "__typename": [ 1 @@ -10996,7 +11016,7 @@ export default { 1 ], "update": [ - 437 + 438 ], "__typename": [ 1 @@ -11026,7 +11046,7 @@ export default { }, "CreateOneFieldMetadataInput": { "field": [ - 440 + 441 ], "__typename": [ 1 @@ -11099,7 +11119,7 @@ export default { 3 ], "update": [ - 442 + 443 ], "__typename": [ 1 @@ -11194,7 +11214,7 @@ export default { 3 ], "update": [ - 446 + 447 ], "__typename": [ 1 @@ -11279,7 +11299,7 @@ export default { }, "UpdateRoleInput": { "update": [ - 451 + 452 ], "id": [ 3 @@ -11334,7 +11354,7 @@ export default { 3 ], "objectPermissions": [ - 453 + 454 ], "__typename": [ 1 @@ -11376,7 +11396,7 @@ export default { 3 ], "fieldPermissions": [ - 456 + 457 ], "__typename": [ 1 @@ -11407,10 +11427,10 @@ export default { 3 ], "predicates": [ - 458 + 459 ], "predicateGroups": [ - 459 + 460 ], "__typename": [ 1 @@ -11528,7 +11548,7 @@ export default { 1 ], "visibility": [ - 286 + 287 ], "__typename": [ 1 @@ -11545,7 +11565,7 @@ export default { 1 ], "visibility": [ - 286 + 287 ], "__typename": [ 1 @@ -11556,7 +11576,7 @@ export default { 3 ], "update": [ - 465 + 466 ], "__typename": [ 1 @@ -11564,16 +11584,16 @@ export default { }, "UpdateMessageChannelInputUpdates": { "visibility": [ - 273 + 274 ], "isContactAutoCreationEnabled": [ 6 ], "contactAutoCreationPolicy": [ - 275 + 276 ], "messageFolderImportPolicy": [ - 276 + 277 ], "isSyncEnabled": [ 6 @@ -11640,7 +11660,7 @@ export default { 3 ], "update": [ - 471 + 472 ], "__typename": [ 1 @@ -11668,7 +11688,7 @@ export default { 3 ], "update": [ - 473 + 474 ], "__typename": [ 1 @@ -11687,7 +11707,7 @@ export default { 3 ], "update": [ - 473 + 474 ], "__typename": [ 1 @@ -11698,7 +11718,7 @@ export default { 3 ], "update": [ - 476 + 477 ], "__typename": [ 1 @@ -11706,13 +11726,13 @@ export default { }, "UpdateCalendarChannelInputUpdates": { "visibility": [ - 328 + 329 ], "isContactAutoCreationEnabled": [ 6 ], "contactAutoCreationPolicy": [ - 329 + 330 ], "isSyncEnabled": [ 6 @@ -11881,7 +11901,7 @@ export default { 3 ], "status": [ - 176 + 177 ], "__typename": [ 1 @@ -11952,7 +11972,7 @@ export default { 1 ], "files": [ - 491 + 492 ], "__typename": [ 1 @@ -11974,13 +11994,13 @@ export default { 1 ], "IMAP": [ - 493 + 494 ], "SMTP": [ - 493 + 494 ], "CALDAV": [ - 493 + 494 ], "__typename": [ 1 @@ -12000,7 +12020,7 @@ export default { 1 ], "connectionSecurity": [ - 237 + 238 ], "__typename": [ 1 @@ -12019,7 +12039,7 @@ export default { }, "Subscription": { "onEventSubscription": [ - 164, + 165, { "eventStreamId": [ 1, @@ -12028,16 +12048,16 @@ export default { } ], "logicFunctionLogs": [ - 235, + 236, { "input": [ - 496, + 497, "LogicFunctionLogsInput!" ] } ], "onAgentChatEvent": [ - 321, + 322, { "threadId": [ 3, @@ -12046,10 +12066,10 @@ export default { } ], "eventLogsLive": [ - 310, + 311, { "table": [ - 345, + 346, "EventLogTable!" ] } diff --git a/packages/twenty-front-component-renderer/src/remote/worker/constants/SdkClientImportSpecifiers.ts b/packages/twenty-front-component-renderer/src/constants/SdkClientImportSpecifiers.ts similarity index 100% rename from packages/twenty-front-component-renderer/src/remote/worker/constants/SdkClientImportSpecifiers.ts rename to packages/twenty-front-component-renderer/src/constants/SdkClientImportSpecifiers.ts diff --git a/packages/twenty-front-component-renderer/src/remote/worker/utils/__tests__/fetchJavaScriptModuleSourceText.test.ts b/packages/twenty-front-component-renderer/src/host/utils/__tests__/fetchJavaScriptModuleSourceText.spec.ts similarity index 62% rename from packages/twenty-front-component-renderer/src/remote/worker/utils/__tests__/fetchJavaScriptModuleSourceText.test.ts rename to packages/twenty-front-component-renderer/src/host/utils/__tests__/fetchJavaScriptModuleSourceText.spec.ts index 1cb32c9253..298da9138f 100644 --- a/packages/twenty-front-component-renderer/src/remote/worker/utils/__tests__/fetchJavaScriptModuleSourceText.test.ts +++ b/packages/twenty-front-component-renderer/src/host/utils/__tests__/fetchJavaScriptModuleSourceText.spec.ts @@ -1,4 +1,4 @@ -import { fetchJavaScriptModuleSourceText } from '../fetchJavaScriptModuleSourceText'; +import { fetchJavaScriptModuleSourceText } from '@/host/utils/fetchJavaScriptModuleSourceText'; const originalFetch = globalThis.fetch; @@ -14,11 +14,11 @@ describe('fetchJavaScriptModuleSourceText', () => { })) as unknown as typeof fetch; await expect( - fetchJavaScriptModuleSourceText('https://api.twenty.test/component.js'), + fetchJavaScriptModuleSourceText('https://api.twenty.test/core/abc.js'), ).resolves.toBe('module source'); }); - it('should forward headers to fetch', async () => { + it('should forward headers and omit credentials', async () => { const fetchSpy = jest.fn(async () => ({ ok: true, text: async () => '', @@ -26,15 +26,13 @@ describe('fetchJavaScriptModuleSourceText', () => { globalThis.fetch = fetchSpy as unknown as typeof fetch; await fetchJavaScriptModuleSourceText( - 'https://api.twenty.test/component.js', - { - Authorization: 'Bearer token', - }, + 'https://api.twenty.test/core/abc.js', + { Authorization: 'Bearer token' }, ); expect(fetchSpy).toHaveBeenCalledWith( - 'https://api.twenty.test/component.js', - { headers: { Authorization: 'Bearer token' } }, + 'https://api.twenty.test/core/abc.js', + { headers: { Authorization: 'Bearer token' }, credentials: 'omit' }, ); }); @@ -46,7 +44,7 @@ describe('fetchJavaScriptModuleSourceText', () => { })) as unknown as typeof fetch; await expect( - fetchJavaScriptModuleSourceText('https://api.twenty.test/component.js'), + fetchJavaScriptModuleSourceText('https://api.twenty.test/core/abc.js'), ).rejects.toMatchObject({ code: 'FRONT_COMPONENT_MODULE_FETCH_FAILED' }); }); @@ -56,25 +54,11 @@ describe('fetchJavaScriptModuleSourceText', () => { }) as unknown as typeof fetch; await expect( - fetchJavaScriptModuleSourceText('https://api.twenty.test/component.js'), + fetchJavaScriptModuleSourceText('https://api.twenty.test/core/abc.js'), ).rejects.toMatchObject({ code: 'FRONT_COMPONENT_MODULE_FETCH_FAILED', message: - 'Failed to fetch front component module https://api.twenty.test/component.js: Failed to fetch', + 'Failed to fetch SDK client module https://api.twenty.test/core/abc.js: Failed to fetch', }); }); - - it('should include the url and status in the error message', async () => { - globalThis.fetch = jest.fn(async () => ({ - ok: false, - status: 403, - statusText: 'Forbidden', - })) as unknown as typeof fetch; - - await expect( - fetchJavaScriptModuleSourceText('https://api.twenty.test/component.js'), - ).rejects.toThrow( - 'Failed to fetch front component module https://api.twenty.test/component.js: 403 Forbidden', - ); - }); }); diff --git a/packages/twenty-front-component-renderer/src/host/utils/fetchJavaScriptModuleSourceText.ts b/packages/twenty-front-component-renderer/src/host/utils/fetchJavaScriptModuleSourceText.ts new file mode 100644 index 0000000000..63e9388745 --- /dev/null +++ b/packages/twenty-front-component-renderer/src/host/utils/fetchJavaScriptModuleSourceText.ts @@ -0,0 +1,33 @@ +import { CustomError } from 'twenty-shared/utils'; + +// Plain fetch for a JavaScript module served directly from a content-addressed, +// immutable URL. The browser HTTP cache handles both reuse and checksum-based +// invalidation (a new checksum yields a new URL), so no host-side CacheStorage +// layer is needed here — that layer exists only to cache front-component +// sources across the rotating presigned URLs they are served through. +export const fetchJavaScriptModuleSourceText = async ( + url: string, + headers?: Record, +): Promise => { + let response: Response; + + try { + response = await fetch(url, { headers, credentials: 'omit' }); + } catch (error) { + throw new CustomError( + `Failed to fetch SDK client module ${url}: ${ + error instanceof Error ? error.message : String(error) + }`, + 'FRONT_COMPONENT_MODULE_FETCH_FAILED', + ); + } + + if (!response.ok) { + throw new CustomError( + `Failed to fetch SDK client module ${url}: ${response.status} ${response.statusText}`, + 'FRONT_COMPONENT_MODULE_FETCH_FAILED', + ); + } + + return response.text(); +}; diff --git a/packages/twenty-front-component-renderer/src/host/utils/fetchSdkClientSources.ts b/packages/twenty-front-component-renderer/src/host/utils/fetchSdkClientSources.ts new file mode 100644 index 0000000000..88acdd96d4 --- /dev/null +++ b/packages/twenty-front-component-renderer/src/host/utils/fetchSdkClientSources.ts @@ -0,0 +1,18 @@ +import { fetchJavaScriptModuleSourceText } from '@/host/utils/fetchJavaScriptModuleSourceText'; +import { type SdkClientSources } from '@/types/SdkClientSources'; +import { type SdkClientUrls } from '@/types/SdkClientUrls'; + +export const fetchSdkClientSources = async ({ + sdkClientUrls, + headers, +}: { + sdkClientUrls: SdkClientUrls; + headers?: Record; +}): Promise => { + const [core, metadata] = await Promise.all([ + fetchJavaScriptModuleSourceText(sdkClientUrls.core, headers), + fetchJavaScriptModuleSourceText(sdkClientUrls.metadata, headers), + ]); + + return { core, metadata }; +}; diff --git a/packages/twenty-front-component-renderer/src/remote/components/FrontComponentWorkerEffect.tsx b/packages/twenty-front-component-renderer/src/remote/components/FrontComponentWorkerEffect.tsx index 1b6d732d36..b8c7a284c1 100644 --- a/packages/twenty-front-component-renderer/src/remote/components/FrontComponentWorkerEffect.tsx +++ b/packages/twenty-front-component-renderer/src/remote/components/FrontComponentWorkerEffect.tsx @@ -1,17 +1,20 @@ import { release, retain } from '@quilted/threads'; import { RemoteReceiver } from '@remote-dom/core/receivers'; import { useEffect, useRef } from 'react'; +import { isDefined } from 'twenty-shared/utils'; import { buildHostFetchPolicyFromFrontComponentUrls } from '@/host/utils/buildHostFetchPolicyFromFrontComponentUrls'; import { createFrontComponentHostThread } from '@/host/utils/createFrontComponentHostThread'; import { createHostFetchEnforcingPolicy } from '@/host/utils/createHostFetchEnforcingPolicy'; import { fetchComponentSource } from '@/host/utils/fetchComponentSource'; +import { fetchSdkClientSources } from '@/host/utils/fetchSdkClientSources'; import { FRONT_COMPONENT_SANDBOX_DOCUMENT } from '@/remote/sandbox/generated/frontComponentSandboxDocument'; import { createFrontComponentSandboxIframe } from '@/remote/sandbox/utils/createFrontComponentSandboxIframe'; import { createFrontComponentSandboxMessageHandler } from '@/remote/sandbox/utils/createFrontComponentSandboxMessageHandler'; import { type FrontComponentThread } from '@/types/FrontComponentThread'; import { type SdkClientUrls } from '@/types/SdkClientUrls'; import { buildAuthorizationHeadersFromAccessToken } from '@/utils/buildAuthorizationHeadersFromAccessToken'; +import { containsSdkClientImportSpecifier } from '@/utils/containsSdkClientImportSpecifier'; type FrontComponentWorkerEffectProps = { componentUrl: string; @@ -77,24 +80,39 @@ export const FrontComponentWorkerEffect = ({ const resolveComponentSourceAndRender = async () => { try { + const authorizationHeaders = buildAuthorizationHeadersFromAccessToken( + applicationAccessToken, + ); + const componentSource = await fetchComponentSource({ url: componentUrl, - headers: buildAuthorizationHeadersFromAccessToken( - applicationAccessToken, - ), + headers: authorizationHeaders, }); if (isCancelled) { return; } + const sdkClientSources = + isDefined(sdkClientUrls) && + containsSdkClientImportSpecifier(componentSource) + ? await fetchSdkClientSources({ + sdkClientUrls, + headers: authorizationHeaders, + }) + : undefined; + + if (isCancelled) { + return; + } + await thread.imports.render(newReceiver.connection, { componentUrl, componentSource, applicationAccessToken, apiUrl, functionsBaseUrl, - sdkClientUrls, + sdkClientSources, hostFetchOrigins: hostFetchPolicy.allowedOrigins, applicationVariables, }); diff --git a/packages/twenty-front-component-renderer/src/remote/worker/utils/__tests__/createSdkClientModuleBlobUrls.test.ts b/packages/twenty-front-component-renderer/src/remote/worker/utils/__tests__/createSdkClientModuleBlobUrls.test.ts new file mode 100644 index 0000000000..06fd80f7e0 --- /dev/null +++ b/packages/twenty-front-component-renderer/src/remote/worker/utils/__tests__/createSdkClientModuleBlobUrls.test.ts @@ -0,0 +1,26 @@ +import { createSdkClientModuleBlobUrls } from '../createSdkClientModuleBlobUrls'; + +describe('createSdkClientModuleBlobUrls', () => { + const createObjectURLMock = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + createObjectURLMock.mockImplementation( + (blob: Blob) => `blob:mock/${(blob as Blob).size}`, + ); + global.URL.createObjectURL = + createObjectURLMock as unknown as typeof URL.createObjectURL; + }); + + it('mints a blob url for each sdk client module source', () => { + const blobUrls = createSdkClientModuleBlobUrls({ + core: 'export const core = 1;', + metadata: 'export const metadata = 2;', + }); + + expect(createObjectURLMock).toHaveBeenCalledTimes(2); + expect(blobUrls.core).toMatch(/^blob:mock\//); + expect(blobUrls.metadata).toMatch(/^blob:mock\//); + expect(blobUrls.core).not.toEqual(blobUrls.metadata); + }); +}); diff --git a/packages/twenty-front-component-renderer/src/remote/worker/utils/__tests__/fetchSdkClientModulesAsBlobUrls.test.ts b/packages/twenty-front-component-renderer/src/remote/worker/utils/__tests__/fetchSdkClientModulesAsBlobUrls.test.ts deleted file mode 100644 index 84b501202c..0000000000 --- a/packages/twenty-front-component-renderer/src/remote/worker/utils/__tests__/fetchSdkClientModulesAsBlobUrls.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { fetchSdkClientModulesAsBlobUrls } from '../fetchSdkClientModulesAsBlobUrls'; - -const originalFetch = globalThis.fetch; -const originalCreateObjectUrl = URL.createObjectURL; -const originalRevokeObjectUrl = URL.revokeObjectURL; - -const sdkClientUrls = { - core: 'https://api.twenty.test/sdk-client/application-id/core', - metadata: 'https://api.twenty.test/sdk-client/application-id/metadata', -}; - -describe('fetchSdkClientModulesAsBlobUrls', () => { - afterEach(() => { - globalThis.fetch = originalFetch; - URL.createObjectURL = originalCreateObjectUrl; - URL.revokeObjectURL = originalRevokeObjectUrl; - }); - - it('should fetch both sdk modules and return their blob urls', async () => { - const fetchSpy = jest.fn(async (url: string) => ({ - ok: true, - text: async () => `source of ${url}`, - })); - globalThis.fetch = fetchSpy as unknown as typeof fetch; - URL.createObjectURL = jest - .fn() - .mockReturnValueOnce('blob:core-url') - .mockReturnValueOnce('blob:metadata-url'); - - await expect( - fetchSdkClientModulesAsBlobUrls(sdkClientUrls), - ).resolves.toEqual({ - core: 'blob:core-url', - metadata: 'blob:metadata-url', - }); - expect(fetchSpy).toHaveBeenCalledWith(sdkClientUrls.core, { - headers: undefined, - }); - expect(fetchSpy).toHaveBeenCalledWith(sdkClientUrls.metadata, { - headers: undefined, - }); - }); - - it('should forward headers to both module fetches', async () => { - const fetchSpy = jest.fn(async () => ({ - ok: true, - text: async () => '', - })); - globalThis.fetch = fetchSpy as unknown as typeof fetch; - URL.createObjectURL = jest.fn(() => 'blob:mock-url'); - - await fetchSdkClientModulesAsBlobUrls(sdkClientUrls, { - Authorization: 'Bearer token', - }); - - expect(fetchSpy).toHaveBeenCalledWith(sdkClientUrls.core, { - headers: { Authorization: 'Bearer token' }, - }); - expect(fetchSpy).toHaveBeenCalledWith(sdkClientUrls.metadata, { - headers: { Authorization: 'Bearer token' }, - }); - }); - - it('should propagate the fetch error when one module fails to load', async () => { - globalThis.fetch = jest.fn(async (url: string) => ({ - ok: url !== sdkClientUrls.metadata, - status: 500, - statusText: 'Internal Server Error', - text: async () => '', - })) as unknown as typeof fetch; - URL.createObjectURL = jest.fn(() => 'blob:mock-url'); - URL.revokeObjectURL = jest.fn(); - - await expect( - fetchSdkClientModulesAsBlobUrls(sdkClientUrls), - ).rejects.toMatchObject({ - code: 'FRONT_COMPONENT_MODULE_FETCH_FAILED', - }); - }); - - it('should revoke the created blob url when the other module fails to load', async () => { - globalThis.fetch = jest.fn(async (url: string) => ({ - ok: url !== sdkClientUrls.metadata, - status: 500, - statusText: 'Internal Server Error', - text: async () => '', - })) as unknown as typeof fetch; - URL.createObjectURL = jest.fn(() => 'blob:core-url'); - const revokeObjectUrlSpy = jest.fn(); - URL.revokeObjectURL = revokeObjectUrlSpy; - - await expect( - fetchSdkClientModulesAsBlobUrls(sdkClientUrls), - ).rejects.toMatchObject({ - code: 'FRONT_COMPONENT_MODULE_FETCH_FAILED', - }); - expect(revokeObjectUrlSpy).toHaveBeenCalledWith('blob:core-url'); - }); -}); diff --git a/packages/twenty-front-component-renderer/src/remote/worker/utils/containsSdkClientImportSpecifier.ts b/packages/twenty-front-component-renderer/src/remote/worker/utils/containsSdkClientImportSpecifier.ts deleted file mode 100644 index 6751b7da5f..0000000000 --- a/packages/twenty-front-component-renderer/src/remote/worker/utils/containsSdkClientImportSpecifier.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { SDK_CLIENT_IMPORT_SPECIFIERS } from '@/remote/worker/constants/SdkClientImportSpecifiers'; - -export const containsSdkClientImportSpecifier = (source: string): boolean => - SDK_CLIENT_IMPORT_SPECIFIERS.some((specifier) => source.includes(specifier)); diff --git a/packages/twenty-front-component-renderer/src/remote/worker/utils/createSdkClientModuleBlobUrls.ts b/packages/twenty-front-component-renderer/src/remote/worker/utils/createSdkClientModuleBlobUrls.ts new file mode 100644 index 0000000000..37a61d2aed --- /dev/null +++ b/packages/twenty-front-component-renderer/src/remote/worker/utils/createSdkClientModuleBlobUrls.ts @@ -0,0 +1,10 @@ +import { createJavaScriptModuleBlobUrl } from '@/remote/worker/utils/createJavaScriptModuleBlobUrl'; +import { type SdkClientSources } from '@/types/SdkClientSources'; +import { type SdkClientUrls } from '@/types/SdkClientUrls'; + +export const createSdkClientModuleBlobUrls = ( + sdkClientSources: SdkClientSources, +): SdkClientUrls => ({ + core: createJavaScriptModuleBlobUrl(sdkClientSources.core), + metadata: createJavaScriptModuleBlobUrl(sdkClientSources.metadata), +}); diff --git a/packages/twenty-front-component-renderer/src/remote/worker/utils/fetchJavaScriptModuleSourceText.ts b/packages/twenty-front-component-renderer/src/remote/worker/utils/fetchJavaScriptModuleSourceText.ts deleted file mode 100644 index 401246478c..0000000000 --- a/packages/twenty-front-component-renderer/src/remote/worker/utils/fetchJavaScriptModuleSourceText.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { CustomError } from 'twenty-shared/utils'; - -export const fetchJavaScriptModuleSourceText = async ( - url: string, - headers?: Record, -): Promise => { - let response: Response; - - try { - response = await fetch(url, { headers }); - } catch (error) { - throw new CustomError( - `Failed to fetch front component module ${url}: ${ - error instanceof Error ? error.message : String(error) - }`, - 'FRONT_COMPONENT_MODULE_FETCH_FAILED', - ); - } - - if (!response.ok) { - throw new CustomError( - `Failed to fetch front component module ${url}: ${response.status} ${response.statusText}`, - 'FRONT_COMPONENT_MODULE_FETCH_FAILED', - ); - } - - return response.text(); -}; diff --git a/packages/twenty-front-component-renderer/src/remote/worker/utils/fetchSdkClientModulesAsBlobUrls.ts b/packages/twenty-front-component-renderer/src/remote/worker/utils/fetchSdkClientModulesAsBlobUrls.ts deleted file mode 100644 index f9c5f72754..0000000000 --- a/packages/twenty-front-component-renderer/src/remote/worker/utils/fetchSdkClientModulesAsBlobUrls.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { createJavaScriptModuleBlobUrl } from '@/remote/worker/utils/createJavaScriptModuleBlobUrl'; -import { fetchJavaScriptModuleSourceText } from '@/remote/worker/utils/fetchJavaScriptModuleSourceText'; -import { type SdkClientUrls } from '@/types/SdkClientUrls'; - -export const fetchSdkClientModulesAsBlobUrls = async ( - sdkClientUrls: SdkClientUrls, - headers?: Record, -): Promise => { - const [coreResult, metadataResult] = await Promise.allSettled([ - fetchJavaScriptModuleSourceText(sdkClientUrls.core, headers).then( - createJavaScriptModuleBlobUrl, - ), - fetchJavaScriptModuleSourceText(sdkClientUrls.metadata, headers).then( - createJavaScriptModuleBlobUrl, - ), - ]); - - if ( - coreResult.status === 'rejected' || - metadataResult.status === 'rejected' - ) { - for (const result of [coreResult, metadataResult]) { - if (result.status === 'fulfilled') { - URL.revokeObjectURL(result.value); - } - } - - throw coreResult.status === 'rejected' - ? coreResult.reason - : (metadataResult as PromiseRejectedResult).reason; - } - - return { core: coreResult.value, metadata: metadataResult.value }; -}; diff --git a/packages/twenty-front-component-renderer/src/remote/worker/utils/loadFrontComponentModule.ts b/packages/twenty-front-component-renderer/src/remote/worker/utils/loadFrontComponentModule.ts index b00ad1bfd4..bfc3447cd8 100644 --- a/packages/twenty-front-component-renderer/src/remote/worker/utils/loadFrontComponentModule.ts +++ b/packages/twenty-front-component-renderer/src/remote/worker/utils/loadFrontComponentModule.ts @@ -1,17 +1,14 @@ import { isDefined } from 'twenty-shared/utils'; -import { containsSdkClientImportSpecifier } from '@/remote/worker/utils/containsSdkClientImportSpecifier'; import { createJavaScriptModuleBlobUrl } from '@/remote/worker/utils/createJavaScriptModuleBlobUrl'; -import { fetchSdkClientModulesAsBlobUrls } from '@/remote/worker/utils/fetchSdkClientModulesAsBlobUrls'; +import { createSdkClientModuleBlobUrls } from '@/remote/worker/utils/createSdkClientModuleBlobUrls'; import { revokeSdkClientModuleBlobUrls } from '@/remote/worker/utils/revokeSdkClientModuleBlobUrls'; import { rewriteSdkClientImportsToBlobUrls } from '@/remote/worker/utils/rewriteSdkClientImportsToBlobUrls'; -import { type SdkClientUrls } from '@/types/SdkClientUrls'; -import { buildAuthorizationHeadersFromAccessToken } from '@/utils/buildAuthorizationHeadersFromAccessToken'; +import { type SdkClientSources } from '@/types/SdkClientSources'; type LoadFrontComponentModuleInput = { componentSource: string; - sdkClientUrls?: SdkClientUrls; - applicationAccessToken?: string; + sdkClientSources?: SdkClientSources; }; type FrontComponentModule = { @@ -20,21 +17,11 @@ type FrontComponentModule = { export const loadFrontComponentModule = async ({ componentSource, - sdkClientUrls, - applicationAccessToken, + sdkClientSources, }: LoadFrontComponentModuleInput): Promise => { - const authorizationHeaders = buildAuthorizationHeadersFromAccessToken( - applicationAccessToken, - ); - - const sdkModuleBlobUrls = - isDefined(sdkClientUrls) && - containsSdkClientImportSpecifier(componentSource) - ? await fetchSdkClientModulesAsBlobUrls( - sdkClientUrls, - authorizationHeaders, - ) - : null; + const sdkModuleBlobUrls = isDefined(sdkClientSources) + ? createSdkClientModuleBlobUrls(sdkClientSources) + : null; const componentModuleSource = isDefined(sdkModuleBlobUrls) ? rewriteSdkClientImportsToBlobUrls(componentSource, sdkModuleBlobUrls) diff --git a/packages/twenty-front-component-renderer/src/remote/worker/utils/renderFrontComponent.ts b/packages/twenty-front-component-renderer/src/remote/worker/utils/renderFrontComponent.ts index 386ebe77b6..64d881ff6a 100644 --- a/packages/twenty-front-component-renderer/src/remote/worker/utils/renderFrontComponent.ts +++ b/packages/twenty-front-component-renderer/src/remote/worker/utils/renderFrontComponent.ts @@ -34,8 +34,7 @@ export const renderFrontComponent = async ({ const componentModule = await loadFrontComponentModule({ componentSource: renderContext.componentSource, - sdkClientUrls: renderContext.sdkClientUrls, - applicationAccessToken: renderContext.applicationAccessToken, + sdkClientSources: renderContext.sdkClientSources, }); componentModule.default(renderContainer); diff --git a/packages/twenty-front-component-renderer/src/remote/worker/utils/rewriteSdkClientImportsToBlobUrls.ts b/packages/twenty-front-component-renderer/src/remote/worker/utils/rewriteSdkClientImportsToBlobUrls.ts index 4e12db3f0b..3adbcdd58e 100644 --- a/packages/twenty-front-component-renderer/src/remote/worker/utils/rewriteSdkClientImportsToBlobUrls.ts +++ b/packages/twenty-front-component-renderer/src/remote/worker/utils/rewriteSdkClientImportsToBlobUrls.ts @@ -1,14 +1,6 @@ -import { type SDK_CLIENT_IMPORT_SPECIFIERS } from '@/remote/worker/constants/SdkClientImportSpecifiers'; +import { type SDK_CLIENT_IMPORT_SPECIFIERS } from '@/constants/SdkClientImportSpecifiers'; import { type SdkClientUrls } from '@/types/SdkClientUrls'; - -const escapeRegExpToken = (value: string): string => - value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - -const buildImportContextPattern = (specifier: string): RegExp => - new RegExp( - `(\\bfrom\\s*|\\bimport\\s*\\(\\s*|\\bimport\\s*)(["'])${escapeRegExpToken(specifier)}\\2`, - 'g', - ); +import { buildSdkClientImportContextPattern } from '@/utils/buildSdkClientImportContextPattern'; export const rewriteSdkClientImportsToBlobUrls = ( source: string, @@ -26,7 +18,7 @@ export const rewriteSdkClientImportsToBlobUrls = ( for (const [specifier, blobUrl] of Object.entries(specifierToBlobUrl)) { rewrittenSource = rewrittenSource.replace( - buildImportContextPattern(specifier), + buildSdkClientImportContextPattern(specifier), (_fullMatch, importContext: string, quote: string) => `${importContext}${quote}${blobUrl}${quote}`, ); diff --git a/packages/twenty-front-component-renderer/src/types/HostToWorkerRenderContext.ts b/packages/twenty-front-component-renderer/src/types/HostToWorkerRenderContext.ts index 873d06aa7a..f0b91c27dd 100644 --- a/packages/twenty-front-component-renderer/src/types/HostToWorkerRenderContext.ts +++ b/packages/twenty-front-component-renderer/src/types/HostToWorkerRenderContext.ts @@ -1,4 +1,4 @@ -import { type SdkClientUrls } from '@/types/SdkClientUrls'; +import { type SdkClientSources } from '@/types/SdkClientSources'; export type HostToWorkerRenderContext = { componentUrl: string; @@ -6,7 +6,7 @@ export type HostToWorkerRenderContext = { applicationAccessToken?: string; apiUrl?: string; functionsBaseUrl?: string; - sdkClientUrls?: SdkClientUrls; + sdkClientSources?: SdkClientSources; hostFetchOrigins?: string[]; applicationVariables?: Record; }; diff --git a/packages/twenty-front-component-renderer/src/types/SdkClientSources.ts b/packages/twenty-front-component-renderer/src/types/SdkClientSources.ts new file mode 100644 index 0000000000..134a6792eb --- /dev/null +++ b/packages/twenty-front-component-renderer/src/types/SdkClientSources.ts @@ -0,0 +1,4 @@ +export type SdkClientSources = { + core: string; + metadata: string; +}; diff --git a/packages/twenty-front-component-renderer/src/utils/__tests__/containsSdkClientImportSpecifier.test.ts b/packages/twenty-front-component-renderer/src/utils/__tests__/containsSdkClientImportSpecifier.test.ts new file mode 100644 index 0000000000..6eb9fa94af --- /dev/null +++ b/packages/twenty-front-component-renderer/src/utils/__tests__/containsSdkClientImportSpecifier.test.ts @@ -0,0 +1,61 @@ +import { containsSdkClientImportSpecifier } from '../containsSdkClientImportSpecifier'; + +describe('containsSdkClientImportSpecifier', () => { + it('returns true when the source imports an sdk client module', () => { + expect( + containsSdkClientImportSpecifier( + "import { getClient } from 'twenty-client-sdk/core';", + ), + ).toBe(true); + expect( + containsSdkClientImportSpecifier( + "import { getMetadata } from 'twenty-client-sdk/metadata';", + ), + ).toBe(true); + }); + + it('returns true for dynamic import and re-export module positions', () => { + expect( + containsSdkClientImportSpecifier( + "const client = await import('twenty-client-sdk/core');", + ), + ).toBe(true); + expect( + containsSdkClientImportSpecifier( + "export { getClient } from 'twenty-client-sdk/core';", + ), + ).toBe(true); + }); + + it('returns false when the source does not import an sdk client module', () => { + expect( + containsSdkClientImportSpecifier("import { useState } from 'react';"), + ).toBe(false); + }); + + it('returns false for a sibling module sharing the specifier prefix', () => { + expect( + containsSdkClientImportSpecifier( + "import { thing } from 'twenty-client-sdk/core-extra';", + ), + ).toBe(false); + expect( + containsSdkClientImportSpecifier( + "import { thing } from 'twenty-client-sdk/metadata-utils';", + ), + ).toBe(false); + }); + + it('returns false when the specifier only appears outside an import position', () => { + expect( + containsSdkClientImportSpecifier( + "const docsUrl = 'twenty-client-sdk/core';", + ), + ).toBe(false); + expect( + containsSdkClientImportSpecifier( + '// see twenty-client-sdk/core for the generated client', + ), + ).toBe(false); + }); +}); diff --git a/packages/twenty-front-component-renderer/src/utils/buildSdkClientImportContextPattern.ts b/packages/twenty-front-component-renderer/src/utils/buildSdkClientImportContextPattern.ts new file mode 100644 index 0000000000..20d03bea81 --- /dev/null +++ b/packages/twenty-front-component-renderer/src/utils/buildSdkClientImportContextPattern.ts @@ -0,0 +1,8 @@ +const escapeRegExpToken = (value: string): string => + value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +export const buildSdkClientImportContextPattern = (specifier: string): RegExp => + new RegExp( + `(\\bfrom\\s*|\\bimport\\s*\\(\\s*|\\bimport\\s*)(["'])${escapeRegExpToken(specifier)}\\2`, + 'g', + ); diff --git a/packages/twenty-front-component-renderer/src/utils/containsSdkClientImportSpecifier.ts b/packages/twenty-front-component-renderer/src/utils/containsSdkClientImportSpecifier.ts new file mode 100644 index 0000000000..d31763aeb2 --- /dev/null +++ b/packages/twenty-front-component-renderer/src/utils/containsSdkClientImportSpecifier.ts @@ -0,0 +1,7 @@ +import { SDK_CLIENT_IMPORT_SPECIFIERS } from '@/constants/SdkClientImportSpecifiers'; +import { buildSdkClientImportContextPattern } from '@/utils/buildSdkClientImportContextPattern'; + +export const containsSdkClientImportSpecifier = (source: string): boolean => + SDK_CLIENT_IMPORT_SPECIFIERS.some((specifier) => + buildSdkClientImportContextPattern(specifier).test(source), + ); diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 32a4a99cb2..045da6015f 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -4345,6 +4345,7 @@ export type Query = { appConnections: Array; applicationConnectionProviders: Array; applicationRegistrationTarballUrl?: Maybe; + applicationSdkClientChecksums?: Maybe; barChartData: BarChartData; billingPortalSession: BillingSession; chatMessages: Array; @@ -4482,6 +4483,11 @@ export type QueryApplicationRegistrationTarballUrlArgs = { }; +export type QueryApplicationSdkClientChecksumsArgs = { + applicationId: Scalars['UUID']['input']; +}; + + export type QueryBarChartDataArgs = { input: BarChartDataInput; }; @@ -5052,6 +5058,12 @@ export enum SsoIdentityProviderStatus { Inactive = 'Inactive' } +export type SdkClientChecksums = { + __typename?: 'SdkClientChecksums'; + core?: Maybe; + metadata: Scalars['String']['output']; +}; + export type SearchField = { __typename?: 'SearchField'; createdAt: Scalars['DateTime']['output']; @@ -7202,6 +7214,13 @@ export type FindOneFrontComponentQueryVariables = Exact<{ export type FindOneFrontComponentQuery = { __typename?: 'Query', frontComponent?: { __typename?: 'FrontComponent', id: string, name: string, applicationId: string, builtComponentChecksum: string, isHeadless: boolean, usesSdkClient: boolean, applicationVariables?: any | null, applicationTokenPair?: { __typename?: 'ApplicationTokenPair', applicationAccessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, applicationRefreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } | null } | null }; +export type GetApplicationSdkClientChecksumsQueryVariables = Exact<{ + applicationId: Scalars['UUID']['input']; +}>; + + +export type GetApplicationSdkClientChecksumsQuery = { __typename?: 'Query', applicationSdkClientChecksums?: { __typename?: 'SdkClientChecksums', core?: string | null, metadata: string } | null }; + export type LogicFunctionFieldsFragment = { __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, executionMode: LogicFunctionExecutionMode, sourceHandlerPath: string, handlerName: string, cronTriggerSettings?: any | null, databaseEventTriggerSettings?: any | null, httpRouteTriggerSettings?: any | null, toolTriggerSettings?: any | null, workflowActionTriggerSettings?: any | null, applicationId?: string | null, universalIdentifier?: string | null, createdAt: string, updatedAt: string }; export type CreateOneLogicFunctionMutationVariables = Exact<{ @@ -9001,6 +9020,7 @@ export const UploadWorkflowFileDocument = {"kind":"Document","definitions":[{"ki export const RenewApplicationTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RenewApplicationToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRefreshToken"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"renewApplicationToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRefreshToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRefreshToken"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"applicationAccessToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"applicationRefreshToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const FindManyFrontComponentsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyFrontComponents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"frontComponents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"builtComponentChecksum"}},{"kind":"Field","name":{"kind":"Name","value":"builtComponentPath"}},{"kind":"Field","name":{"kind":"Name","value":"componentName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"isHeadless"}},{"kind":"Field","name":{"kind":"Name","value":"sourceComponentPath"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"usesSdkClient"}}]}}]}}]} as unknown as DocumentNode; export const FindOneFrontComponentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneFrontComponent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"frontComponent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"builtComponentChecksum"}},{"kind":"Field","name":{"kind":"Name","value":"isHeadless"}},{"kind":"Field","name":{"kind":"Name","value":"usesSdkClient"}},{"kind":"Field","name":{"kind":"Name","value":"applicationVariables"}},{"kind":"Field","name":{"kind":"Name","value":"applicationTokenPair"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"applicationAccessToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"applicationRefreshToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetApplicationSdkClientChecksumsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetApplicationSdkClientChecksums"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"applicationSdkClientChecksums"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"core"}},{"kind":"Field","name":{"kind":"Name","value":"metadata"}}]}}]}}]} as unknown as DocumentNode; export const CreateOneLogicFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneLogicFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateLogicFunctionFromSourceInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneLogicFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LogicFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LogicFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"executionMode"}},{"kind":"Field","name":{"kind":"Name","value":"sourceHandlerPath"}},{"kind":"Field","name":{"kind":"Name","value":"handlerName"}},{"kind":"Field","name":{"kind":"Name","value":"cronTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"databaseEventTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"httpRouteTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"toolTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"workflowActionTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const DeleteOneLogicFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneLogicFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneLogicFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LogicFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LogicFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"executionMode"}},{"kind":"Field","name":{"kind":"Name","value":"sourceHandlerPath"}},{"kind":"Field","name":{"kind":"Name","value":"handlerName"}},{"kind":"Field","name":{"kind":"Name","value":"cronTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"databaseEventTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"httpRouteTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"toolTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"workflowActionTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const ExecuteOneLogicFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ExecuteOneLogicFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ExecuteOneLogicFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"executeOneLogicFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"logs"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/browser-event/hooks/useListenToMetadataOperationBrowserEvent.ts b/packages/twenty-front/src/modules/browser-event/hooks/useListenToMetadataOperationBrowserEvent.ts index a1e0734b60..7acab44ea6 100644 --- a/packages/twenty-front/src/modules/browser-event/hooks/useListenToMetadataOperationBrowserEvent.ts +++ b/packages/twenty-front/src/modules/browser-event/hooks/useListenToMetadataOperationBrowserEvent.ts @@ -11,14 +11,20 @@ export const useListenToMetadataOperationBrowserEvent = < onMetadataOperationBrowserEvent, metadataName, operationTypes, + skip = false, }: { onMetadataOperationBrowserEvent: ( detail: MetadataOperationBrowserEventDetail, ) => void; metadataName?: BroadcastEntityName; operationTypes?: MetadataOperation['type'][]; + skip?: boolean; }) => { useEffect(() => { + if (skip) { + return; + } + const handleMetadataOperationEvent = ( event: CustomEvent>, ) => { @@ -49,5 +55,5 @@ export const useListenToMetadataOperationBrowserEvent = < handleMetadataOperationEvent as EventListener, ); }; - }, [metadataName, onMetadataOperationBrowserEvent, operationTypes]); + }, [metadataName, onMetadataOperationBrowserEvent, operationTypes, skip]); }; diff --git a/packages/twenty-front/src/modules/browser-event/types/BroadcastEntityName.ts b/packages/twenty-front/src/modules/browser-event/types/BroadcastEntityName.ts index 6f04898a01..83af71e07b 100644 --- a/packages/twenty-front/src/modules/browser-event/types/BroadcastEntityName.ts +++ b/packages/twenty-front/src/modules/browser-event/types/BroadcastEntityName.ts @@ -5,6 +5,7 @@ import { type AllMetadataName } from 'twenty-shared/metadata'; // workspace migration / syncable entity system (ALL_METADATA_NAME). const ALL_NON_SYNCABLE_BROADCAST_ENTITY_NAME = { agentChatThread: 'agentChatThread', + application: 'application', } as const; type NonSyncableBroadcastEntityName = diff --git a/packages/twenty-front/src/modules/front-components/components/FrontComponentApplicationTokenPairEffect.tsx b/packages/twenty-front/src/modules/front-components/components/FrontComponentApplicationTokenPairEffect.tsx new file mode 100644 index 0000000000..68539da1d3 --- /dev/null +++ b/packages/twenty-front/src/modules/front-components/components/FrontComponentApplicationTokenPairEffect.tsx @@ -0,0 +1,25 @@ +import { frontComponentApplicationTokenPairComponentState } from '@/front-components/states/frontComponentApplicationTokenPairComponentState'; +import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; +import { useEffect } from 'react'; +import { type ApplicationTokenPair } from '~/generated-metadata/graphql'; + +type FrontComponentApplicationTokenPairEffectProps = { + frontComponentId: string; + applicationTokenPair: ApplicationTokenPair | null; +}; + +export const FrontComponentApplicationTokenPairEffect = ({ + frontComponentId, + applicationTokenPair, +}: FrontComponentApplicationTokenPairEffectProps) => { + const setFrontComponentApplicationTokenPair = useSetAtomComponentState( + frontComponentApplicationTokenPairComponentState, + frontComponentId, + ); + + useEffect(() => { + setFrontComponentApplicationTokenPair(applicationTokenPair); + }, [applicationTokenPair, setFrontComponentApplicationTokenPair]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/front-components/components/FrontComponentLoadErrorSnackBarEffect.tsx b/packages/twenty-front/src/modules/front-components/components/FrontComponentLoadErrorSnackBarEffect.tsx new file mode 100644 index 0000000000..a81905c883 --- /dev/null +++ b/packages/twenty-front/src/modules/front-components/components/FrontComponentLoadErrorSnackBarEffect.tsx @@ -0,0 +1,24 @@ +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { t } from '@lingui/core/macro'; +import { useEffect } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +type FrontComponentLoadErrorSnackBarEffectProps = { + errorMessage?: string; +}; + +export const FrontComponentLoadErrorSnackBarEffect = ({ + errorMessage, +}: FrontComponentLoadErrorSnackBarEffectProps) => { + const { enqueueErrorSnackBar } = useSnackBar(); + + useEffect(() => { + if (isDefined(errorMessage)) { + enqueueErrorSnackBar({ + message: t`Failed to load front component: ${errorMessage}`, + }); + } + }, [errorMessage, enqueueErrorSnackBar]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx b/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx index 477ec87052..3d4116e61a 100644 --- a/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx +++ b/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx @@ -1,20 +1,25 @@ +import { FrontComponentApplicationTokenPairEffect } from '@/front-components/components/FrontComponentApplicationTokenPairEffect'; +import { FrontComponentLoadErrorSnackBarEffect } from '@/front-components/components/FrontComponentLoadErrorSnackBarEffect'; import { FrontComponentRendererProvider } from '@/front-components/components/FrontComponentRendererProvider'; +import { useFrontComponentExecutionContext } from '@/front-components/hooks/useFrontComponentExecutionContext'; +import { useOnApplicationSdkClientChecksumsUpdated } from '@/front-components/hooks/useOnApplicationSdkClientChecksumsUpdated'; +import { useOnFrontComponentUpdated } from '@/front-components/hooks/useOnFrontComponentUpdated'; +import { getFrontComponentUrl } from '@/front-components/utils/getFrontComponentUrl'; import { getSdkClientUrls } from '@/front-components/utils/getSdkClientUrls'; import { useGetLogicFunctionHttpUrl } from '@/settings/logic-functions/hooks/useGetLogicFunctionHttpUrl'; -import { useFrontComponentExecutionContext } from '@/front-components/hooks/useFrontComponentExecutionContext'; -import { useOnFrontComponentUpdated } from '@/front-components/hooks/useOnFrontComponentUpdated'; -import { frontComponentApplicationTokenPairComponentState } from '@/front-components/states/frontComponentApplicationTokenPairComponentState'; -import { getFrontComponentUrl } from '@/front-components/utils/getFrontComponentUrl'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; +import { useQuery } from '@apollo/client/react'; import { t } from '@lingui/core/macro'; -import { useCallback, useContext, useEffect, useMemo } from 'react'; +import { useCallback, useContext, useMemo } from 'react'; import { FrontComponentRenderer as SharedFrontComponentRenderer } from 'twenty-front-component-renderer'; import { isDefined } from 'twenty-shared/utils'; import { ThemeContext } from 'twenty-ui/theme-constants'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; -import { useQuery } from '@apollo/client/react'; -import { FindOneFrontComponentDocument } from '~/generated-metadata/graphql'; +import { + FindOneFrontComponentDocument, + type FindOneFrontComponentQuery, + GetApplicationSdkClientChecksumsDocument, +} from '~/generated-metadata/graphql'; type FrontComponentRendererProps = { frontComponentId: string; @@ -22,20 +27,55 @@ type FrontComponentRendererProps = { selectedRecordIds?: string[]; }; +type ResolvedFrontComponent = NonNullable< + FindOneFrontComponentQuery['frontComponent'] +>; + +type FrontComponentRendererContentProps = { + frontComponent: ResolvedFrontComponent; + commandMenuItemId?: string; + selectedRecordIds?: string[]; +}; + export const FrontComponentRenderer = ({ frontComponentId, commandMenuItemId, selectedRecordIds, }: FrontComponentRendererProps) => { + const { data, loading, error } = useQuery(FindOneFrontComponentDocument, { + variables: { id: frontComponentId }, + }); + + useOnFrontComponentUpdated({ + frontComponentId, + }); + + const frontComponent = data?.frontComponent; + + return ( + <> + + {!loading && isDefined(frontComponent) && ( + + )} + + ); +}; + +const FrontComponentRendererContent = ({ + frontComponent, + commandMenuItemId, + selectedRecordIds, +}: FrontComponentRendererContentProps) => { const { colorScheme } = useContext(ThemeContext); const { enqueueErrorSnackBar } = useSnackBar(); - const { functionsBaseUrl } = useGetLogicFunctionHttpUrl(); - const setFrontComponentApplicationTokenPair = useSetAtomComponentState( - frontComponentApplicationTokenPairComponentState, - frontComponentId, - ); + const { id: frontComponentId, applicationId, usesSdkClient } = frontComponent; const { executionContext, frontComponentHostCommunicationApi } = useFrontComponentExecutionContext({ @@ -51,78 +91,69 @@ export const FrontComponentRenderer = ({ return; } - const errorMessage = error.message; - enqueueErrorSnackBar({ - message: t`Failed to load front component: ${errorMessage}`, + message: t`Failed to load front component: ${error.message}`, }); }, [enqueueErrorSnackBar], ); - const { data, loading, error } = useQuery(FindOneFrontComponentDocument, { - variables: { id: frontComponentId }, + const applicationTokenPair = frontComponent.applicationTokenPair ?? null; + + const { data: sdkClientChecksumsData, loading: sdkClientChecksumsLoading } = + useQuery(GetApplicationSdkClientChecksumsDocument, { + variables: { applicationId }, + skip: !usesSdkClient, + }); + + useOnApplicationSdkClientChecksumsUpdated({ + applicationId, + skip: !usesSdkClient, }); - useEffect(() => { - if (error) { - handleError(error); - } - }, [error, handleError]); - - const applicationTokenPair = - data?.frontComponent?.applicationTokenPair ?? null; - - useEffect(() => { - if (isDefined(applicationTokenPair)) { - setFrontComponentApplicationTokenPair(applicationTokenPair); - } - }, [applicationTokenPair, setFrontComponentApplicationTokenPair]); - - useOnFrontComponentUpdated({ - frontComponentId, - }); - - const applicationId = data?.frontComponent?.applicationId; + const sdkClientChecksums = + sdkClientChecksumsData?.applicationSdkClientChecksums; const sdkClientUrls = useMemo( - () => - isDefined(applicationId) ? getSdkClientUrls(applicationId) : undefined, - [applicationId], + () => getSdkClientUrls(applicationId, sdkClientChecksums), + [applicationId, sdkClientChecksums], ); - if ( - loading || - !isDefined(data?.frontComponent) || - !isDefined(applicationTokenPair) - ) { - return null; - } - const componentUrl = getFrontComponentUrl({ frontComponentId, - checksum: data.frontComponent.builtComponentChecksum, + checksum: frontComponent.builtComponentChecksum, }); - const accessToken = applicationTokenPair.applicationAccessToken.token; + const applicationVariables = frontComponent.applicationVariables ?? undefined; - const applicationVariables = - data.frontComponent.applicationVariables ?? undefined; + const isSdkClientReady = !usesSdkClient || !sdkClientChecksumsLoading; return ( - - + - + {isDefined(applicationTokenPair) && isSdkClientReady && ( + + + + )} + ); }; diff --git a/packages/twenty-front/src/modules/front-components/graphql/queries/getApplicationSdkClientChecksums.ts b/packages/twenty-front/src/modules/front-components/graphql/queries/getApplicationSdkClientChecksums.ts new file mode 100644 index 0000000000..323c95723a --- /dev/null +++ b/packages/twenty-front/src/modules/front-components/graphql/queries/getApplicationSdkClientChecksums.ts @@ -0,0 +1,10 @@ +import gql from 'graphql-tag'; + +export const GET_APPLICATION_SDK_CLIENT_CHECKSUMS = gql` + query GetApplicationSdkClientChecksums($applicationId: UUID!) { + applicationSdkClientChecksums(applicationId: $applicationId) { + core + metadata + } + } +`; diff --git a/packages/twenty-front/src/modules/front-components/hooks/__tests__/useUpdateSdkClientChecksumsApolloCache.test.tsx b/packages/twenty-front/src/modules/front-components/hooks/__tests__/useUpdateSdkClientChecksumsApolloCache.test.tsx new file mode 100644 index 0000000000..3d3ece2a2b --- /dev/null +++ b/packages/twenty-front/src/modules/front-components/hooks/__tests__/useUpdateSdkClientChecksumsApolloCache.test.tsx @@ -0,0 +1,194 @@ +import { renderHook } from '@testing-library/react'; + +import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail'; +import { useUpdateSdkClientChecksumsApolloCache } from '@/front-components/hooks/useUpdateSdkClientChecksumsApolloCache'; +import { type ApplicationSdkClientChecksumsBroadcastRecord } from '@/front-components/types/ApplicationSdkClientChecksumsBroadcastRecord'; + +const mockReadQuery = jest.fn(); +const mockUpdateQuery = jest.fn(); +const mockQuery = jest.fn().mockResolvedValue({ data: undefined }); +const mockApolloClient = { + cache: { readQuery: mockReadQuery, updateQuery: mockUpdateQuery }, + query: mockQuery, +}; + +jest.mock('@apollo/client/react', () => ({ + ...jest.requireActual('@apollo/client/react'), + useApolloClient: () => mockApolloClient, +})); + +const APPLICATION_ID = 'app-test-id'; + +const CACHED_CHECKSUM_PAIR = { + applicationSdkClientChecksums: { + __typename: 'SdkClientChecksums' as const, + core: 'old-core-checksum', + metadata: 'old-metadata-checksum', + }, +}; + +const buildApplicationRecord = ( + overrides: Partial = {}, +): ApplicationSdkClientChecksumsBroadcastRecord => ({ + id: APPLICATION_ID, + sdkClientCoreChecksum: 'a'.repeat(64), + ...overrides, +}); + +const buildUpdateDetail = ( + updatedRecord: ApplicationSdkClientChecksumsBroadcastRecord, +): MetadataOperationBrowserEventDetail => ({ + metadataName: 'application', + operation: { + type: 'update', + updatedRecord, + }, +}); + +describe('useUpdateSdkClientChecksumsApolloCache', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockReadQuery.mockReturnValue(CACHED_CHECKSUM_PAIR); + }); + + it('should call cache.updateQuery when the application id matches', () => { + const { result } = renderHook(() => + useUpdateSdkClientChecksumsApolloCache({ + applicationId: APPLICATION_ID, + }), + ); + + result.current.updateSdkClientChecksumsApolloCache( + buildUpdateDetail(buildApplicationRecord()), + ); + + expect(mockUpdateQuery).toHaveBeenCalledTimes(1); + expect(mockQuery).not.toHaveBeenCalled(); + }); + + it('should not call cache.updateQuery when the application id does not match', () => { + const { result } = renderHook(() => + useUpdateSdkClientChecksumsApolloCache({ + applicationId: APPLICATION_ID, + }), + ); + + result.current.updateSdkClientChecksumsApolloCache( + buildUpdateDetail(buildApplicationRecord({ id: 'other-app-id' })), + ); + + expect(mockUpdateQuery).not.toHaveBeenCalled(); + expect(mockQuery).not.toHaveBeenCalled(); + }); + + it('should not call cache.updateQuery when applicationId is undefined', () => { + const { result } = renderHook(() => + useUpdateSdkClientChecksumsApolloCache({}), + ); + + result.current.updateSdkClientChecksumsApolloCache( + buildUpdateDetail(buildApplicationRecord()), + ); + + expect(mockUpdateQuery).not.toHaveBeenCalled(); + expect(mockQuery).not.toHaveBeenCalled(); + }); + + it('should not call cache.updateQuery when checksums are missing', () => { + const { result } = renderHook(() => + useUpdateSdkClientChecksumsApolloCache({ + applicationId: APPLICATION_ID, + }), + ); + + result.current.updateSdkClientChecksumsApolloCache( + buildUpdateDetail( + buildApplicationRecord({ sdkClientCoreChecksum: null }), + ), + ); + + expect(mockUpdateQuery).not.toHaveBeenCalled(); + expect(mockQuery).not.toHaveBeenCalled(); + }); + + it('should not call cache.updateQuery for create operations', () => { + const { result } = renderHook(() => + useUpdateSdkClientChecksumsApolloCache({ + applicationId: APPLICATION_ID, + }), + ); + + result.current.updateSdkClientChecksumsApolloCache({ + metadataName: 'application', + operation: { + type: 'create', + createdRecord: buildApplicationRecord(), + }, + }); + + expect(mockUpdateQuery).not.toHaveBeenCalled(); + expect(mockQuery).not.toHaveBeenCalled(); + }); + + it('should update only the core checksum and preserve the cached metadata checksum', () => { + const { result } = renderHook(() => + useUpdateSdkClientChecksumsApolloCache({ + applicationId: APPLICATION_ID, + }), + ); + + result.current.updateSdkClientChecksumsApolloCache( + buildUpdateDetail(buildApplicationRecord()), + ); + + const updaterFn = mockUpdateQuery.mock.calls[0][1]; + + const updatedData = updaterFn(CACHED_CHECKSUM_PAIR); + + expect(updatedData.applicationSdkClientChecksums).toEqual({ + __typename: 'SdkClientChecksums', + core: 'a'.repeat(64), + metadata: 'old-metadata-checksum', + }); + }); + + it('should refetch the checksum query when no checksum pair is cached yet', () => { + mockReadQuery.mockReturnValue({ applicationSdkClientChecksums: null }); + + const { result } = renderHook(() => + useUpdateSdkClientChecksumsApolloCache({ + applicationId: APPLICATION_ID, + }), + ); + + result.current.updateSdkClientChecksumsApolloCache( + buildUpdateDetail(buildApplicationRecord()), + ); + + expect(mockUpdateQuery).not.toHaveBeenCalled(); + expect(mockQuery).toHaveBeenCalledTimes(1); + expect(mockQuery).toHaveBeenCalledWith( + expect.objectContaining({ + variables: { applicationId: APPLICATION_ID }, + fetchPolicy: 'network-only', + }), + ); + }); + + it('should refetch the checksum query when the query result is absent from the cache', () => { + mockReadQuery.mockReturnValue(null); + + const { result } = renderHook(() => + useUpdateSdkClientChecksumsApolloCache({ + applicationId: APPLICATION_ID, + }), + ); + + result.current.updateSdkClientChecksumsApolloCache( + buildUpdateDetail(buildApplicationRecord()), + ); + + expect(mockUpdateQuery).not.toHaveBeenCalled(); + expect(mockQuery).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/twenty-front/src/modules/front-components/hooks/useOnApplicationSdkClientChecksumsUpdated.ts b/packages/twenty-front/src/modules/front-components/hooks/useOnApplicationSdkClientChecksumsUpdated.ts new file mode 100644 index 0000000000..0852f5ba2b --- /dev/null +++ b/packages/twenty-front/src/modules/front-components/hooks/useOnApplicationSdkClientChecksumsUpdated.ts @@ -0,0 +1,26 @@ +import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent'; +import { useUpdateSdkClientChecksumsApolloCache } from '@/front-components/hooks/useUpdateSdkClientChecksumsApolloCache'; +import { type ApplicationSdkClientChecksumsBroadcastRecord } from '@/front-components/types/ApplicationSdkClientChecksumsBroadcastRecord'; + +type UseOnApplicationSdkClientChecksumsUpdatedArgs = { + applicationId?: string; + skip?: boolean; +}; + +export const useOnApplicationSdkClientChecksumsUpdated = ({ + applicationId, + skip = false, +}: UseOnApplicationSdkClientChecksumsUpdatedArgs) => { + const { updateSdkClientChecksumsApolloCache } = + useUpdateSdkClientChecksumsApolloCache({ + applicationId, + }); + + useListenToMetadataOperationBrowserEvent( + { + metadataName: 'application', + onMetadataOperationBrowserEvent: updateSdkClientChecksumsApolloCache, + skip, + }, + ); +}; diff --git a/packages/twenty-front/src/modules/front-components/hooks/useUpdateSdkClientChecksumsApolloCache.ts b/packages/twenty-front/src/modules/front-components/hooks/useUpdateSdkClientChecksumsApolloCache.ts new file mode 100644 index 0000000000..ebcb47fce7 --- /dev/null +++ b/packages/twenty-front/src/modules/front-components/hooks/useUpdateSdkClientChecksumsApolloCache.ts @@ -0,0 +1,76 @@ +import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail'; +import { type ApplicationSdkClientChecksumsBroadcastRecord } from '@/front-components/types/ApplicationSdkClientChecksumsBroadcastRecord'; +import { useApolloClient } from '@apollo/client/react'; +import { useCallback } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { + GetApplicationSdkClientChecksumsDocument, + type GetApplicationSdkClientChecksumsQuery, +} from '~/generated-metadata/graphql'; + +type UseUpdateSdkClientChecksumsApolloCacheArgs = { + applicationId?: string; +}; + +export const useUpdateSdkClientChecksumsApolloCache = ({ + applicationId, +}: UseUpdateSdkClientChecksumsApolloCacheArgs) => { + const apolloClient = useApolloClient(); + + const updateSdkClientChecksumsApolloCache = useCallback( + ( + detail: MetadataOperationBrowserEventDetail, + ) => { + if (detail.operation.type !== 'update') { + return; + } + + const { updatedRecord } = detail.operation; + + if (!isDefined(applicationId) || updatedRecord.id !== applicationId) { + return; + } + + const { sdkClientCoreChecksum } = updatedRecord; + + if (!isDefined(sdkClientCoreChecksum)) { + return; + } + + const cachedData = + apolloClient.cache.readQuery({ + query: GetApplicationSdkClientChecksumsDocument, + variables: { applicationId }, + }); + + const existingChecksums = cachedData?.applicationSdkClientChecksums; + + if (!isDefined(existingChecksums)) { + void apolloClient.query({ + query: GetApplicationSdkClientChecksumsDocument, + variables: { applicationId }, + fetchPolicy: 'network-only', + }); + + return; + } + + apolloClient.cache.updateQuery( + { + query: GetApplicationSdkClientChecksumsDocument, + variables: { applicationId }, + }, + (existingData) => ({ + ...existingData, + applicationSdkClientChecksums: { + ...existingChecksums, + core: sdkClientCoreChecksum, + }, + }), + ); + }, + [apolloClient, applicationId], + ); + + return { updateSdkClientChecksumsApolloCache }; +}; diff --git a/packages/twenty-front/src/modules/front-components/types/ApplicationSdkClientChecksumsBroadcastRecord.ts b/packages/twenty-front/src/modules/front-components/types/ApplicationSdkClientChecksumsBroadcastRecord.ts new file mode 100644 index 0000000000..634302a8ac --- /dev/null +++ b/packages/twenty-front/src/modules/front-components/types/ApplicationSdkClientChecksumsBroadcastRecord.ts @@ -0,0 +1,4 @@ +export type ApplicationSdkClientChecksumsBroadcastRecord = { + id: string; + sdkClientCoreChecksum?: string | null; +}; diff --git a/packages/twenty-front/src/modules/front-components/utils/__tests__/getSdkClientUrls.test.ts b/packages/twenty-front/src/modules/front-components/utils/__tests__/getSdkClientUrls.test.ts index c210887ddb..c16620d95c 100644 --- a/packages/twenty-front/src/modules/front-components/utils/__tests__/getSdkClientUrls.test.ts +++ b/packages/twenty-front/src/modules/front-components/utils/__tests__/getSdkClientUrls.test.ts @@ -2,10 +2,34 @@ import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url'; import { getSdkClientUrls } from '@/front-components/utils/getSdkClientUrls'; describe('getSdkClientUrls', () => { - it('builds application-scoped core and metadata sdk client urls', () => { + it('builds bare urls when checksums are absent', () => { expect(getSdkClientUrls('application-id')).toEqual({ core: `${REST_API_BASE_URL}/sdk-client/application-id/core`, - metadata: `${REST_API_BASE_URL}/sdk-client/application-id/metadata`, + metadata: `${REST_API_BASE_URL}/sdk-client/metadata`, + }); + }); + + it('builds content-addressed urls when checksums are provided', () => { + expect( + getSdkClientUrls('application-id', { + core: 'core-checksum', + metadata: 'metadata-checksum', + }), + ).toEqual({ + core: `${REST_API_BASE_URL}/sdk-client/application-id/core/core-checksum`, + metadata: `${REST_API_BASE_URL}/sdk-client/metadata/metadata-checksum`, + }); + }); + + it('falls back to the bare core url when only the metadata checksum is available', () => { + expect( + getSdkClientUrls('application-id', { + core: null, + metadata: 'metadata-checksum', + }), + ).toEqual({ + core: `${REST_API_BASE_URL}/sdk-client/application-id/core`, + metadata: `${REST_API_BASE_URL}/sdk-client/metadata/metadata-checksum`, }); }); }); diff --git a/packages/twenty-front/src/modules/front-components/utils/getSdkClientUrls.ts b/packages/twenty-front/src/modules/front-components/utils/getSdkClientUrls.ts index 68c7c87c8b..9fde08ebc1 100644 --- a/packages/twenty-front/src/modules/front-components/utils/getSdkClientUrls.ts +++ b/packages/twenty-front/src/modules/front-components/utils/getSdkClientUrls.ts @@ -1,6 +1,23 @@ import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url'; +import { isDefined } from 'twenty-shared/utils'; +import { type SdkClientChecksums } from '~/generated-metadata/graphql'; -export const getSdkClientUrls = (applicationId: string) => ({ - core: `${REST_API_BASE_URL}/sdk-client/${applicationId}/core`, - metadata: `${REST_API_BASE_URL}/sdk-client/${applicationId}/metadata`, -}); +export const getSdkClientUrls = ( + applicationId: string, + checksums?: Pick | null, +) => { + const applicationBaseUrl = `${REST_API_BASE_URL}/sdk-client/${applicationId}`; + const metadataBaseUrl = `${REST_API_BASE_URL}/sdk-client/metadata`; + + const coreChecksum = checksums?.core; + const metadataChecksum = checksums?.metadata; + + return { + core: isDefined(coreChecksum) + ? `${applicationBaseUrl}/core/${coreChecksum}` + : `${applicationBaseUrl}/core`, + metadata: isDefined(metadataChecksum) + ? `${metadataBaseUrl}/${metadataChecksum}` + : metadataBaseUrl, + }; +}; diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-23/2-23-instance-command-fast-1784625638000-add-sdk-client-core-checksum-to-application.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-23/2-23-instance-command-fast-1784625638000-add-sdk-client-core-checksum-to-application.ts new file mode 100644 index 0000000000..667ee27dff --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-23/2-23-instance-command-fast-1784625638000-add-sdk-client-core-checksum-to-application.ts @@ -0,0 +1,21 @@ +import { QueryRunner } from 'typeorm'; + +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface'; + +@RegisteredInstanceCommand('2.23.0', 1784625638000) +export class AddSdkClientCoreChecksumToApplicationFastInstanceCommand + implements FastInstanceCommand +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE "core"."application" ADD COLUMN IF NOT EXISTS "sdkClientCoreChecksum" text', + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE "core"."application" DROP COLUMN IF EXISTS "sdkClientCoreChecksum"', + ); + } +} 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 21d53a2752..bfce8a2a28 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 @@ -43,11 +43,16 @@ import { AddTsVectorFieldMetadataIdToSearchFieldMetadataFastInstanceCommand } fr import { BackfillTsVectorFieldMetadataIdOnSearchFieldMetadataSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-18/2-18-instance-command-slow-1810000003000-backfill-ts-vector-field-metadata-id-on-search-field-metadata'; import { AddMetadataOverridesColumnFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-instance-command-fast-1782986475000-add-metadata-overrides-column'; import { AddLastStreamErrorToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-instance-command-fast-1782996657000-add-last-stream-error-to-agent-chat-thread'; +import { AddPendingQuestionMessageIdToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-instance-command-fast-1782999138000-add-pending-question-to-agent-chat-thread'; import { AddLogoFileIdToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-instance-command-fast-1783062755137-add-logo-file-id-to-application'; -import { BackfillMetadataOverridesSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-instance-command-slow-1782986476000-backfill-metadata-overrides'; import { AddTypeAndOptionsToApplicationVariablesFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-instance-command-fast-1783065514000-add-type-and-options-to-application-variables'; +import { BackfillMetadataOverridesSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-instance-command-slow-1782986476000-backfill-metadata-overrides'; import { AddCacheTokensToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777455269302-add-cache-tokens-to-agent-chat-thread'; import { AddLogoToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777539664664-add-logo-to-application'; +import { AllowServerScopedFileFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-instance-command-fast-1783499671541-allow-server-scoped-file'; +import { AddCalendarEndFieldMetadataIdToViewFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-22/2-22-instance-command-fast-1783956795000-add-calendar-end-field-metadata-id-to-view'; +import { AddSdkClientCoreChecksumToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-23/2-23-instance-command-fast-1784625638000-add-sdk-client-core-checksum-to-application'; +import { AddAutoUpgradeToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-23/2-23-instance-command-fast-1784297307235-add-auto-upgrade-to-application'; import { AddSubFieldNameToViewSortEarlyFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1747234200000-add-sub-field-name-to-view-sort'; import { AddRelationTargetFieldMetadataIdToViewFilterEarlyFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1747234300000-add-relation-target-field-metadata-id-to-view-filter'; import { AddUpgradeMigrationWorkspaceIdIndexFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777308014234-add-upgrade-migration-workspace-id-index'; @@ -89,34 +94,30 @@ import { EncryptNonSecretApplicationVariableSlowInstanceCommand } from 'src/data import { MigrateAiModelPreferencesSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-slow-1799000010000-migrate-ai-model-preferences'; import { AddFolderImportToMessageFolderPendingSyncActionFastInstanceCommand } from './2-15/2-15-instance-command-fast-1781714499016-add-folder-import-to-message-folder-pending-sync-action'; import { AddViewKanbanColumnWidthFastInstanceCommand } from './2-15/2-15-instance-command-fast-1781900000000-add-view-kanban-column-width'; -import { AddPendingQuestionMessageIdToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-instance-command-fast-1782999138000-add-pending-question-to-agent-chat-thread'; import { AddWorkspaceDiscoverabilityToWorkspaceFastInstanceCommand } from './2-19/2-19-instance-command-fast-1783004140000-add-workspace-discoverability-to-workspace'; +import { AddLogoToApplicationRegistrationFastInstanceCommand } from './2-19/2-19-instance-command-fast-1783069672191-add-logo-to-application-registration'; +import { AddDisplayFieldsToApplicationRegistrationFastInstanceCommand } from './2-19/2-19-instance-command-fast-1783073776590-add-display-fields-to-application-registration'; import { AddStatusToFileFastInstanceCommand } from './2-19/2-19-instance-command-fast-1783082964705-add-status-to-file'; import { AddPendingMimeCheckToFileFastInstanceCommand } from './2-19/2-19-instance-command-fast-1783094691548-add-pending-mime-check-to-file'; -import { DropMetadataStandardOverridesColumnFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783511477234-drop-metadata-standard-overrides-column'; -import { AddLogoToApplicationRegistrationFastInstanceCommand } from './2-19/2-19-instance-command-fast-1783069672191-add-logo-to-application-registration'; import { BackfillLogoOnApplicationRegistrationSlowInstanceCommand } from './2-19/2-19-instance-command-slow-1783069673191-backfill-logo-on-application-registration'; -import { AddDisplayFieldsToApplicationRegistrationFastInstanceCommand } from './2-19/2-19-instance-command-fast-1783073776590-add-display-fields-to-application-registration'; import { BackfillDisplayFieldsOnApplicationRegistrationSlowInstanceCommand } from './2-19/2-19-instance-command-slow-1783073776591-backfill-display-fields-on-application-registration'; import { BackfillIsFeaturedOnApplicationRegistrationSlowInstanceCommand } from './2-19/2-19-instance-command-slow-1783120000000-backfill-is-featured-on-application-registration'; -import { AllowServerScopedFileFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-instance-command-fast-1783499671541-allow-server-scoped-file'; +import { DropMetadataStandardOverridesColumnFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783511477234-drop-metadata-standard-overrides-column'; import { CreateWorkflowVersionCoreTableFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783512000000-create-workflow-version-core-table'; -import { BackfillNameFieldIsSystemSideEffectSlowInstanceCommand } from './2-20/2-20-instance-command-slow-1783529458168-backfill-name-field-is-system-side-effect'; import { RenameIsFeaturedToIsVettedOnApplicationRegistrationFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783527064000-rename-is-featured-to-is-vetted-on-application-registration'; import { AddIsSystemSideEffectToSearchFieldMetadataFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783580127637-add-is-system-side-effect-to-search-field-metadata'; import { CreateWorkflowCoreTableFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783603454479-create-workflow-core-table'; -import { AddGalleryImagesToApplicationRegistrationFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783615890055-add-gallery-images-to-application-registration'; -import { BackfillGalleryImagesOnApplicationRegistrationSlowInstanceCommand } from './2-20/2-20-instance-command-slow-1783615890056-backfill-gallery-images-on-application-registration'; import { AddWorkflowVersionSyncableColumnsFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783603454480-add-workflow-version-syncable-columns'; -import { BackfillWorkspaceDatabaseSchemaSlowInstanceCommand } from './2-21/2-21-instance-command-slow-1783934147089-backfill-workspace-database-schema'; +import { AddGalleryImagesToApplicationRegistrationFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783615890055-add-gallery-images-to-application-registration'; +import { BackfillNameFieldIsSystemSideEffectSlowInstanceCommand } from './2-20/2-20-instance-command-slow-1783529458168-backfill-name-field-is-system-side-effect'; +import { BackfillGalleryImagesOnApplicationRegistrationSlowInstanceCommand } from './2-20/2-20-instance-command-slow-1783615890056-backfill-gallery-images-on-application-registration'; import { AddLogoFileIdToApplicationRegistrationFastInstanceCommand } from './2-21/2-21-instance-command-fast-1783945979243-add-logo-file-id-to-application-registration'; -import { AddCalendarEndFieldMetadataIdToViewFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-22/2-22-instance-command-fast-1783956795000-add-calendar-end-field-metadata-id-to-view'; +import { BackfillWorkspaceDatabaseSchemaSlowInstanceCommand } from './2-21/2-21-instance-command-slow-1783934147089-backfill-workspace-database-schema'; import { AddCreatedWorkspaceActivationStatusSlowInstanceCommand } from './2-22/2-22-instance-command-slow-1784106205000-add-created-workspace-activation-status'; -import { UnlistUnclaimedNpmApplicationRegistrationsSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784322591746-unlist-unclaimed-npm-application-registrations'; -import { BackfillCreatedWorkspaceActivationStatusSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784286705000-backfill-created-workspace-activation-status'; import { AddKanbanAndCalendarWidgetViewTypesFastInstanceCommand } from './2-23/2-23-instance-command-fast-1784620626405-add-kanban-and-calendar-widget-view-types'; import { WidenViewCalendarIntegrityConstraintFastInstanceCommand } from './2-23/2-23-instance-command-fast-1784620626406-widen-view-calendar-integrity-constraint'; -import { AddAutoUpgradeToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-23/2-23-instance-command-fast-1784297307235-add-auto-upgrade-to-application'; +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'; export const INSTANCE_COMMANDS = [ AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand, @@ -236,4 +237,5 @@ export const INSTANCE_COMMANDS = [ AddKanbanAndCalendarWidgetViewTypesFastInstanceCommand, WidenViewCalendarIntegrityConstraintFastInstanceCommand, AddAutoUpgradeToApplicationFastInstanceCommand, + AddSdkClientCoreChecksumToApplicationFastInstanceCommand, ]; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/__tests__/application-manifest-apply.service.spec.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/__tests__/application-manifest-apply.service.spec.ts index 9b7143ad9b..878db218e4 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/__tests__/application-manifest-apply.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/__tests__/application-manifest-apply.service.spec.ts @@ -92,6 +92,7 @@ describe('ApplicationManifestApplyService', () => { workspaceId: WORKSPACE_ID, applicationId: APPLICATION_ID, applicationUniversalIdentifier: 'my-app', + trigger: 'manifest-sync', }); }); diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest-apply.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest-apply.service.ts index de56d4814d..0eba646665 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest-apply.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest-apply.service.ts @@ -51,6 +51,7 @@ export class ApplicationManifestApplyService { workspaceId, applicationId: application.id, applicationUniversalIdentifier: application.universalIdentifier, + trigger: 'manifest-sync', }); } diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-sync.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-sync.service.ts index b7df506377..e1c31a2070 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-sync.service.ts @@ -163,6 +163,7 @@ export class ApplicationSyncService { canBeUninstalled: true, autoUpgrade: false, isSdkLayerStale: false, + sdkClientCoreChecksum: null, applicationRegistrationId: null, primaryPublicDomainId: null, createdAt: now, diff --git a/packages/twenty-server/src/engine/core-modules/application/application.entity.ts b/packages/twenty-server/src/engine/core-modules/application/application.entity.ts index d58db52c64..9975c20b33 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.entity.ts @@ -134,6 +134,13 @@ export class ApplicationEntity extends WorkspaceRelatedEntity { @Column({ nullable: false, type: 'boolean', default: false }) isSdkLayerStale: boolean; + @Column({ nullable: true, type: 'text' }) + @WasIntroducedInUpgrade({ + upgradeCommandName: + '2.23.0_AddSdkClientCoreChecksumToApplicationFastInstanceCommand_1784625638000', + }) + sdkClientCoreChecksum: string | null; + @Column({ nullable: true, type: 'uuid' }) applicationRegistrationId: string | null; diff --git a/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts index 7d48943f65..33b8978224 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts @@ -1,20 +1,50 @@ import { UseGuards } from '@nestjs/common'; -import { Parent, ResolveField } from '@nestjs/graphql'; +import { Args, Parent, Query, ResolveField } from '@nestjs/graphql'; import { isAbsoluteUrl, isDefined } from 'twenty-shared/utils'; import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto'; +import { SdkClientChecksumsDTO } from 'src/engine/core-modules/sdk-client/dtos/sdk-client-checksums.dto'; +import { getInstalledSdkMetadataModule } from 'src/engine/core-modules/sdk-client/utils/get-installed-sdk-metadata-module.util'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @UseGuards(WorkspaceAuthGuard, NoPermissionGuard) @MetadataResolver(() => ApplicationDTO) export class ApplicationResolver { - constructor(private readonly twentyConfigService: TwentyConfigService) {} + constructor( + private readonly twentyConfigService: TwentyConfigService, + private readonly workspaceCacheService: WorkspaceCacheService, + ) {} + + @Query(() => SdkClientChecksumsDTO, { nullable: true }) + async applicationSdkClientChecksums( + @Args('applicationId', { type: () => UUIDScalarType }) + applicationId: string, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + const { flatApplicationMaps } = + await this.workspaceCacheService.getOrRecompute(workspace.id, [ + 'flatApplicationMaps', + ]); + + const application = flatApplicationMaps.byId[applicationId]; + + if (!isDefined(application)) { + return null; + } + + return { + core: application.sdkClientCoreChecksum, + metadata: (await getInstalledSdkMetadataModule()).checksum, + }; + } // Resolves the display url of the logo bundled in the installed // application's public assets, so clients never build file urls themselves. diff --git a/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts b/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts index 7150db9695..7b7e9fdda2 100644 --- a/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts +++ b/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts @@ -65,6 +65,9 @@ export enum MetricsKeys { AiChatTurnCompleted = 'ai-chat/turn-completed', AiChatTurnFailed = 'ai-chat/turn-failed', WorkspaceMetadataCacheLocalEviction = 'workspace-metadata-cache/local-eviction', + SdkClientGenerationSucceeded = 'sdk-client-generation/succeeded', + SdkClientGenerationFailed = 'sdk-client-generation/failed', + SdkClientGenerationDurationMs = 'sdk-client-generation/duration-ms', ConnectedAccountSyncWebhookReceivedMessaging = 'connected-account-sync-webhook/received/messaging', ConnectedAccountSyncWebhookReceivedCalendar = 'connected-account-sync-webhook/received/calendar', } diff --git a/packages/twenty-server/src/engine/core-modules/sdk-client/__tests__/sdk-client-generation.service.spec.ts b/packages/twenty-server/src/engine/core-modules/sdk-client/__tests__/sdk-client-generation.service.spec.ts index 015a7ccc5c..02822f243c 100644 --- a/packages/twenty-server/src/engine/core-modules/sdk-client/__tests__/sdk-client-generation.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/sdk-client/__tests__/sdk-client-generation.service.spec.ts @@ -10,9 +10,11 @@ import { FileStorageService } from 'src/engine/core-modules/file-storage/service import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util'; 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 { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { GENERATE_SDK_CLIENT_JOB_NAME } from 'src/engine/core-modules/sdk-client/jobs/generate-sdk-client.job-constants'; import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { WorkspaceEventBroadcaster } from 'src/engine/subscriptions/workspace-event-broadcaster/workspace-event-broadcaster.service'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; describe('SdkClientGenerationService', () => { @@ -24,6 +26,9 @@ describe('SdkClientGenerationService', () => { > >; let messageQueueService: jest.Mocked>; + let workspaceCacheService: jest.Mocked< + Pick + >; beforeEach(async () => { applicationService = { @@ -32,6 +37,9 @@ describe('SdkClientGenerationService', () => { messageQueueService = { add: jest.fn().mockResolvedValue(undefined), }; + workspaceCacheService = { + getOrRecompute: jest.fn(), + }; const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -45,13 +53,24 @@ describe('SdkClientGenerationService', () => { provide: getRepositoryToken(WorkspaceEntity), useValue: {} as Repository, }, - { provide: WorkspaceCacheService, useValue: {} }, + { provide: WorkspaceCacheService, useValue: workspaceCacheService }, { provide: WorkspaceSchemaFactory, useValue: {} }, { provide: ApplicationService, useValue: applicationService }, { provide: getQueueToken(MessageQueue.workspaceQueue), useValue: messageQueueService, }, + { + provide: WorkspaceEventBroadcaster, + useValue: { broadcast: jest.fn().mockResolvedValue(undefined) }, + }, + { + provide: MetricsService, + useValue: { + incrementCounterBy: jest.fn(), + recordHistogram: jest.fn(), + }, + }, ], }).compile(); @@ -88,6 +107,7 @@ describe('SdkClientGenerationService', () => { workspaceId, applicationId: 'std-app-id', applicationUniversalIdentifier: 'twenty-standard', + trigger: 'workspace-activation', }, { id: `sdk-client:${workspaceId}:std-app-id`, @@ -101,6 +121,7 @@ describe('SdkClientGenerationService', () => { workspaceId, applicationId: 'custom-app-id', applicationUniversalIdentifier: 'workspace-custom', + trigger: 'workspace-activation', }, { id: `sdk-client:${workspaceId}:custom-app-id`, diff --git a/packages/twenty-server/src/engine/core-modules/sdk-client/__tests__/sdk-client.controller.spec.ts b/packages/twenty-server/src/engine/core-modules/sdk-client/__tests__/sdk-client.controller.spec.ts new file mode 100644 index 0000000000..909df1ae5b --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sdk-client/__tests__/sdk-client.controller.spec.ts @@ -0,0 +1,257 @@ +import { NotFoundException } from '@nestjs/common'; +import { Test, type TestingModule } from '@nestjs/testing'; + +import { type Response } from 'express'; + +import { + SDK_CLIENT_MODULE_CACHE_CONTROL, + SDK_CLIENT_MODULE_NO_STORE_CACHE_CONTROL, +} from 'src/engine/core-modules/sdk-client/constants/sdk-client-module-cache-control'; +import { SdkClientController } from 'src/engine/core-modules/sdk-client/controllers/sdk-client.controller'; +import { SdkClientArchiveService } from 'src/engine/core-modules/sdk-client/sdk-client-archive.service'; +import { getInstalledSdkMetadataModule } from 'src/engine/core-modules/sdk-client/utils/get-installed-sdk-metadata-module.util'; +import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; + +jest.mock( + 'src/engine/core-modules/sdk-client/utils/get-installed-sdk-metadata-module.util', + () => ({ + getInstalledSdkMetadataModule: jest.fn(), + }), +); + +const mockGetInstalledSdkMetadataModule = jest.mocked( + getInstalledSdkMetadataModule, +); + +const WORKSPACE_ID = 'workspace-1'; +const APPLICATION_ID = 'app-1'; + +const CORE_MODULE_BUFFER = Buffer.from('core module from archive'); +const PERSISTED_CORE_CHECKSUM = 'b'.repeat(64); + +const workspace = { id: WORKSPACE_ID } as WorkspaceEntity; + +describe('SdkClientController', () => { + let controller: SdkClientController; + let workspaceCacheService: jest.Mocked< + Pick + >; + let sdkClientArchiveService: jest.Mocked< + Pick + >; + let response: jest.Mocked>; + + beforeEach(async () => { + jest.clearAllMocks(); + + workspaceCacheService = { + getOrRecompute: jest.fn().mockResolvedValue({ + flatApplicationMaps: { + byId: { + [APPLICATION_ID]: { + id: APPLICATION_ID, + universalIdentifier: 'my-app', + sdkClientCoreChecksum: PERSISTED_CORE_CHECKSUM, + }, + }, + }, + }), + }; + sdkClientArchiveService = { + getClientModuleFromArchive: jest + .fn() + .mockResolvedValue(CORE_MODULE_BUFFER), + }; + response = { + setHeader: jest.fn(), + send: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + controllers: [SdkClientController], + providers: [ + { provide: WorkspaceCacheService, useValue: workspaceCacheService }, + { + provide: SdkClientArchiveService, + useValue: sdkClientArchiveService, + }, + ], + }).compile(); + + controller = module.get(SdkClientController); + }); + + describe('instance-wide metadata route', () => { + const INSTALLED_METADATA_BUFFER = Buffer.from('installed metadata module'); + const INSTALLED_METADATA_CHECKSUM = 'a'.repeat(64); + + beforeEach(() => { + mockGetInstalledSdkMetadataModule.mockResolvedValue({ + moduleBuffer: INSTALLED_METADATA_BUFFER, + checksum: INSTALLED_METADATA_CHECKSUM, + }); + }); + + it('serves the installed metadata module without touching workspace caches', async () => { + await controller.getInstanceSdkMetadataModule( + response as unknown as Response, + ); + + expect(workspaceCacheService.getOrRecompute).not.toHaveBeenCalled(); + expect( + sdkClientArchiveService.getClientModuleFromArchive, + ).not.toHaveBeenCalled(); + expect(response.send).toHaveBeenCalledWith(INSTALLED_METADATA_BUFFER); + }); + + it('opts out of caching on the bare url', async () => { + await controller.getInstanceSdkMetadataModule( + response as unknown as Response, + ); + + expect(response.setHeader).toHaveBeenCalledWith( + 'Cache-Control', + SDK_CLIENT_MODULE_NO_STORE_CACHE_CONTROL, + ); + }); + + it('serves fingerprinted urls as immutable when the checksum matches the installed module', async () => { + await controller.getInstanceSdkMetadataModule( + response as unknown as Response, + INSTALLED_METADATA_CHECKSUM, + ); + + expect(response.setHeader).toHaveBeenCalledWith( + 'Cache-Control', + SDK_CLIENT_MODULE_CACHE_CONTROL, + ); + }); + + it('opts out of caching when the fingerprint does not match the installed module', async () => { + await controller.getInstanceSdkMetadataModule( + response as unknown as Response, + 'c'.repeat(64), + ); + + expect(response.setHeader).toHaveBeenCalledWith( + 'Cache-Control', + SDK_CLIENT_MODULE_NO_STORE_CACHE_CONTROL, + ); + }); + }); + + it('serves the metadata module from the installed package, not the archive', async () => { + const installedModuleBuffer = Buffer.from('installed metadata module'); + + mockGetInstalledSdkMetadataModule.mockResolvedValue({ + moduleBuffer: installedModuleBuffer, + checksum: 'a'.repeat(64), + }); + + await controller.getSdkModule( + response as unknown as Response, + APPLICATION_ID, + 'metadata', + workspace, + ); + + expect( + sdkClientArchiveService.getClientModuleFromArchive, + ).not.toHaveBeenCalled(); + expect(response.send).toHaveBeenCalledWith(installedModuleBuffer); + }); + + it('serves the core module from the application archive', async () => { + await controller.getSdkModule( + response as unknown as Response, + APPLICATION_ID, + 'core', + workspace, + ); + + expect( + sdkClientArchiveService.getClientModuleFromArchive, + ).toHaveBeenCalledWith({ + workspaceId: WORKSPACE_ID, + applicationId: APPLICATION_ID, + applicationUniversalIdentifier: 'my-app', + moduleName: 'core', + }); + expect(mockGetInstalledSdkMetadataModule).not.toHaveBeenCalled(); + }); + + it('disables MIME sniffing and opts out of HTTP caching on the bare fallback url', async () => { + await controller.getSdkModule( + response as unknown as Response, + APPLICATION_ID, + 'core', + workspace, + ); + + expect(response.setHeader).toHaveBeenCalledWith( + 'Content-Type', + 'application/javascript', + ); + expect(response.setHeader).toHaveBeenCalledWith( + 'X-Content-Type-Options', + 'nosniff', + ); + expect(response.setHeader).toHaveBeenCalledWith( + 'Cache-Control', + SDK_CLIENT_MODULE_NO_STORE_CACHE_CONTROL, + ); + }); + + it('serves fingerprinted urls as immutable when the checksum matches the persisted core checksum', async () => { + await controller.getSdkModule( + response as unknown as Response, + APPLICATION_ID, + 'core', + workspace, + PERSISTED_CORE_CHECKSUM, + ); + + expect(response.setHeader).toHaveBeenCalledWith( + 'Cache-Control', + SDK_CLIENT_MODULE_CACHE_CONTROL, + ); + }); + + it('opts out of caching when the fingerprint does not match the persisted checksum', async () => { + await controller.getSdkModule( + response as unknown as Response, + APPLICATION_ID, + 'core', + workspace, + 'a'.repeat(64), + ); + + expect(response.setHeader).toHaveBeenCalledWith( + 'Cache-Control', + SDK_CLIENT_MODULE_NO_STORE_CACHE_CONTROL, + ); + }); + + it('rejects unknown module names', async () => { + await expect( + controller.getSdkModule( + response as unknown as Response, + APPLICATION_ID, + 'evil' as never, + workspace, + ), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('rejects unknown applications', async () => { + await expect( + controller.getSdkModule( + response as unknown as Response, + 'unknown-app', + 'metadata', + workspace, + ), + ).rejects.toBeInstanceOf(NotFoundException); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/sdk-client/constants/sdk-client-module-cache-control.ts b/packages/twenty-server/src/engine/core-modules/sdk-client/constants/sdk-client-module-cache-control.ts new file mode 100644 index 0000000000..097a882a98 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sdk-client/constants/sdk-client-module-cache-control.ts @@ -0,0 +1,4 @@ +export const SDK_CLIENT_MODULE_CACHE_CONTROL = + 'private, max-age=31536000, immutable'; + +export const SDK_CLIENT_MODULE_NO_STORE_CACHE_CONTROL = 'private, no-store'; diff --git a/packages/twenty-server/src/engine/core-modules/sdk-client/controllers/sdk-client.controller.ts b/packages/twenty-server/src/engine/core-modules/sdk-client/controllers/sdk-client.controller.ts index b8752fcab3..bea09127d4 100644 --- a/packages/twenty-server/src/engine/core-modules/sdk-client/controllers/sdk-client.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/sdk-client/controllers/sdk-client.controller.ts @@ -14,7 +14,12 @@ import { ALLOWED_SDK_MODULES, type SdkModuleName, } from 'src/engine/core-modules/sdk-client/constants/allowed-sdk-modules'; +import { + SDK_CLIENT_MODULE_CACHE_CONTROL, + SDK_CLIENT_MODULE_NO_STORE_CACHE_CONTROL, +} from 'src/engine/core-modules/sdk-client/constants/sdk-client-module-cache-control'; import { SdkClientArchiveService } from 'src/engine/core-modules/sdk-client/sdk-client-archive.service'; +import { getInstalledSdkMetadataModule } from 'src/engine/core-modules/sdk-client/utils/get-installed-sdk-metadata-module.util'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; @@ -29,13 +34,31 @@ export class SdkClientController { private readonly sdkClientArchiveService: SdkClientArchiveService, ) {} - @Get(':applicationId/:moduleName') + @Get(['metadata', 'metadata/:checksum']) + @UseGuards(NoPermissionGuard) + async getInstanceSdkMetadataModule( + @Res() res: Response, + @Param('checksum') checksum?: string, + ) { + const { moduleBuffer, checksum: servedModuleChecksum } = + await getInstalledSdkMetadataModule(); + + this.sendSdkModule({ + res, + moduleBuffer, + servedModuleChecksum, + requestedChecksum: checksum, + }); + } + + @Get([':applicationId/:moduleName', ':applicationId/:moduleName/:checksum']) @UseGuards(NoPermissionGuard) async getSdkModule( @Res() res: Response, @Param('applicationId') applicationId: string, @Param('moduleName') moduleName: SdkModuleName, @AuthWorkspace() workspace: WorkspaceEntity, + @Param('checksum') checksum?: string, ) { if (!ALLOWED_SDK_MODULES.includes(moduleName)) { throw new NotFoundException( @@ -56,15 +79,51 @@ export class SdkClientController { ); } - const fileBuffer = - await this.sdkClientArchiveService.getClientModuleFromArchive({ - workspaceId: workspace.id, - applicationId, - applicationUniversalIdentifier: application.universalIdentifier, - moduleName, - }); + const { moduleBuffer, checksum: servedModuleChecksum } = + moduleName === 'metadata' + ? await getInstalledSdkMetadataModule() + : { + moduleBuffer: + await this.sdkClientArchiveService.getClientModuleFromArchive({ + workspaceId: workspace.id, + applicationId, + applicationUniversalIdentifier: application.universalIdentifier, + moduleName, + }), + checksum: application.sdkClientCoreChecksum, + }; + + this.sendSdkModule({ + res, + moduleBuffer, + servedModuleChecksum, + requestedChecksum: checksum, + }); + } + + private sendSdkModule({ + res, + moduleBuffer, + servedModuleChecksum, + requestedChecksum, + }: { + res: Response; + moduleBuffer: Buffer; + servedModuleChecksum: string | null; + requestedChecksum?: string; + }) { + const isChecksumMatch = + isDefined(requestedChecksum) && + requestedChecksum === servedModuleChecksum; res.setHeader('Content-Type', 'application/javascript'); - res.send(fileBuffer); + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader( + 'Cache-Control', + isChecksumMatch + ? SDK_CLIENT_MODULE_CACHE_CONTROL + : SDK_CLIENT_MODULE_NO_STORE_CACHE_CONTROL, + ); + res.send(moduleBuffer); } } diff --git a/packages/twenty-server/src/engine/core-modules/sdk-client/dtos/sdk-client-checksums.dto.ts b/packages/twenty-server/src/engine/core-modules/sdk-client/dtos/sdk-client-checksums.dto.ts new file mode 100644 index 0000000000..936707e97b --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sdk-client/dtos/sdk-client-checksums.dto.ts @@ -0,0 +1,16 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +@ObjectType('SdkClientChecksums') +export class SdkClientChecksumsDTO { + @IsString() + @IsOptional() + @Field(() => String, { nullable: true }) + core: string | null; + + @IsString() + @IsNotEmpty() + @Field() + metadata: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/sdk-client/jobs/generate-sdk-client.job-constants.ts b/packages/twenty-server/src/engine/core-modules/sdk-client/jobs/generate-sdk-client.job-constants.ts index bdbf9edda0..cd413cb8aa 100644 --- a/packages/twenty-server/src/engine/core-modules/sdk-client/jobs/generate-sdk-client.job-constants.ts +++ b/packages/twenty-server/src/engine/core-modules/sdk-client/jobs/generate-sdk-client.job-constants.ts @@ -1,7 +1,11 @@ +import { type SdkClientGenerationTrigger } from 'src/engine/core-modules/sdk-client/types/sdk-client-generation-trigger.type'; + export const GENERATE_SDK_CLIENT_JOB_NAME = 'GenerateSdkClientJob'; export type GenerateSdkClientJobData = { workspaceId: string; applicationId: string; applicationUniversalIdentifier: string; + // Optional: jobs enqueued before this field existed carry no trigger + trigger?: SdkClientGenerationTrigger; }; diff --git a/packages/twenty-server/src/engine/core-modules/sdk-client/jobs/generate-sdk-client.job.ts b/packages/twenty-server/src/engine/core-modules/sdk-client/jobs/generate-sdk-client.job.ts index cb20aaa03a..b009a53682 100644 --- a/packages/twenty-server/src/engine/core-modules/sdk-client/jobs/generate-sdk-client.job.ts +++ b/packages/twenty-server/src/engine/core-modules/sdk-client/jobs/generate-sdk-client.job.ts @@ -19,6 +19,7 @@ export class GenerateSdkClientJob { workspaceId: data.workspaceId, applicationId: data.applicationId, applicationUniversalIdentifier: data.applicationUniversalIdentifier, + trigger: data.trigger, }); } } diff --git a/packages/twenty-server/src/engine/core-modules/sdk-client/sdk-client-archive.service.ts b/packages/twenty-server/src/engine/core-modules/sdk-client/sdk-client-archive.service.ts index ff7b4aa260..fb5149c97e 100644 --- a/packages/twenty-server/src/engine/core-modules/sdk-client/sdk-client-archive.service.ts +++ b/packages/twenty-server/src/engine/core-modules/sdk-client/sdk-client-archive.service.ts @@ -165,6 +165,7 @@ export class SdkClientArchiveService { workspaceId, applicationId, applicationUniversalIdentifier, + trigger: 'missing-archive', }); } } diff --git a/packages/twenty-server/src/engine/core-modules/sdk-client/sdk-client-generation.service.ts b/packages/twenty-server/src/engine/core-modules/sdk-client/sdk-client-generation.service.ts index 9933655972..bdf887fdef 100644 --- a/packages/twenty-server/src/engine/core-modules/sdk-client/sdk-client-generation.service.ts +++ b/packages/twenty-server/src/engine/core-modules/sdk-client/sdk-client-generation.service.ts @@ -1,6 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { createHash } from 'crypto'; import * as fs from 'fs/promises'; import { printSchema } from 'graphql'; import path, { join } from 'path'; @@ -18,6 +19,9 @@ import { TemporaryDirManager } from 'src/engine/core-modules/logic-function/logi 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 { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; +import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type'; +import { type SdkModuleName } from 'src/engine/core-modules/sdk-client/constants/allowed-sdk-modules'; import { SDK_CLIENT_PACKAGE_DIRNAME } from 'src/engine/core-modules/sdk-client/constants/sdk-client-package-dirname'; import { SdkClientException, @@ -27,8 +31,10 @@ import { GENERATE_SDK_CLIENT_JOB_NAME, type GenerateSdkClientJobData, } from 'src/engine/core-modules/sdk-client/jobs/generate-sdk-client.job-constants'; +import { type SdkClientGenerationTrigger } from 'src/engine/core-modules/sdk-client/types/sdk-client-generation-trigger.type'; import { fromWorkspaceEntityToFlat } from 'src/engine/core-modules/workspace/utils/from-workspace-entity-to-flat.util'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { WorkspaceEventBroadcaster } from 'src/engine/subscriptions/workspace-event-broadcaster/workspace-event-broadcaster.service'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; const SDK_CLIENT_ARCHIVE_NAME = 'twenty-client-sdk.zip'; @@ -49,6 +55,8 @@ export class SdkClientGenerationService { private readonly applicationService: ApplicationService, @InjectMessageQueue(MessageQueue.workspaceQueue) private readonly messageQueueService: MessageQueueService, + private readonly workspaceEventBroadcaster: WorkspaceEventBroadcaster, + private readonly metricsService: MetricsService, ) {} async enqueueSdkClientGenerationForWorkspace( @@ -62,52 +70,101 @@ export class SdkClientGenerationService { await Promise.all( [twentyStandardFlatApplication, workspaceCustomFlatApplication].map( (application) => - this.messageQueueService.add( - GENERATE_SDK_CLIENT_JOB_NAME, - { - workspaceId, - applicationId: application.id, - applicationUniversalIdentifier: application.universalIdentifier, - }, - { - id: `sdk-client:${workspaceId}:${application.id}`, - retryLimit: SDK_CLIENT_GENERATION_RETRY_LIMIT, - }, - ), + this.enqueueSdkClientGenerationForApplication({ + workspaceId, + applicationId: application.id, + applicationUniversalIdentifier: application.universalIdentifier, + trigger: 'workspace-activation', + }), ), ); } + private async enqueueSdkClientGenerationForApplication({ + workspaceId, + applicationId, + applicationUniversalIdentifier, + trigger, + }: { + workspaceId: string; + applicationId: string; + applicationUniversalIdentifier: string; + trigger: SdkClientGenerationTrigger; + }): Promise { + await this.messageQueueService.add( + GENERATE_SDK_CLIENT_JOB_NAME, + { + workspaceId, + applicationId, + applicationUniversalIdentifier, + trigger, + }, + { + id: `sdk-client:${workspaceId}:${applicationId}`, + retryLimit: SDK_CLIENT_GENERATION_RETRY_LIMIT, + }, + ); + } + async generateSdkClientForApplication({ workspaceId, applicationId, applicationUniversalIdentifier, + trigger = 'unknown', }: { workspaceId: string; applicationId: string; applicationUniversalIdentifier: string; + trigger?: SdkClientGenerationTrigger; }): Promise { - const workspaceEntity = await this.workspaceRepository.findOneByOrFail({ - id: workspaceId, - }); + const generationStart = performance.now(); - const graphqlSchema = await this.workspaceSchemaFactory.createGraphQLSchema( - fromWorkspaceEntityToFlat(workspaceEntity), - applicationId, - ); + try { + const workspaceEntity = await this.workspaceRepository.findOneByOrFail({ + id: workspaceId, + }); - const archiveBuffer = await this.generateAndStore({ - workspaceId, - applicationId, - applicationUniversalIdentifier, - schema: printSchema(graphqlSchema), - }); + const graphqlSchema = + await this.workspaceSchemaFactory.createGraphQLSchema( + fromWorkspaceEntityToFlat(workspaceEntity), + applicationId, + ); - this.logger.log( - `Generated SDK client for application ${applicationUniversalIdentifier}`, - ); + const archiveBuffer = await this.generateAndStore({ + workspaceId, + applicationId, + applicationUniversalIdentifier, + schema: printSchema(graphqlSchema), + }); - return archiveBuffer; + const generationDurationMs = performance.now() - generationStart; + + this.metricsService.incrementCounterBy({ + key: MetricsKeys.SdkClientGenerationSucceeded, + amount: 1, + attributes: { trigger }, + }); + this.metricsService.recordHistogram({ + key: MetricsKeys.SdkClientGenerationDurationMs, + value: generationDurationMs, + unit: 'ms', + attributes: { trigger }, + }); + + this.logger.log( + `Generated SDK client for application ${applicationUniversalIdentifier} (trigger: ${trigger})`, + ); + + return archiveBuffer; + } catch (error) { + this.metricsService.incrementCounterBy({ + key: MetricsKeys.SdkClientGenerationFailed, + amount: 1, + attributes: { trigger }, + }); + + throw error; + } } private async generateAndStore({ @@ -145,6 +202,11 @@ export class SdkClientGenerationService { await replaceCoreClient({ packageRoot: tempPackageRoot, schema }); + const sdkClientCoreChecksum = await this.computeSdkModuleChecksum( + tempPackageRoot, + 'core', + ); + const archivePath = join(sourceTemporaryDir, SDK_CLIENT_ARCHIVE_NAME); await createZipFile(tempPackageRoot, archivePath); @@ -162,13 +224,22 @@ export class SdkClientGenerationService { await this.applicationRepository.update( { id: applicationId, workspaceId }, - { isSdkLayerStale: true }, + { + isSdkLayerStale: true, + sdkClientCoreChecksum, + }, ); await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ 'flatApplicationMaps', ]); + await this.broadcastSdkClientCoreChecksumUpdate({ + workspaceId, + applicationId, + sdkClientCoreChecksum, + }); + return archiveBuffer; } catch (error) { throw new SdkClientException( @@ -179,4 +250,52 @@ export class SdkClientGenerationService { await temporaryDirManager.clean(); } } + + private async broadcastSdkClientCoreChecksumUpdate({ + workspaceId, + applicationId, + sdkClientCoreChecksum, + }: { + workspaceId: string; + applicationId: string; + sdkClientCoreChecksum: string; + }): Promise { + try { + await this.workspaceEventBroadcaster.broadcast({ + workspaceId, + events: [ + { + type: 'updated', + entityName: 'application', + recordId: applicationId, + properties: { + updatedFields: ['sdkClientCoreChecksum'], + after: { + id: applicationId, + sdkClientCoreChecksum, + }, + }, + }, + ], + }); + } catch (error) { + this.logger.warn( + `Failed to broadcast SDK client core checksum update for application ${applicationId} in workspace ${workspaceId}`, + error, + ); + } + } + + // sha-256 (not md5) so the renderer can verify cached bundles against the URL + // checksum with WebCrypto, which has no md5 support + private async computeSdkModuleChecksum( + tempPackageRoot: string, + moduleName: SdkModuleName, + ): Promise { + const moduleBuffer = await fs.readFile( + join(tempPackageRoot, 'dist', `${moduleName}.mjs`), + ); + + return createHash('sha256').update(moduleBuffer).digest('hex'); + } } diff --git a/packages/twenty-server/src/engine/core-modules/sdk-client/sdk-client.module.ts b/packages/twenty-server/src/engine/core-modules/sdk-client/sdk-client.module.ts index 712090ea1d..f3522ec375 100644 --- a/packages/twenty-server/src/engine/core-modules/sdk-client/sdk-client.module.ts +++ b/packages/twenty-server/src/engine/core-modules/sdk-client/sdk-client.module.ts @@ -1,12 +1,14 @@ -import { Module } from '@nestjs/common'; +import { Module, type OnApplicationBootstrap } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CoreGraphQLApiModule } from 'src/engine/api/graphql/core-graphql-api.module'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; +import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module'; import { SdkClientController } from 'src/engine/core-modules/sdk-client/controllers/sdk-client.controller'; import { SdkClientArchiveService } from 'src/engine/core-modules/sdk-client/sdk-client-archive.service'; import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service'; +import { getInstalledSdkMetadataModule } from 'src/engine/core-modules/sdk-client/utils/get-installed-sdk-metadata-module.util'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @@ -16,9 +18,14 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache WorkspaceCacheModule, CoreGraphQLApiModule, ApplicationModule, + MetricsModule, ], controllers: [SdkClientController], providers: [SdkClientGenerationService, SdkClientArchiveService], exports: [SdkClientGenerationService, SdkClientArchiveService], }) -export class SdkClientModule {} +export class SdkClientModule implements OnApplicationBootstrap { + async onApplicationBootstrap(): Promise { + await getInstalledSdkMetadataModule(); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/sdk-client/types/sdk-client-generation-trigger.type.ts b/packages/twenty-server/src/engine/core-modules/sdk-client/types/sdk-client-generation-trigger.type.ts new file mode 100644 index 0000000000..d298a890fb --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sdk-client/types/sdk-client-generation-trigger.type.ts @@ -0,0 +1,9 @@ +// What caused an SDK client generation. Emitted as a metric attribute so +// regeneration volume can be broken down by cause. +// 'unknown' covers jobs enqueued before the trigger field existed. +export type SdkClientGenerationTrigger = + | 'workspace-activation' + | 'manifest-sync' + | 'missing-archive' + | 'dev-seeder' + | 'unknown'; diff --git a/packages/twenty-server/src/engine/core-modules/sdk-client/utils/get-installed-sdk-metadata-module.util.ts b/packages/twenty-server/src/engine/core-modules/sdk-client/utils/get-installed-sdk-metadata-module.util.ts new file mode 100644 index 0000000000..a844891440 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sdk-client/utils/get-installed-sdk-metadata-module.util.ts @@ -0,0 +1,32 @@ +import { createHash } from 'crypto'; +import * as fs from 'fs/promises'; +import { join } from 'path'; + +import { SDK_CLIENT_PACKAGE_DIRNAME } from 'src/engine/core-modules/sdk-client/constants/sdk-client-package-dirname'; + +export type InstalledSdkMetadataModule = { + moduleBuffer: Buffer; + checksum: string; +}; + +// Warmed once at bootstrap (SdkClientModule.onApplicationBootstrap) and treated +// as a process-lifetime invariant: the module ships inside the server build and +// never changes at runtime. A read/hash failure blocks boot, so a running +// server always has a resolved value here. +let installedSdkMetadataModule: InstalledSdkMetadataModule | undefined; + +export const getInstalledSdkMetadataModule = + async (): Promise => { + if (!installedSdkMetadataModule) { + const moduleBuffer = await fs.readFile( + join(SDK_CLIENT_PACKAGE_DIRNAME, 'dist', 'metadata.mjs'), + ); + + installedSdkMetadataModule = { + moduleBuffer, + checksum: createHash('sha256').update(moduleBuffer).digest('hex'), + }; + } + + return installedSdkMetadataModule; + }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts index c816207845..35ee4695f1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts @@ -48,6 +48,7 @@ const MOCK_FLAT_APPLICATION: FlatApplication = { updatedAt: new Date(), deletedAt: null, isSdkLayerStale: true, + sdkClientCoreChecksum: null, }; const flatObjectMetadataMaps = [ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/generate-morph-or-relation-flat-field-metadata-pair.spec.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/generate-morph-or-relation-flat-field-metadata-pair.spec.ts index dc6021daaf..c31e2643e7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/generate-morph-or-relation-flat-field-metadata-pair.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/generate-morph-or-relation-flat-field-metadata-pair.spec.ts @@ -43,6 +43,7 @@ const MOCK_FLAT_APPLICATION: FlatApplication = { updatedAt: new Date(), deletedAt: null, isSdkLayerStale: true, + sdkClientCoreChecksum: null, }; type GenerateMorphOrRelationFlatFieldMetadataPairTestInput = Parameters< diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts index 07b5d0870e..cd7cc09e5a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts @@ -128,6 +128,7 @@ export class DevSeederService { applicationId: twentyStandardFlatApplication.id, applicationUniversalIdentifier: twentyStandardFlatApplication.universalIdentifier, + trigger: 'dev-seeder', }); await this.devSeederMetadataService.seed({ @@ -140,6 +141,7 @@ export class DevSeederService { applicationId: workspaceCustomFlatApplication.id, applicationUniversalIdentifier: workspaceCustomFlatApplication.universalIdentifier, + trigger: 'dev-seeder', }); await this.devSeederMetadataService.seedRelations({