From 71a1ff7ac81308c048419dc26e4ef277a141d9da Mon Sep 17 00:00:00 2001
From: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Date: Tue, 21 Jul 2026 17:05:00 +0200
Subject: [PATCH] Cache twenty-client-sdk modules host-side via
content-addressed URLs (#22981)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Context
Front component sources are fetched host-side and integrity-verified by
the SHA-256 checksum embedded in their URL, cached in Cache Storage — a
layer that exists specifically because their download URLs are presigned
and rotate. The `twenty-client-sdk` modules (`core` and `metadata`) were
re-fetched on every render and could not be cached safely: their URLs
carried no checksum and the server exposed no freshness signal.
This PR makes the SDK module URLs **content-addressed** and relies on
the **browser HTTP cache** for immutability, and it keys the checksums
on their real owners: the **application** for `core`, the **instance**
for `metadata`. The checksum does double duty: cache invalidation
(regeneration changes the checksum → the URL changes → guaranteed cache
miss) and a server-side cacheability guard (the server only grants
`immutable` when the checksum in the URL matches the authoritative
checksum it knows for that module — persisted at generation time for
`core`, hashed once at bootstrap for `metadata` — so no per-request
hashing of the served bytes). Note this is **not** an end-to-end
integrity guarantee: there is no client-side hash verification, and on a
fingerprint mismatch the server still serves the current bytes with
`no-store` (self-healing for stale URLs) rather than failing.
Closes twentyhq/core-team-issues#2688.
## Routes
| Module | URL | Scope |
| --- | --- | --- |
| `core` | `/rest/sdk-client/{applicationId}/core[/{checksum}]` | Per
application (generated bundle) |
| `metadata` | `/rest/sdk-client/metadata[/{checksum}]` |
**Instance-wide**: no application segment, so every application
converges on one URL and the browser downloads the module once per
release instead of once per application |
The previous application-scoped metadata path
(`/rest/sdk-client/{applicationId}/metadata[/{checksum}]`) is **kept for
backward compatibility**, new clients just stop generating those URLs.
The instance-wide route is declared before the parameterized route so
`metadata/{checksum}` is not swallowed as `:applicationId/:moduleName`.
## Caching model
| Request | `Cache-Control` | Effect |
| --- | --- | --- |
| Fingerprinted URL, checksum matches the known module checksum |
`immutable` | Cached indefinitely by the browser HTTP cache; a new
checksum is a new URL |
| Fingerprinted URL, checksum does not match | `no-store` | Current
bytes served uncached (self-healing for stale URLs) |
| Bare URL (pre-generation fallback, `core` only in practice) |
`no-store` | Never cached |
- Both responses also set `X-Content-Type-Options: nosniff` and
`Content-Type: application/javascript`.
- SDK modules are intentionally **not** placed in Cache Storage. That
layer stays reserved for the presigned/rotating component-source URLs;
SDK modules are served directly and authenticated, so the browser HTTP
cache (keyed by the content-addressed URL) is their single cache layer.
## Checksum provenance
- **core** — per **application**, persisted on
`application.sdkClientCoreChecksum` at generation time and read back
from `flatApplicationMaps` (never re-hashed per request).
- **metadata** — **instance-wide**, hashed once from the installed
`twenty-client-sdk/dist/metadata.mjs` package (warmed at bootstrap,
memoized per process) and served straight from that package, so it is
fresh from the first request after a release with no archive dependency.
## Server (twenty-server)
- Hash `dist/core.mjs` at SDK generation and persist
`sdkClientCoreChecksum` via `applicationRepository.update`. Adds the
nullable text column to `application.entity.ts` (mirroring
`packageJsonChecksum`) plus a fast instance command with up/down;
`FlatApplication` picks it up automatically.
- New **application-scoped** query
`applicationSdkClientChecksums(applicationId: UUID!):
SdkClientChecksums` on `ApplicationResolver` (metadata schema,
`WorkspaceAuthGuard` + `NoPermissionGuard`). `SdkClientChecksums.core`
is **nullable** and stays `null` until the SDK has been generated at
least once; `metadata` is **always present** (bootstrap-warmed), so the
metadata module is cacheable from the very first render of any app. The
query itself returns `null` only for unknown applications.
- `SdkClientChecksumsDTO` now lives in the shared
`core-modules/sdk-client/dtos/`. `FrontComponentDTO` and the
`frontComponent` resolver no longer carry checksums (decoupled from the
front-component row).
- `sdk-client` controller: instance-wide `metadata[/:checksum]` route
(no workspace-cache or application lookup, serves the memoized installed
module) + application-scoped `:applicationId/:moduleName[/:checksum]`
route (serves `core` from the per-application archive, `metadata` kept
for back-compat). Cacheability compares the URL checksum against the
**known** checksum — persisted `sdkClientCoreChecksum` for `core`,
memoized package hash for `metadata` — instead of hashing the served
bytes on every request: `immutable` on match, `no-store` otherwise (bare
URL or stale fingerprint), plus `nosniff`. A persisted checksum out of
sync with the archive only downgrades to `no-store` until the next
regeneration.
## Front (twenty-front)
- New metadata query `GetApplicationSdkClientChecksums`, keyed by
`applicationId`; removed the `sdkClientChecksums` selection from
`FindOneFrontComponent`.
- `getSdkClientUrls` builds the two module URLs independently:
`/sdk-client/{applicationId}/core/{checksum}` and the **instance-wide**
`/sdk-client/metadata/{checksum}` (no application segment → one shared
browser cache entry per release across all applications). Each falls
back to its bare URL when its checksum is absent — since `core` is
nullable, a never-generated app still gets a content-addressed metadata
URL and only `core` falls back. The checksum type is sourced from the
codegen `SdkClientChecksums` type rather than a hand-maintained
duplicate.
- `FrontComponentRenderer` is split into a gating outer component (runs
`FindOneFrontComponent`, renders nothing while loading) and a content
component that receives a guaranteed-non-null `frontComponent`.
Following project conventions, the side effects live in dedicated effect
components: `FrontComponentLoadErrorSnackBarEffect` (query error →
snackbar) and `FrontComponentApplicationTokenPairEffect` (mirrors the
query-derived token pair into component state unconditionally, `null`
included, so revoked credentials can never be retained or refreshed).
The content component fetches checksums via the application-keyed query
and **gates the mount of SDK-using components on that query**, so the
very first module fetch is always the content-addressed (`immutable`)
URL instead of the bare `no-store` one. Non-SDK components skip the
query and are never blocked.
- **Live invalidation without reload:** SDK regeneration updates the
application row, and the server broadcasts an `application` metadata
event carrying the new core checksum.
`useOnApplicationSdkClientChecksumsUpdated` /
`useUpdateSdkClientChecksumsApolloCache` patch the application-keyed
checksum query cache (core only; the instance-wide metadata is
preserved), so every mounted component of that application picks up the
new URL at once. This replaces the previous frontComponent-derived field
and closes the earlier "known gap" (a mounted component staying on a
session-old checksum until a full reload). The cache-patching callback
is memoized (`useCallback`) so the window listener is registered once
per application, and the listener is **skipped entirely** for non-SDK
components (`useListenToMetadataOperationBrowserEvent` gained a `skip`
option) — they register no listener and never refetch a query they don't
consume.
## Renderer (twenty-front-component-renderer)
- SDK sources are fetched through a dedicated plain authenticated fetch,
`fetchJavaScriptModuleSourceText` (Bearer header, `credentials:
'omit'`), instead of the Cache Storage `fetchComponentSource` path;
`fetchSdkClientSources` uses it. Execution stays exclusively in the
opaque-origin worker via blob URLs; the host only fetches and forwards
source strings (no hashing host-side). Staleness self-resolves through
the checksum: new checksum → new URL → cache miss.
---
.../src/metadata/generated/schema.graphql | 6 +
.../src/metadata/generated/schema.ts | 23 +
.../src/metadata/generated/types.ts | 1394 +++++++++--------
.../constants/SdkClientImportSpecifiers.ts | 0
.../fetchJavaScriptModuleSourceText.spec.ts} | 36 +-
.../utils/fetchJavaScriptModuleSourceText.ts | 33 +
.../src/host/utils/fetchSdkClientSources.ts | 18 +
.../components/FrontComponentWorkerEffect.tsx | 26 +-
.../createSdkClientModuleBlobUrls.test.ts | 26 +
.../fetchSdkClientModulesAsBlobUrls.test.ts | 99 --
.../utils/containsSdkClientImportSpecifier.ts | 4 -
.../utils/createSdkClientModuleBlobUrls.ts | 10 +
.../utils/fetchJavaScriptModuleSourceText.ts | 28 -
.../utils/fetchSdkClientModulesAsBlobUrls.ts | 34 -
.../worker/utils/loadFrontComponentModule.ts | 27 +-
.../worker/utils/renderFrontComponent.ts | 3 +-
.../rewriteSdkClientImportsToBlobUrls.ts | 14 +-
.../src/types/HostToWorkerRenderContext.ts | 4 +-
.../src/types/SdkClientSources.ts | 4 +
.../containsSdkClientImportSpecifier.test.ts | 61 +
.../buildSdkClientImportContextPattern.ts | 8 +
.../utils/containsSdkClientImportSpecifier.ts | 7 +
.../src/generated-metadata/graphql.ts | 20 +
...seListenToMetadataOperationBrowserEvent.ts | 8 +-
.../types/BroadcastEntityName.ts | 1 +
...ontComponentApplicationTokenPairEffect.tsx | 25 +
.../FrontComponentLoadErrorSnackBarEffect.tsx | 24 +
.../components/FrontComponentRenderer.tsx | 163 +-
.../getApplicationSdkClientChecksums.ts | 10 +
...dateSdkClientChecksumsApolloCache.test.tsx | 194 +++
...eOnApplicationSdkClientChecksumsUpdated.ts | 26 +
.../useUpdateSdkClientChecksumsApolloCache.ts | 76 +
...cationSdkClientChecksumsBroadcastRecord.ts | 4 +
.../utils/__tests__/getSdkClientUrls.test.ts | 28 +-
.../utils/getSdkClientUrls.ts | 25 +-
...sdk-client-core-checksum-to-application.ts | 21 +
.../instance-commands.constant.ts | 30 +-
...application-manifest-apply.service.spec.ts | 1 +
.../application-manifest-apply.service.ts | 1 +
.../application-sync.service.ts | 1 +
.../application/application.entity.ts | 7 +
.../application/application.resolver.ts | 34 +-
.../metrics/types/metrics-keys.type.ts | 3 +
.../sdk-client-generation.service.spec.ts | 23 +-
.../__tests__/sdk-client.controller.spec.ts | 257 +++
.../sdk-client-module-cache-control.ts | 4 +
.../controllers/sdk-client.controller.ts | 77 +-
.../dtos/sdk-client-checksums.dto.ts | 16 +
.../jobs/generate-sdk-client.job-constants.ts | 4 +
.../jobs/generate-sdk-client.job.ts | 1 +
.../sdk-client/sdk-client-archive.service.ts | 1 +
.../sdk-client-generation.service.ts | 179 ++-
.../sdk-client/sdk-client.module.ts | 11 +-
.../sdk-client-generation-trigger.type.ts | 9 +
.../get-installed-sdk-metadata-module.util.ts | 32 +
...-to-flat-field-metadatas-to-create.spec.ts | 1 +
...-relation-flat-field-metadata-pair.spec.ts | 1 +
.../dev-seeder/services/dev-seeder.service.ts | 2 +
58 files changed, 2137 insertions(+), 1048 deletions(-)
rename packages/twenty-front-component-renderer/src/{remote/worker => }/constants/SdkClientImportSpecifiers.ts (100%)
rename packages/twenty-front-component-renderer/src/{remote/worker/utils/__tests__/fetchJavaScriptModuleSourceText.test.ts => host/utils/__tests__/fetchJavaScriptModuleSourceText.spec.ts} (62%)
create mode 100644 packages/twenty-front-component-renderer/src/host/utils/fetchJavaScriptModuleSourceText.ts
create mode 100644 packages/twenty-front-component-renderer/src/host/utils/fetchSdkClientSources.ts
create mode 100644 packages/twenty-front-component-renderer/src/remote/worker/utils/__tests__/createSdkClientModuleBlobUrls.test.ts
delete mode 100644 packages/twenty-front-component-renderer/src/remote/worker/utils/__tests__/fetchSdkClientModulesAsBlobUrls.test.ts
delete mode 100644 packages/twenty-front-component-renderer/src/remote/worker/utils/containsSdkClientImportSpecifier.ts
create mode 100644 packages/twenty-front-component-renderer/src/remote/worker/utils/createSdkClientModuleBlobUrls.ts
delete mode 100644 packages/twenty-front-component-renderer/src/remote/worker/utils/fetchJavaScriptModuleSourceText.ts
delete mode 100644 packages/twenty-front-component-renderer/src/remote/worker/utils/fetchSdkClientModulesAsBlobUrls.ts
create mode 100644 packages/twenty-front-component-renderer/src/types/SdkClientSources.ts
create mode 100644 packages/twenty-front-component-renderer/src/utils/__tests__/containsSdkClientImportSpecifier.test.ts
create mode 100644 packages/twenty-front-component-renderer/src/utils/buildSdkClientImportContextPattern.ts
create mode 100644 packages/twenty-front-component-renderer/src/utils/containsSdkClientImportSpecifier.ts
create mode 100644 packages/twenty-front/src/modules/front-components/components/FrontComponentApplicationTokenPairEffect.tsx
create mode 100644 packages/twenty-front/src/modules/front-components/components/FrontComponentLoadErrorSnackBarEffect.tsx
create mode 100644 packages/twenty-front/src/modules/front-components/graphql/queries/getApplicationSdkClientChecksums.ts
create mode 100644 packages/twenty-front/src/modules/front-components/hooks/__tests__/useUpdateSdkClientChecksumsApolloCache.test.tsx
create mode 100644 packages/twenty-front/src/modules/front-components/hooks/useOnApplicationSdkClientChecksumsUpdated.ts
create mode 100644 packages/twenty-front/src/modules/front-components/hooks/useUpdateSdkClientChecksumsApolloCache.ts
create mode 100644 packages/twenty-front/src/modules/front-components/types/ApplicationSdkClientChecksumsBroadcastRecord.ts
create mode 100644 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
create mode 100644 packages/twenty-server/src/engine/core-modules/sdk-client/__tests__/sdk-client.controller.spec.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/sdk-client/constants/sdk-client-module-cache-control.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/sdk-client/dtos/sdk-client-checksums.dto.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/sdk-client/types/sdk-client-generation-trigger.type.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/sdk-client/utils/get-installed-sdk-metadata-module.util.ts
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({