From b87c7d4a0ca0523c54961664595ccfa2c74fdfa7 Mon Sep 17 00:00:00 2001 From: Hitarth Sheth <133380930+Hitarthsheth07@users.noreply.github.com> Date: Mon, 21 Oct 2024 10:07:14 -0400 Subject: [PATCH 01/13] [FIX] install node version before corepack enable (#7809) FIX #7696 This correctly installs the recommended node version before enabling corepack. ![Screenshot 2024-10-14 142628](https://github.com/user-attachments/assets/674f28d5-8d1b-40fc-aaae-4195c14ea350) Thanks! --- .../src/content/developers/local-setup.mdx | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/twenty-website/src/content/developers/local-setup.mdx b/packages/twenty-website/src/content/developers/local-setup.mdx index b66ea2ee45aa..1db380bcec2f 100644 --- a/packages/twenty-website/src/content/developers/local-setup.mdx +++ b/packages/twenty-website/src/content/developers/local-setup.mdx @@ -48,15 +48,28 @@ git config --global user.name "Your Name" git config --global user.email "youremail@domain.com" ``` -3. Install Node.js, nvm, yarn +3. Install nvm, node.js and yarn + + +Use `nvm` to install the correct `node` version. The `.nvmrc` ensures all contributors use the same version. + + + ```bash sudo apt-get install curl curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash +``` +Close and reopen your terminal to use nvm. Then run the following commands. + +```bash + +nvm install # installs recommended node version + +nvm use # use recommended node version corepack enable ``` -Close and reopen your terminal to start using nvm. @@ -186,17 +199,8 @@ cp ./packages/twenty-server/.env.example ./packages/twenty-server/.env ``` ## Step 6: Installing dependencies - - - -Use `nvm` to install the correct `node` version. The `.nvmrc` ensures all contributors use the same version. - - - -To build Twenty server and seed some data into your database, run the following commands: +To build Twenty server and seed some data into your database, run the following command: ```bash -nvm install # installs recommended node version -nvm use # use recommended node version yarn ``` @@ -280,4 +284,4 @@ This should work out of the box with the eslint extension installed. If this doe } ``` - + \ No newline at end of file From 1f5d9bd69e5ff4bb1600f07bed585d118aa19238 Mon Sep 17 00:00:00 2001 From: sateshcharan Date: Mon, 21 Oct 2024 19:38:26 +0530 Subject: [PATCH 02/13] =?UTF-8?q?[=F0=9F=95=B9=EF=B8=8F]=20quest-wizard=20?= =?UTF-8?q?=20(300=20points)=20(#7921)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ![image](https://github.com/user-attachments/assets/56fa4f09-fa11-41ae-9198-dbd92c0596fd) ![image](https://github.com/user-attachments/assets/543a1f8f-b196-4b3b-be2d-7ced9dd9e0e0) ![image](https://github.com/user-attachments/assets/5d13b4a3-1f40-4e1b-a368-fe95c49cbb2c) ![image](https://github.com/user-attachments/assets/936457e6-d18c-444f-a853-162b75b9b807) ![image](https://github.com/user-attachments/assets/c48e4b5e-30a6-404b-bea3-7633a499c69b) --- oss-gg/twenty-side-quest/6-quest-wizard.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oss-gg/twenty-side-quest/6-quest-wizard.md b/oss-gg/twenty-side-quest/6-quest-wizard.md index 9543e3767d6f..1b7f1c8c5f58 100644 --- a/oss-gg/twenty-side-quest/6-quest-wizard.md +++ b/oss-gg/twenty-side-quest/6-quest-wizard.md @@ -16,4 +16,6 @@ Your turn 👇 » 01-October-2024 by X +» 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) + --- From 9aed944da94d6efbbac18f461e8d9276fe0303fe Mon Sep 17 00:00:00 2001 From: sateshcharan Date: Mon, 21 Oct 2024 19:48:12 +0530 Subject: [PATCH 03/13] [oss.gg] design-promotional-poster (300 points) (#7919) ![image](https://github.com/user-attachments/assets/763c57a3-4dd1-4882-a989-d6df3f00269f) --- .../1-design-promotional-poster-20-share.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md b/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md index 7bfb7d49382a..481fa9572d17 100644 --- a/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md +++ b/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md @@ -29,4 +29,7 @@ Your turn 👇 » 17-October-2024 by [Atharva Deshmukh](https://oss.gg/Atharva-3000) poster Link: [poster](https://x.com/0x_atharva/status/1846915861191577697) » 20-October-2024 by [Naprila](https://oss.gg/Naprila) poster Link: [poster](https://x.com/mkprasad_821/status/1848037527921254625) + +» 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) poster Link: [poster](https://x.com/sateshcharans/status/1848358958970396727) + --- From 4bd8945519ed4b0e87beb27fec2a67c4de06c4ba Mon Sep 17 00:00:00 2001 From: sateshcharan Date: Mon, 21 Oct 2024 19:53:30 +0530 Subject: [PATCH 04/13] [oss.gg] design-new-logo-twenty (#7917) ![image](https://github.com/user-attachments/assets/9b1fd757-1fd5-402d-ab6d-9389d738d97f) --- oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md b/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md index f29319349cbf..ab02480c9aa1 100644 --- a/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md +++ b/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md @@ -30,4 +30,6 @@ Your turn 👇 » 20-October-2024 by [Naprila](https://oss.gg/Naprila) Logo Link: [logo](https://drive.google.com/file/d/105fWXNtOkOPkU31AV0FDZKOdrJ8XLwBb/view?usp=drivesdk) » tweet Link: [tweet](https://x.com/mkprasad_821/status/1847978789713695133) +» 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) Logo Link: [logo](https://drive.google.com/file/d/1fwvOcg8oQZC3NlTNV8EcyJxh9v_OYdpY/view?usp=sharing) » tweet Link: [tweet](https://x.com/sateshcharans/status/1848344729483690455) + --- From edf4ae084bbc9905b70bdd1fb9814d41a0be4417 Mon Sep 17 00:00:00 2001 From: Weiko Date: Mon, 21 Oct 2024 17:23:50 +0200 Subject: [PATCH 05/13] Add simplify-search-vector-expression to upgrade 0-32 command (#7925) ## Context Moving this command to the global command runner for 0.32 upgrade. This should fix searchVector expression introduced in 0.31 to later handle soft-deleted + search filter properly. ## Test ``` git co v0.31.0 -- reset DB git co c--add-simplify-search-vector-expression-to-upgrade-0-32 -- migrate typeorm -- upgrade 0.32 command ``` search is working and expression is correctly set. Soft deleted records are not shown as before but it's now possible to override that behavior via filters. cc @ijreilly --- .../commands/database-command.module.ts | 2 -- ...implify-search-vector-expression.module.ts | 21 ------------------- .../0-32-simplify-search-vector-expression.ts | 0 .../0-32/0-32-upgrade-version.command.ts | 8 +++++++ .../0-32/0-32-upgrade-version.module.ts | 17 +++++++++++++-- 5 files changed, 23 insertions(+), 25 deletions(-) delete mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-31/0-32/0-32-simplify-search-vector-expression.module.ts rename packages/twenty-server/src/database/commands/upgrade-version/{0-31 => }/0-32/0-32-simplify-search-vector-expression.ts (100%) diff --git a/packages/twenty-server/src/database/commands/database-command.module.ts b/packages/twenty-server/src/database/commands/database-command.module.ts index 808785d9f497..f8207c318b24 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -7,7 +7,6 @@ import { DataSeedDemoWorkspaceCommand } from 'src/database/commands/data-seed-de import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module'; import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command'; import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question'; -import { SimplifySearchVectorExpressionCommandModule } from 'src/database/commands/upgrade-version/0-31/0-32/0-32-simplify-search-vector-expression.module'; import { UpgradeTo0_32CommandModule } from 'src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; @@ -47,7 +46,6 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp DataSeedDemoWorkspaceModule, WorkspaceCacheStorageModule, WorkspaceMetadataVersionModule, - SimplifySearchVectorExpressionCommandModule, UpgradeTo0_32CommandModule, FeatureFlagModule, ], diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-32/0-32-simplify-search-vector-expression.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-32/0-32-simplify-search-vector-expression.module.ts deleted file mode 100644 index 9e6ea5e2be04..000000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-32/0-32-simplify-search-vector-expression.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { SimplifySearchVectorExpressionCommand } from 'src/database/commands/upgrade-version/0-31/0-32/0-32-simplify-search-vector-expression'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { SearchModule } from 'src/engine/metadata-modules/search/search.module'; -import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; -import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module'; - -@Module({ - imports: [ - TypeOrmModule.forFeature([Workspace], 'core'), - TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'), - WorkspaceSyncMetadataCommandsModule, - SearchModule, - WorkspaceMigrationRunnerModule, - ], - providers: [SimplifySearchVectorExpressionCommand], -}) -export class SimplifySearchVectorExpressionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-32/0-32-simplify-search-vector-expression.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-simplify-search-vector-expression.ts similarity index 100% rename from packages/twenty-server/src/database/commands/upgrade-version/0-31/0-32/0-32-simplify-search-vector-expression.ts rename to packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-simplify-search-vector-expression.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.command.ts index 44d5c4a639ef..14042a17d3a9 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.command.ts @@ -4,6 +4,7 @@ import { Command } from 'nest-commander'; import { Repository } from 'typeorm'; import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; +import { SimplifySearchVectorExpressionCommand } from 'src/database/commands/upgrade-version/0-32/0-32-simplify-search-vector-expression'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command'; @@ -23,6 +24,7 @@ export class UpgradeTo0_32Command extends ActiveWorkspacesCommandRunner { protected readonly workspaceRepository: Repository, private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand, private readonly enforceUniqueConstraintsCommand: EnforceUniqueConstraintsCommand, + private readonly simplifySearchVectorExpressionCommand: SimplifySearchVectorExpressionCommand, ) { super(workspaceRepository); } @@ -41,6 +43,12 @@ export class UpgradeTo0_32Command extends ActiveWorkspacesCommandRunner { workspaceIds, ); + await this.simplifySearchVectorExpressionCommand.executeActiveWorkspacesCommand( + passedParam, + options, + workspaceIds, + ); + await this.enforceUniqueConstraintsCommand.executeActiveWorkspacesCommand( passedParam, options, diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module.ts index 1a6ec704f424..2018c4563a54 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module.ts @@ -2,17 +2,30 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { EnforceUniqueConstraintsCommand } from 'src/database/commands/upgrade-version/0-32/0-32-enforce-unique-constraints.command'; +import { SimplifySearchVectorExpressionCommand } from 'src/database/commands/upgrade-version/0-32/0-32-simplify-search-vector-expression'; import { UpgradeTo0_32Command } from 'src/database/commands/upgrade-version/0-32/0-32-upgrade-version.command'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { SearchModule } from 'src/engine/metadata-modules/search/search.module'; +import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module'; @Module({ imports: [ TypeOrmModule.forFeature([Workspace], 'core'), - TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'), + TypeOrmModule.forFeature( + [ObjectMetadataEntity, FieldMetadataEntity], + 'metadata', + ), WorkspaceSyncMetadataCommandsModule, + SearchModule, + WorkspaceMigrationRunnerModule, + ], + providers: [ + UpgradeTo0_32Command, + EnforceUniqueConstraintsCommand, + SimplifySearchVectorExpressionCommand, ], - providers: [UpgradeTo0_32Command, EnforceUniqueConstraintsCommand], }) export class UpgradeTo0_32CommandModule {} From 373926b8955e317662c1fbde76e5307803c6d0c3 Mon Sep 17 00:00:00 2001 From: Ana Sofia Marin Alexandre <61988046+anamarn@users.noreply.github.com> Date: Mon, 21 Oct 2024 12:42:52 -0300 Subject: [PATCH 06/13] Secure connexion between TinyBird and webhookResponseGraph (#7913) TLDR: Secure connexion between tinybird and twenty using jwt when accessing datasource from tinybird. Solves: https://github.com/twentyhq/private-issues/issues/73 In order to test: 1. Set ANALYTICS_ENABLED to true 2. Set TINYBIRD_JWT_TOKEN to the ADMIN token from the workspace twenty_analytics_playground 3. Set TINYBIRD_JWT_TOKEN to the datasource or your admin token from the workspace twenty_analytics_playground 4. Create a Webhook in twenty and set wich events it needs to track 5. Run twenty-worker in order to make the webhooks work. 6. Do your tasks in order to populate the data 7. Enter to settings> webhook>your webhook and the statistics section should be displayed. --------- Co-authored-by: Charles Bochet --- .github/workflows/ci-chrome-extension.yaml | 4 +- .github/workflows/ci-server.yaml | 39 +++++++++------ .vscode/settings.json | 2 +- .../twenty-front/src/generated/graphql.tsx | 10 ++-- .../modules/auth/states/currentUserState.ts | 1 + ...tingsDevelopersWebhookUsageGraphEffect.tsx | 14 ++++-- .../useAnalyticsTinybirdJwt.test.tsx | 47 +++++++++++++++++++ .../webhook/hooks/useAnalyticsTinybirdJwt.ts | 18 +++++++ .../developers/webhook/hooks/useGraphData.tsx | 8 ++++ .../webhook/utils/fetchGraphDataOrThrow.ts | 6 ++- .../graphql/fragments/userQueryFragment.ts | 1 + .../analytics/analytics.module.ts | 3 ++ .../analytics/analytics.resolver.spec.ts | 10 +--- .../analytics/analytics.resolver.ts | 6 +-- .../analytics/analytics.service.spec.ts | 7 ++- .../analytics/analytics.service.ts | 25 +++++++++- .../environment/environment-variables.ts | 10 +++- .../engine/core-modules/user/user.module.ts | 2 + .../engine/core-modules/user/user.resolver.ts | 11 +++++ 19 files changed, 178 insertions(+), 46 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/hooks/__tests__/useAnalyticsTinybirdJwt.test.tsx create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt.ts diff --git a/.github/workflows/ci-chrome-extension.yaml b/.github/workflows/ci-chrome-extension.yaml index 15d99938b130..6f99f922a94d 100644 --- a/.github/workflows/ci-chrome-extension.yaml +++ b/.github/workflows/ci-chrome-extension.yaml @@ -34,10 +34,10 @@ jobs: packages/twenty-chrome-extension/** - name: Install dependencies - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Chrome Extension / Run build - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx build twenty-chrome-extension - name: Mark as Valid if No Changes diff --git a/.github/workflows/ci-server.yaml b/.github/workflows/ci-server.yaml index 101e1df3b47f..9b24cacc4445 100644 --- a/.github/workflows/ci-server.yaml +++ b/.github/workflows/ci-server.yaml @@ -37,30 +37,33 @@ jobs: id: changed-files uses: tj-actions/changed-files@v11 with: - files: 'package.json, packages/twenty-server/**, packages/twenty-emails/**' + files: | + package.json + packages/twenty-server/** + packages/twenty-emails/** - name: Install dependencies - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Server / Restore Task Cache - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:backend - name: Server / Run lint & typecheck - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/nx-affected with: tag: scope:backend tasks: lint,typecheck - name: Server / Build - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx build twenty-server - name: Server / Write .env - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx reset:env twenty-server - name: Worker / Run - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx run twenty-server:worker:ci server-test: @@ -78,18 +81,21 @@ jobs: id: changed-files uses: tj-actions/changed-files@v11 with: - files: 'package.json, packages/twenty-server/**, packages/twenty-emails/**' + files: | + package.json + packages/twenty-server/** + packages/twenty-emails/** - name: Install dependencies - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Server / Restore Task Cache - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:backend - name: Server / Run Tests - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/nx-affected with: tag: scope:backend @@ -122,18 +128,21 @@ jobs: id: changed-files uses: tj-actions/changed-files@v11 with: - files: 'package.json, packages/twenty-server/**, packages/twenty-emails/**' + files: | + package.json + packages/twenty-server/** + packages/twenty-emails/** - name: Install dependencies - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Server / Restore Task Cache - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:backend - name: Server / Run Integration Tests - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/nx-affected with: tag: scope:backend diff --git a/.vscode/settings.json b/.vscode/settings.json index d63c92973cfc..fde6cdfb1ffb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -45,5 +45,5 @@ "search.exclude": { "**/.yarn": true, }, - "eslint.debug": true + "eslint.debug": true, } diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 7f053fc6b10f..aab2dbddd2d5 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1058,6 +1058,7 @@ export type UpdateWorkspaceInput = { export type User = { __typename?: 'User'; + analyticsTinybirdJwt?: Maybe; canImpersonate: Scalars['Boolean']; createdAt: Scalars['DateTime']; defaultAvatarUrl?: Maybe; @@ -1520,7 +1521,7 @@ export type ImpersonateMutationVariables = Exact<{ }>; -export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type RenewTokenMutationVariables = Exact<{ appToken: Scalars['String']; @@ -1553,7 +1554,7 @@ export type VerifyMutationVariables = Exact<{ }>; -export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type CheckUserExistsQueryVariables = Exact<{ email: Scalars['String']; @@ -1607,7 +1608,7 @@ export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string] export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }; +export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -1624,7 +1625,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } }; export type ActivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; @@ -1825,6 +1826,7 @@ export const UserQueryFragmentFragmentDoc = gql` email canImpersonate supportUserHash + analyticsTinybirdJwt onboardingStatus workspaceMember { ...WorkspaceMemberQueryFragment diff --git a/packages/twenty-front/src/modules/auth/states/currentUserState.ts b/packages/twenty-front/src/modules/auth/states/currentUserState.ts index 2feedc94fea6..352013254120 100644 --- a/packages/twenty-front/src/modules/auth/states/currentUserState.ts +++ b/packages/twenty-front/src/modules/auth/states/currentUserState.ts @@ -7,6 +7,7 @@ export type CurrentUser = Pick< | 'id' | 'email' | 'supportUserHash' + | 'analyticsTinybirdJwt' | 'canImpersonate' | 'onboardingStatus' | 'userVars' diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx index 6d4fd06dc0cf..322ec64cf3ab 100644 --- a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx +++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx @@ -1,6 +1,6 @@ import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData'; import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useSetRecoilState } from 'recoil'; type SettingsDevelopersWebhookUsageGraphEffectProps = { @@ -11,14 +11,18 @@ export const SettingsDevelopersWebhookUsageGraphEffect = ({ webhookId, }: SettingsDevelopersWebhookUsageGraphEffectProps) => { const setWebhookGraphData = useSetRecoilState(webhookGraphDataState); + const [isLoaded, setIsLoaded] = useState(false); const { fetchGraphData } = useGraphData(webhookId); useEffect(() => { - fetchGraphData('7D').then((graphInput) => { - setWebhookGraphData(graphInput); - }); - }, [fetchGraphData, setWebhookGraphData, webhookId]); + if (!isLoaded) { + fetchGraphData('7D').then((graphInput) => { + setWebhookGraphData(graphInput); + }); + setIsLoaded(true); + } + }, [fetchGraphData, isLoaded, setWebhookGraphData, webhookId]); return <>; }; diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/hooks/__tests__/useAnalyticsTinybirdJwt.test.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/__tests__/useAnalyticsTinybirdJwt.test.tsx new file mode 100644 index 000000000000..70eb919a137e --- /dev/null +++ b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/__tests__/useAnalyticsTinybirdJwt.test.tsx @@ -0,0 +1,47 @@ +import { renderHook } from '@testing-library/react'; + +import { CurrentUser, currentUserState } from '@/auth/states/currentUserState'; +import { useAnalyticsTinybirdJwt } from '@/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt'; +import { act } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; + +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); + +describe('useAnalyticsTinybirdJwt', () => { + it('should return the analytics jwt token', async () => { + const { result } = renderHook( + () => { + const setCurrentUserState = useSetRecoilState(currentUserState); + + return { + useAnalyticsTinybirdJwt: useAnalyticsTinybirdJwt(), + setCurrentUserState, + }; + }, + { wrapper: Wrapper }, + ); + + act(() => { + result.current.setCurrentUserState({ + analyticsTinybirdJwt: 'jwt', + } as CurrentUser); + }); + + expect(result.current.useAnalyticsTinybirdJwt).toBe('jwt'); + + act(() => { + result.current.setCurrentUserState(null); + }); + + expect(result.current.useAnalyticsTinybirdJwt).toBeUndefined(); + + act(() => { + result.current.setCurrentUserState({} as CurrentUser); + }); + + expect(result.current.useAnalyticsTinybirdJwt).toBeUndefined(); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt.ts b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt.ts new file mode 100644 index 000000000000..67fbf1760268 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt.ts @@ -0,0 +1,18 @@ +import { useRecoilValue } from 'recoil'; + +import { currentUserState } from '@/auth/states/currentUserState'; +import { isNull } from '@sniptt/guards'; + +export const useAnalyticsTinybirdJwt = (): string | undefined => { + const currentUser = useRecoilValue(currentUserState); + + if (!currentUser) { + return undefined; + } + + if (isNull(currentUser.analyticsTinybirdJwt)) { + return undefined; + } + + return currentUser.analyticsTinybirdJwt; +}; diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx index 62fc6d4ad952..d141d82734b5 100644 --- a/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx +++ b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx @@ -1,16 +1,24 @@ +import { useAnalyticsTinybirdJwt } from '@/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt'; import { fetchGraphDataOrThrow } from '@/settings/developers/webhook/utils/fetchGraphDataOrThrow'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { isUndefined } from '@sniptt/guards'; export const useGraphData = (webhookId: string) => { const { enqueueSnackBar } = useSnackBar(); + const analyticsTinybirdJwt = useAnalyticsTinybirdJwt(); const fetchGraphData = async ( windowLengthGraphOption: '7D' | '1D' | '12H' | '4H', ) => { try { + if (isUndefined(analyticsTinybirdJwt)) { + throw new Error('No analyticsTinybirdJwt found'); + } + return await fetchGraphDataOrThrow({ webhookId, windowLength: windowLengthGraphOption, + tinybirdJwt: analyticsTinybirdJwt, }); } catch (error) { enqueueSnackBar('Something went wrong while fetching webhook usage', { diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts b/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts index b7123f712579..4b66c4b9b0d4 100644 --- a/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts +++ b/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts @@ -4,22 +4,24 @@ import { WEBHOOK_GRAPH_API_OPTIONS_MAP } from '@/settings/developers/webhook/con type fetchGraphDataOrThrowProps = { webhookId: string; windowLength: '7D' | '1D' | '12H' | '4H'; + tinybirdJwt: string; }; export const fetchGraphDataOrThrow = async ({ webhookId, windowLength, + tinybirdJwt, }: fetchGraphDataOrThrowProps) => { const queryString = new URLSearchParams({ ...WEBHOOK_GRAPH_API_OPTIONS_MAP[windowLength], webhookIdRequest: webhookId, }).toString(); - const token = 'REPLACE_ME'; + const response = await fetch( `https://api.eu-central-1.aws.tinybird.co/v0/pipes/getWebhooksAnalyticsV2.json?${queryString}`, { headers: { - Authorization: 'Bearer ' + token, + Authorization: 'Bearer ' + tinybirdJwt, }, }, ); diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts index 76e5400c34e9..8cdb26be8a17 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -8,6 +8,7 @@ export const USER_QUERY_FRAGMENT = gql` email canImpersonate supportUserHash + analyticsTinybirdJwt onboardingStatus workspaceMember { ...WorkspaceMemberQueryFragment diff --git a/packages/twenty-server/src/engine/core-modules/analytics/analytics.module.ts b/packages/twenty-server/src/engine/core-modules/analytics/analytics.module.ts index 2b4c8705d62d..3897a136e5d2 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/analytics.module.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/analytics.module.ts @@ -1,6 +1,8 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; +import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module'; + import { AnalyticsResolver } from './analytics.resolver'; import { AnalyticsService } from './analytics.service'; @@ -9,6 +11,7 @@ const TINYBIRD_BASE_URL = 'https://api.eu-central-1.aws.tinybird.co/v0'; @Module({ providers: [AnalyticsResolver, AnalyticsService], imports: [ + JwtModule, HttpModule.register({ baseURL: TINYBIRD_BASE_URL, }), diff --git a/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.spec.ts b/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.spec.ts index ba23dc9a2c22..3d700198e590 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.spec.ts @@ -1,7 +1,4 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { HttpService } from '@nestjs/axios'; - -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { AnalyticsResolver } from './analytics.resolver'; import { AnalyticsService } from './analytics.service'; @@ -13,13 +10,8 @@ describe('AnalyticsResolver', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ AnalyticsResolver, - AnalyticsService, - { - provide: EnvironmentService, - useValue: {}, - }, { - provide: HttpService, + provide: AnalyticsService, useValue: {}, }, ], diff --git a/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.ts b/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.ts index 71e76f8bd80a..9c6cd30900b2 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.ts @@ -1,6 +1,5 @@ import { Args, Mutation, Resolver } from '@nestjs/graphql'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; @@ -13,10 +12,7 @@ import { CreateAnalyticsInput } from './dtos/create-analytics.input'; @Resolver(() => Analytics) export class AnalyticsResolver { - constructor( - private readonly analyticsService: AnalyticsService, - private readonly environmentService: EnvironmentService, - ) {} + constructor(private readonly analyticsService: AnalyticsService) {} @Mutation(() => Analytics) track( diff --git a/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.spec.ts b/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.spec.ts index a22ede293be4..36feaed4d461 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.spec.ts @@ -1,7 +1,8 @@ -import { Test, TestingModule } from '@nestjs/testing'; import { HttpService } from '@nestjs/axios'; +import { Test, TestingModule } from '@nestjs/testing'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; import { AnalyticsService } from './analytics.service'; @@ -16,6 +17,10 @@ describe('AnalyticsService', () => { provide: EnvironmentService, useValue: {}, }, + { + provide: JwtWrapperService, + useValue: {}, + }, { provide: HttpService, useValue: {}, diff --git a/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts b/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts index 2b2eeb3d683e..49980b54902b 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts @@ -4,6 +4,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { AxiosRequestConfig } from 'axios'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; type CreateEventInput = { action: string; @@ -16,6 +17,7 @@ export class AnalyticsService { private readonly defaultDatasource = 'event'; constructor( + private readonly jwtWrapperService: JwtWrapperService, private readonly environmentService: EnvironmentService, private readonly httpService: HttpService, ) {} @@ -58,7 +60,7 @@ export class AnalyticsService { const config: AxiosRequestConfig = { headers: { Authorization: - 'Bearer ' + this.environmentService.get('TINYBIRD_TOKEN'), + 'Bearer ' + this.environmentService.get('TINYBIRD_INGEST_TOKEN'), }, }; @@ -86,4 +88,25 @@ export class AnalyticsService { return { success: true }; } + + async generateWorkspaceJwt(workspaceId: string | undefined) { + const pipeId = 't_b49e0fe60f9e438eae81cb31c5260df2'; // refactor this pass as params + //perhaps a constant of name:pipeId??? better typing in this func^ + const payload = { + name: 'my_demo_jwt', + workspace_id: this.environmentService.get('TINYBIRD_WORKSPACE_UUID'), + scopes: [ + { + type: 'PIPES:READ', + resource: pipeId, + fixed_params: { workspaceId: workspaceId }, + }, + ], + }; + + return this.jwtWrapperService.sign(payload, { + secret: this.environmentService.get('TINYBIRD_GENERATE_JWT_TOKEN'), + expiresIn: '7d', + }); + } } diff --git a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts index faab8c9d6497..03b0d234e1fe 100644 --- a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts @@ -95,7 +95,15 @@ export class EnvironmentVariables { @IsString() @ValidateIf((env) => env.ANALYTICS_ENABLED) - TINYBIRD_TOKEN: string; + TINYBIRD_INGEST_TOKEN: string; + + @IsString() + @ValidateIf((env) => env.ANALYTICS_ENABLED) + TINYBIRD_WORKSPACE_UUID: string; + + @IsString() + @ValidateIf((env) => env.ANALYTICS_ENABLED) + TINYBIRD_GENERATE_JWT_TOKEN: string; @CastToPositiveNumber() @IsNumber() diff --git a/packages/twenty-server/src/engine/core-modules/user/user.module.ts b/packages/twenty-server/src/engine/core-modules/user/user.module.ts index 7a776cca2508..57ea2140cec2 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.module.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.module.ts @@ -7,6 +7,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity'; @@ -37,6 +38,7 @@ import { UserService } from './services/user.service'; OnboardingModule, TypeOrmModule.forFeature([KeyValuePair], 'core'), UserVarsModule, + AnalyticsModule, ], exports: [UserService], providers: [UserService, UserResolver, TypeORMService], diff --git a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts index d4304622af76..86f27d821b4e 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts @@ -19,6 +19,7 @@ import { Repository } from 'typeorm'; import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface'; import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; +import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; @@ -55,6 +56,7 @@ export class UserResolver { private readonly onboardingService: OnboardingService, private readonly userVarService: UserVarsService, private readonly fileService: FileService, + private readonly analyticsService: AnalyticsService, ) {} @Query(() => User) @@ -154,6 +156,15 @@ export class UserResolver { return getHMACKey(parent.email, key); } + @ResolveField(() => String, { + nullable: true, + }) + async analyticsTinybirdJwt( + @AuthWorkspace() workspace: Workspace | undefined, + ): Promise { + return await this.analyticsService.generateWorkspaceJwt(workspace?.id); + } + @Mutation(() => String) async uploadProfilePicture( @AuthUser() { id }: User, From 0e9d2708d4b62e983acd72efe58bd6c5ecd54356 Mon Sep 17 00:00:00 2001 From: sateshcharan Date: Mon, 21 Oct 2024 21:38:36 +0530 Subject: [PATCH 07/13] [oss.gg] write-selfthost-guide-blog-post (750 points) (#7924) ![image](https://github.com/user-attachments/assets/c1817eb5-8ffa-466e-8385-1644decf48a0) --- .../3-write-selfthost-guide-blog-post-20.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md b/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md index c7352ec430fc..57308fb4d373 100644 --- a/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md +++ b/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md @@ -18,4 +18,6 @@ Your turn 👇 » 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) blog Link: [blog](https://twenty.com/) ---- \ No newline at end of file +» 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) blog Link: [blog](https://dev.to/sateshcharan/streamlined-self-hosting-with-twenty-crm-1-click-docker-compose-setup-188o) + +--- From 3f2751ef6c6ca904ac5befa9bfadd2e3057e922e Mon Sep 17 00:00:00 2001 From: sateshcharan Date: Mon, 21 Oct 2024 21:40:18 +0530 Subject: [PATCH 08/13] [oss.gg] write-blog-post-about-20 (750 points) (#7922) ![image](https://github.com/user-attachments/assets/3de0b8a6-1c2e-40ae-a82b-ad6d2c21f84c) --- oss-gg/twenty-content-challenges/2-write-blog-post-about-20.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oss-gg/twenty-content-challenges/2-write-blog-post-about-20.md b/oss-gg/twenty-content-challenges/2-write-blog-post-about-20.md index ca36278ff3f6..c53fe4babefc 100644 --- a/oss-gg/twenty-content-challenges/2-write-blog-post-about-20.md +++ b/oss-gg/twenty-content-challenges/2-write-blog-post-about-20.md @@ -20,4 +20,6 @@ Your turn 👇 » 19-October-2024 by [Thefool76](https://oss.gg/thefool76) blog Link: [blog](https://k5lo7h.hashnode.dev/twenty-crm-a-fresh-start-for-modern-businesses) +» 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) blog Link: [blog](https://dev.to/sateshcharan/twenty-crm-a-fresh-start-for-modern-businesses-46kf) + --- From 7b10bfa7d240e6afa81a33de92646480ae50e87f Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:11:02 +0200 Subject: [PATCH 09/13] Add filter on array and jsonb field types (#7839) This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-6784](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-6784). This ticket was imported from: [TWNTY-6784](https://github.com/twentyhq/twenty/issues/6784) --- ### Description - Add filter on array and jsonb field types - We did not implement the contains any filter for arrays on the frontend because we would need to change the UI design since this should be an array of values, and now we have only one input ### Demo Fixes #6784 --------- Co-authored-by: gitstart-twenty Co-authored-by: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com> Co-authored-by: Weiko --- ...ColumnDefinitionsFromFieldMetadata.test.ts | 20 ++++++ .../useColumnDefinitionsFromFieldMetadata.ts | 6 ++ ...atFieldMetadataItemsAsFilterDefinitions.ts | 12 +++- .../graphql/types/RecordGqlOperationFilter.ts | 13 ++++ .../ObjectFilterDropdownFilterInput.tsx | 1 + .../types/FilterableFieldType.ts | 1 + .../utils/getOperandsForFilterType.ts | 13 +++- .../record-filter/utils/applyEmptyFilters.ts | 20 ++++++ .../utils/isMatchingArrayFilter.ts | 34 +++++++++ .../utils/isMatchingRawJsonFilter.ts | 32 +++++++++ .../utils/isRecordMatchingFilter.ts | 17 +++++ .../utils/turnFiltersIntoQueryFilter.ts | 69 +++++++++++++++++++ ...blesFromActiveFieldsOfViewOrDefaultView.ts | 6 ++ .../views/utils/getQueryVariablesFromView.ts | 3 + .../modules/workspace/types/FeatureFlagKey.ts | 3 +- .../graphql-query-filter-field.parser.ts | 17 +++-- .../utils/compute-where-condition-parts.ts | 20 ++++-- .../input/array-filter.input-type.ts | 4 +- .../input/raw-json-filter.input-type.ts | 3 +- 19 files changed, 277 insertions(+), 17 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingArrayFilter.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingRawJsonFilter.ts diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts index 05c87497dcc6..62846c8fbab5 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts @@ -1,16 +1,35 @@ import { renderHook } from '@testing-library/react'; import { Nullable } from 'twenty-ui'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { WorkspaceActivationStatus } from '~/generated/graphql'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], + onInitializeRecoilSnapshot: ({ set }) => { + set(currentWorkspaceState, { + id: '1', + featureFlags: [], + allowImpersonation: false, + activationStatus: WorkspaceActivationStatus.Active, + metadataVersion: 1, + }); + }, +}); + describe('useColumnDefinitionsFromFieldMetadata', () => { it('should return empty definitions if no object is passed', () => { const { result } = renderHook( (objectMetadataItem?: Nullable) => { return useColumnDefinitionsFromFieldMetadata(objectMetadataItem); }, + { + wrapper: Wrapper, + }, ); expect(Array.isArray(result.current.columnDefinitions)).toBe(true); @@ -32,6 +51,7 @@ describe('useColumnDefinitionsFromFieldMetadata', () => { }, { initialProps: companyObjectMetadata, + wrapper: Wrapper, }, ); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts index b7764673e991..d7155f3d7166 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata.ts @@ -6,6 +6,7 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata' import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { formatFieldMetadataItemAsColumnDefinition } from '../utils/formatFieldMetadataItemAsColumnDefinition'; import { formatFieldMetadataItemsAsFilterDefinitions } from '../utils/formatFieldMetadataItemsAsFilterDefinitions'; import { formatFieldMetadataItemsAsSortDefinitions } from '../utils/formatFieldMetadataItemsAsSortDefinitions'; @@ -23,8 +24,13 @@ export const useColumnDefinitionsFromFieldMetadata = ( [objectMetadataItem], ); + const isArrayAndJsonFilterEnabled = useIsFeatureEnabled( + 'IS_ARRAY_AND_JSON_FILTER_ENABLED', + ); + const filterDefinitions = formatFieldMetadataItemsAsFilterDefinitions({ fields: activeFieldMetadataItems, + isArrayAndJsonFilterEnabled, }); const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({ diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts index a110acdceba4..42734cb92f6f 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts @@ -8,10 +8,12 @@ import { ObjectMetadataItem } from '../types/ObjectMetadataItem'; export const formatFieldMetadataItemsAsFilterDefinitions = ({ fields, + isArrayAndJsonFilterEnabled, }: { fields: Array; -}): FilterDefinition[] => - fields.reduce((acc, field) => { + isArrayAndJsonFilterEnabled: boolean; +}): FilterDefinition[] => { + return fields.reduce((acc, field) => { if ( field.type === FieldMetadataType.Relation && field.relationDefinition?.direction !== @@ -37,6 +39,9 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({ FieldMetadataType.Rating, FieldMetadataType.Actor, FieldMetadataType.Phones, + ...(isArrayAndJsonFilterEnabled + ? [FieldMetadataType.Array, FieldMetadataType.RawJson] + : []), ].includes(field.type) ) { return acc; @@ -44,6 +49,7 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({ return [...acc, formatFieldMetadataItemAsFilterDefinition({ field })]; }, [] as FilterDefinition[]); +}; export const formatFieldMetadataItemAsFilterDefinition = ({ field, @@ -92,6 +98,8 @@ export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => { return 'ACTOR'; case FieldMetadataType.Array: return 'ARRAY'; + case FieldMetadataType.RawJson: + return 'RAW_JSON'; default: return 'TEXT'; } diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts index 6c1615b17afa..9393d98b841a 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts @@ -104,6 +104,17 @@ export type PhonesFilter = { primaryPhoneCountryCode?: StringFilter; }; +export type ArrayFilter = { + contains?: string[]; + not_contains?: string[]; + is?: IsFilter; +}; + +export type RawJsonFilter = { + like?: string; + is?: IsFilter; +}; + export type LeafFilter = | UUIDFilter | StringFilter @@ -117,6 +128,8 @@ export type LeafFilter = | LinksFilter | ActorFilter | PhonesFilter + | ArrayFilter + | RawJsonFilter | undefined; export type AndObjectRecordFilter = { diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx index a630286ffadb..35f20b4a52fd 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx @@ -93,6 +93,7 @@ export const ObjectFilterDropdownFilterInput = ({ 'ADDRESS', 'ACTOR', 'ARRAY', + 'RAW_JSON', 'PHONES', ].includes(filterDefinitionUsedInDropdown.type) && !isActorSourceCompositeFilter(filterDefinitionUsedInDropdown) && ( diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterableFieldType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterableFieldType.ts index 0624fe937ef7..b2bf87102729 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterableFieldType.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterableFieldType.ts @@ -19,4 +19,5 @@ export type FilterableFieldType = PickLiteral< | 'MULTI_SELECT' | 'ACTOR' | 'ARRAY' + | 'RAW_JSON' >; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts index 688aa02b6c79..3634a7baefd5 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts @@ -18,7 +18,6 @@ export const getOperandsForFilterDefinition = ( case 'FULL_NAME': case 'ADDRESS': case 'LINKS': - case 'ARRAY': case 'PHONES': return [ ViewFilterOperand.Contains, @@ -32,6 +31,12 @@ export const getOperandsForFilterDefinition = ( ViewFilterOperand.LessThan, ...emptyOperands, ]; + case 'RAW_JSON': + return [ + ViewFilterOperand.Contains, + ViewFilterOperand.DoesNotContain, + ...emptyOperands, + ]; case 'DATE_TIME': case 'DATE': return [ @@ -70,6 +75,12 @@ export const getOperandsForFilterDefinition = ( ...emptyOperands, ]; } + case 'ARRAY': + return [ + ViewFilterOperand.Contains, + ViewFilterOperand.DoesNotContain, + ...emptyOperands, + ]; default: return []; } diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/applyEmptyFilters.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/applyEmptyFilters.ts index e004288ceba0..03ea135cebd0 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/applyEmptyFilters.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/applyEmptyFilters.ts @@ -1,10 +1,12 @@ import { ActorFilter, AddressFilter, + ArrayFilter, CurrencyFilter, DateFilter, EmailsFilter, FloatFilter, + RawJsonFilter, RecordGqlOperationFilter, RelationFilter, StringFilter, @@ -290,6 +292,24 @@ export const applyEmptyFilters = ( ], }; break; + case 'ARRAY': + emptyRecordFilter = { + or: [ + { + [correspondingField.name]: { is: 'NULL' } as ArrayFilter, + }, + ], + }; + break; + case 'RAW_JSON': + emptyRecordFilter = { + or: [ + { + [correspondingField.name]: { is: 'NULL' } as RawJsonFilter, + }, + ], + }; + break; case 'EMAILS': emptyRecordFilter = { or: [ diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingArrayFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingArrayFilter.ts new file mode 100644 index 000000000000..7578a04aac49 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingArrayFilter.ts @@ -0,0 +1,34 @@ +import { ArrayFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; + +export const isMatchingArrayFilter = ({ + arrayFilter, + value, +}: { + arrayFilter: ArrayFilter; + value: string[]; +}) => { + if (value === null || !Array.isArray(value)) { + return false; + } + + switch (true) { + case arrayFilter.contains !== undefined: { + return arrayFilter.contains.every((item) => value.includes(item)); + } + case arrayFilter.not_contains !== undefined: { + return !arrayFilter.not_contains.some((item) => value.includes(item)); + } + case arrayFilter.is !== undefined: { + if (arrayFilter.is === 'NULL') { + return value === null; + } else { + return value !== null; + } + } + default: { + throw new Error( + `Unexpected value for array filter: ${JSON.stringify(arrayFilter)}`, + ); + } + } +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingRawJsonFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingRawJsonFilter.ts new file mode 100644 index 000000000000..8251bca722a5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingRawJsonFilter.ts @@ -0,0 +1,32 @@ +import { RawJsonFilter } from '../../graphql/types/RecordGqlOperationFilter'; + +export const isMatchingRawJsonFilter = ({ + rawJsonFilter, + value, +}: { + rawJsonFilter: RawJsonFilter; + value: string; +}) => { + switch (true) { + case rawJsonFilter.like !== undefined: { + const regexPattern = rawJsonFilter.like.replace(/%/g, '.*'); + const regexCaseInsensitive = new RegExp(`^${regexPattern}$`, 'i'); + + const stringValue = JSON.stringify(value); + + return regexCaseInsensitive.test(stringValue); + } + case rawJsonFilter.is !== undefined: { + if (rawJsonFilter.is === 'NULL') { + return value === null; + } else { + return value !== null; + } + } + default: { + throw new Error( + `Unexpected value for string filter : ${JSON.stringify(rawJsonFilter)}`, + ); + } + } +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts index 929956d77128..c2a5da47fdd8 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts @@ -5,6 +5,7 @@ import { ActorFilter, AddressFilter, AndObjectRecordFilter, + ArrayFilter, BooleanFilter, CurrencyFilter, DateFilter, @@ -16,14 +17,17 @@ import { NotObjectRecordFilter, OrObjectRecordFilter, PhonesFilter, + RawJsonFilter, RecordGqlOperationFilter, StringFilter, UUIDFilter, } from '@/object-record/graphql/types/RecordGqlOperationFilter'; +import { isMatchingArrayFilter } from '@/object-record/record-filter/utils/isMatchingArrayFilter'; import { isMatchingBooleanFilter } from '@/object-record/record-filter/utils/isMatchingBooleanFilter'; import { isMatchingCurrencyFilter } from '@/object-record/record-filter/utils/isMatchingCurrencyFilter'; import { isMatchingDateFilter } from '@/object-record/record-filter/utils/isMatchingDateFilter'; import { isMatchingFloatFilter } from '@/object-record/record-filter/utils/isMatchingFloatFilter'; +import { isMatchingRawJsonFilter } from '@/object-record/record-filter/utils/isMatchingRawJsonFilter'; import { isMatchingStringFilter } from '@/object-record/record-filter/utils/isMatchingStringFilter'; import { isMatchingUUIDFilter } from '@/object-record/record-filter/utils/isMatchingUUIDFilter'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -165,6 +169,18 @@ export const isRecordMatchingFilter = ({ value: record[filterKey], }); } + case FieldMetadataType.Array: { + return isMatchingArrayFilter({ + arrayFilter: filterValue as ArrayFilter, + value: record[filterKey], + }); + } + case FieldMetadataType.RawJson: { + return isMatchingRawJsonFilter({ + rawJsonFilter: filterValue as RawJsonFilter, + value: record[filterKey], + }); + } case FieldMetadataType.FullName: { const fullNameFilter = filterValue as FullNameFilter; @@ -302,6 +318,7 @@ export const isRecordMatchingFilter = ({ `Not implemented yet, use UUID filter instead on the corredponding "${filterKey}Id" field`, ); } + default: { throw new Error( `Not implemented yet for field type "${objectMetadataField.type}"`, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnFiltersIntoQueryFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnFiltersIntoQueryFilter.ts index 0e3c69d7c0b8..9829d957e498 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnFiltersIntoQueryFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnFiltersIntoQueryFilter.ts @@ -3,10 +3,12 @@ import { isNonEmptyString } from '@sniptt/guards'; import { ActorFilter, AddressFilter, + ArrayFilter, CurrencyFilter, DateFilter, EmailsFilter, FloatFilter, + RawJsonFilter, RecordGqlOperationFilter, RelationFilter, StringFilter, @@ -98,6 +100,39 @@ export const turnFiltersIntoQueryFilter = ( ); } break; + case 'RAW_JSON': + switch (rawUIFilter.operand) { + case ViewFilterOperand.Contains: + objectRecordFilters.push({ + [correspondingField.name]: { + like: `%${rawUIFilter.value}%`, + } as RawJsonFilter, + }); + break; + case ViewFilterOperand.DoesNotContain: + objectRecordFilters.push({ + not: { + [correspondingField.name]: { + like: `%${rawUIFilter.value}%`, + } as RawJsonFilter, + }, + }); + break; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + applyEmptyFilters( + rawUIFilter.operand, + correspondingField, + objectRecordFilters, + rawUIFilter.definition, + ); + break; + default: + throw new Error( + `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, + ); + } + break; case 'DATE': case 'DATE_TIME': { const resolvedFilterValue = resolveFilterValue(rawUIFilter); @@ -835,6 +870,40 @@ export const turnFiltersIntoQueryFilter = ( } break; } + case 'ARRAY': { + switch (rawUIFilter.operand) { + case ViewFilterOperand.Contains: { + objectRecordFilters.push({ + [correspondingField.name]: { + contains: [`${rawUIFilter.value}`], + } as ArrayFilter, + }); + break; + } + case ViewFilterOperand.DoesNotContain: { + objectRecordFilters.push({ + [correspondingField.name]: { + not_contains: [`${rawUIFilter.value}`], + } as ArrayFilter, + }); + break; + } + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + applyEmptyFilters( + rawUIFilter.operand, + correspondingField, + objectRecordFilters, + rawUIFilter.definition, + ); + break; + default: + throw new Error( + `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.label} filter`, + ); + } + break; + } default: throw new Error('Unknown filter type'); } diff --git a/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView.ts b/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView.ts index f5431bc45a58..d40afd1064a9 100644 --- a/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView.ts @@ -2,6 +2,7 @@ import { useActiveFieldMetadataItems } from '@/object-metadata/hooks/useActiveFi import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useViewOrDefaultViewFromPrefetchedViews } from '@/views/hooks/useViewOrDefaultViewFromPrefetchedViews'; import { getQueryVariablesFromView } from '@/views/utils/getQueryVariablesFromView'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; export const useQueryVariablesFromActiveFieldsOfViewOrDefaultView = ({ objectMetadataItem, @@ -19,10 +20,15 @@ export const useQueryVariablesFromActiveFieldsOfViewOrDefaultView = ({ objectMetadataItem, }); + const isArrayAndJsonFilterEnabled = useIsFeatureEnabled( + 'IS_ARRAY_AND_JSON_FILTER_ENABLED', + ); + const { filter, orderBy } = getQueryVariablesFromView({ fieldMetadataItems: activeFieldMetadataItems, objectMetadataItem, view, + isArrayAndJsonFilterEnabled, }); return { diff --git a/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts b/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts index 8206f52b3a95..fc685af7efff 100644 --- a/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts +++ b/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts @@ -13,10 +13,12 @@ export const getQueryVariablesFromView = ({ view, fieldMetadataItems, objectMetadataItem, + isArrayAndJsonFilterEnabled, }: { view: View | null | undefined; fieldMetadataItems: FieldMetadataItem[]; objectMetadataItem: ObjectMetadataItem; + isArrayAndJsonFilterEnabled: boolean; }) => { if (!isDefined(view)) { return { @@ -29,6 +31,7 @@ export const getQueryVariablesFromView = ({ const filterDefinitions = formatFieldMetadataItemsAsFilterDefinitions({ fields: fieldMetadataItems, + isArrayAndJsonFilterEnabled, }); const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({ diff --git a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts index 5471c5d4d59e..f0346d505548 100644 --- a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts +++ b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts @@ -13,4 +13,5 @@ export type FeatureFlagKey = | 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED' | 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED' | 'IS_ANALYTICS_V2_ENABLED' - | 'IS_UNIQUE_INDEXES_ENABLED'; + | 'IS_UNIQUE_INDEXES_ENABLED' + | 'IS_ARRAY_AND_JSON_FILTER_ENABLED'; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts index 9b1d1020b7bd..5d35ebf5ecba 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts @@ -13,6 +13,8 @@ import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-obj import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { capitalize } from 'src/utils/capitalize'; +const ARRAY_OPERATORS = ['in', 'contains', 'not_contains']; + export class GraphqlQueryFilterFieldParser { private fieldMetadataMap: FieldMetadataMap; @@ -44,13 +46,14 @@ export class GraphqlQueryFilterFieldParser { } const [[operator, value]] = Object.entries(filterValue); - if (operator === 'in') { - if (!Array.isArray(value) || value.length === 0) { - throw new GraphqlQueryRunnerException( - `Invalid filter value for field ${key}. Expected non-empty array`, - GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } + if ( + ARRAY_OPERATORS.includes(operator) && + (!Array.isArray(value) || value.length === 0) + ) { + throw new GraphqlQueryRunnerException( + `Invalid filter value for field ${key}. Expected non-empty array`, + GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); } const { sql, params } = computeWhereConditionParts( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts index ef8d4680ebb3..aae3f9b0015c 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts.ts @@ -61,24 +61,36 @@ export const computeWhereConditionParts = ( }; case 'like': return { - sql: `"${objectNameSingular}"."${key}" LIKE :${key}${uuid}`, + sql: `"${objectNameSingular}"."${key}"::text LIKE :${key}${uuid}`, params: { [`${key}${uuid}`]: `${value}` }, }; case 'ilike': return { - sql: `"${objectNameSingular}"."${key}" ILIKE :${key}${uuid}`, + sql: `"${objectNameSingular}"."${key}"::text ILIKE :${key}${uuid}`, params: { [`${key}${uuid}`]: `${value}` }, }; case 'startsWith': return { - sql: `"${objectNameSingular}"."${key}" LIKE :${key}${uuid}`, + sql: `"${objectNameSingular}"."${key}"::text LIKE :${key}${uuid}`, params: { [`${key}${uuid}`]: `${value}` }, }; case 'endsWith': return { - sql: `"${objectNameSingular}"."${key}" LIKE :${key}${uuid}`, + sql: `"${objectNameSingular}"."${key}"::text LIKE :${key}${uuid}`, params: { [`${key}${uuid}`]: `${value}` }, }; + case 'contains': + return { + sql: `"${objectNameSingular}"."${key}" @> ARRAY[:...${key}${uuid}]`, + params: { [`${key}${uuid}`]: value }, + }; + + case 'not_contains': + return { + sql: `NOT ("${objectNameSingular}"."${key}" && ARRAY[:...${key}${uuid}])`, + params: { [`${key}${uuid}`]: value }, + }; + default: throw new GraphqlQueryRunnerException( `Operator "${operator}" is not supported`, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/array-filter.input-type.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/array-filter.input-type.ts index 37b3ba8293fb..3cd24cbbaac7 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/array-filter.input-type.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/array-filter.input-type.ts @@ -1,10 +1,12 @@ import { GraphQLInputObjectType, GraphQLList, GraphQLString } from 'graphql'; +import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type'; + export const ArrayFilterType = new GraphQLInputObjectType({ name: 'ArrayFilter', fields: { contains: { type: new GraphQLList(GraphQLString) }, - contains_any: { type: new GraphQLList(GraphQLString) }, not_contains: { type: new GraphQLList(GraphQLString) }, + is: { type: FilterIs }, }, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/raw-json-filter.input-type.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/raw-json-filter.input-type.ts index 5b06437dd695..75f40b7e5916 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/raw-json-filter.input-type.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-types/input/raw-json-filter.input-type.ts @@ -1,4 +1,4 @@ -import { GraphQLInputObjectType } from 'graphql'; +import { GraphQLInputObjectType, GraphQLString } from 'graphql'; import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type'; @@ -6,5 +6,6 @@ export const RawJsonFilterType = new GraphQLInputObjectType({ name: 'RawJsonFilter', fields: { is: { type: FilterIs }, + like: { type: GraphQLString }, }, }); From 1a0b387282aaebfb5bf3bdee1ae6c8e315a0f684 Mon Sep 17 00:00:00 2001 From: DivyanshuLohani <59534359+DivyanshuLohani@users.noreply.github.com> Date: Mon, 21 Oct 2024 21:51:08 +0530 Subject: [PATCH 10/13] Add the role=button to search (#7594) In this PR: - Let the `` component render a `