From 27a3d7ec2784d0ffe99e0a24d8a859215a8ea837 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Thu, 2 May 2024 11:00:24 +0200 Subject: [PATCH 01/32] Bump to 0.10.5 --- packages/twenty-docs/package.json | 2 +- packages/twenty-emails/package.json | 2 +- packages/twenty-front/package.json | 2 +- packages/twenty-server/package.json | 2 +- packages/twenty-ui/package.json | 2 +- packages/twenty-website/package.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/twenty-docs/package.json b/packages/twenty-docs/package.json index d11badb82550..a619ef1acf4d 100644 --- a/packages/twenty-docs/package.json +++ b/packages/twenty-docs/package.json @@ -1,6 +1,6 @@ { "name": "twenty-docs", - "version": "0.10.4", + "version": "0.10.5", "private": true, "scripts": { "nx": "NX_DEFAULT_PROJECT=twenty-docs node ../../node_modules/nx/bin/nx.js", diff --git a/packages/twenty-emails/package.json b/packages/twenty-emails/package.json index 2de43474925d..cf9786413e9d 100644 --- a/packages/twenty-emails/package.json +++ b/packages/twenty-emails/package.json @@ -1,6 +1,6 @@ { "name": "twenty-emails", - "version": "0.10.4", + "version": "0.10.5", "description": "", "author": "", "private": true, diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index a8ebd5fc93bc..499d096f09e3 100644 --- a/packages/twenty-front/package.json +++ b/packages/twenty-front/package.json @@ -1,6 +1,6 @@ { "name": "twenty-front", - "version": "0.10.4", + "version": "0.10.5", "private": true, "type": "module", "scripts": { diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 5319806a1bce..2b84699dd342 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -1,6 +1,6 @@ { "name": "twenty-server", - "version": "0.10.4", + "version": "0.10.5", "description": "", "author": "", "private": true, diff --git a/packages/twenty-ui/package.json b/packages/twenty-ui/package.json index e2e4ebd6399b..7a2fdfe41566 100644 --- a/packages/twenty-ui/package.json +++ b/packages/twenty-ui/package.json @@ -1,6 +1,6 @@ { "name": "twenty-ui", - "version": "0.10.4", + "version": "0.10.5", "type": "module", "main": "./src/index.ts", "exports": { diff --git a/packages/twenty-website/package.json b/packages/twenty-website/package.json index 0bc230154644..12cf976e48ea 100644 --- a/packages/twenty-website/package.json +++ b/packages/twenty-website/package.json @@ -1,6 +1,6 @@ { "name": "twenty-website", - "version": "0.10.4", + "version": "0.10.5", "private": true, "scripts": { "nx": "NX_DEFAULT_PROJECT=twenty-website node ../../node_modules/nx/bin/nx.js", From 9a116b08a4392aad687de7c4b5b48c0ae1a9219f Mon Sep 17 00:00:00 2001 From: Weiko Date: Thu, 2 May 2024 12:54:01 +0200 Subject: [PATCH 02/32] User workspace middleware throws 401 if token is invalid (#5245) ## Context Currently, this middleware validates the token and stores the user, workspace and cacheversion in the request object. It only does so when a token is provided and ignores the middleware logic if not. If the token is invalid or expired, the exception is swallowed. This PR removes the try/catch and adds an allowlist to skip the token validation for operations executed while not signed-in. I don't know a better way to do that with Nestjs. We can't easily add the middleware per resolver without refactoring the flexible schema engine so I'm doing it the other way around. Fixes https://github.com/twentyhq/twenty/issues/5224 --- .../middlewares/user-workspace.middleware.ts | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/packages/twenty-server/src/engine/middlewares/user-workspace.middleware.ts b/packages/twenty-server/src/engine/middlewares/user-workspace.middleware.ts index a1ebe7a9298d..a45c6f6fce56 100644 --- a/packages/twenty-server/src/engine/middlewares/user-workspace.middleware.ts +++ b/packages/twenty-server/src/engine/middlewares/user-workspace.middleware.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; +import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; @@ -7,28 +7,42 @@ import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/worksp @Injectable() export class UserWorkspaceMiddleware implements NestMiddleware { - private readonly logger = new Logger(UserWorkspaceMiddleware.name); - constructor( private readonly tokenService: TokenService, private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, ) {} async use(req: Request, res: Response, next: NextFunction) { - if (this.tokenService.isTokenPresent(req)) { - try { - const data = await this.tokenService.validateToken(req); - const cacheVersion = await this.workspaceCacheVersionService.getVersion( - data.workspace.id, - ); - - req.user = data.user; - req.workspace = data.workspace; - req.cacheVersion = cacheVersion; - } catch (error) { - this.logger.error('Error while validating token in middleware.', error); - } + const body = req.body; + const excludedOperations = [ + 'GetClientConfig', + 'GetCurrentUser', + 'GetWorkspaceFromInviteHash', + 'Track', + 'CheckUserExists', + 'Challenge', + 'Verify', + 'SignUp', + 'RenewToken', + ]; + + if ( + body && + body.operationName && + excludedOperations.includes(body.operationName) + ) { + return next(); } + + const data = await this.tokenService.validateToken(req); + const cacheVersion = await this.workspaceCacheVersionService.getVersion( + data.workspace.id, + ); + + req.user = data.user; + req.workspace = data.workspace; + req.cacheVersion = cacheVersion; + next(); } } From 05a90d6153ad642a6b776bdde973322150b331f9 Mon Sep 17 00:00:00 2001 From: brendanlaschke Date: Thu, 2 May 2024 14:21:19 +0200 Subject: [PATCH 03/32] Constant api version (#5248) closes #5206 --- .../engine/core-modules/open-api/utils/base-schema.utils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/base-schema.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/base-schema.utils.ts index d506d6fb6cf0..059f5ae291c3 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/base-schema.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/base-schema.utils.ts @@ -2,6 +2,8 @@ import { OpenAPIV3_1 } from 'openapi-types'; import { computeOpenApiPath } from 'src/engine/core-modules/open-api/utils/path.utils'; +export const API_Version = 'v0.1'; + export const baseSchema = ( schemaName: 'core' | 'metadata', serverUrl: string, @@ -19,7 +21,7 @@ export const baseSchema = ( name: 'AGPL-3.0', url: 'https://github.com/twentyhq/twenty?tab=AGPL-3.0-1-ov-file#readme', }, - version: '0.2.0', + version: API_Version, }, // Testing purposes servers: [ From 1da64c771501ca804673648ced17b88cd162d78f Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Thu, 2 May 2024 15:25:54 +0200 Subject: [PATCH 04/32] [feat] Minor updates to the edit db connection page (#5250) - Add placeholders in db connection edit page - Fix icon alignement and size (should not change) in Info banner --- ...tingsIntegrationDatabaseConnectionForm.tsx | 35 +++++++++++-------- ...tegrationEditDatabaseConnectionContent.tsx | 1 - .../ui/display/info/components/Info.tsx | 7 +++- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionForm.tsx b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionForm.tsx index 08099468bab6..1ecfd0d5a55f 100644 --- a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionForm.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionForm.tsx @@ -33,12 +33,10 @@ const StyledInputsContainer = styled.div` type SettingsIntegrationPostgreSQLConnectionFormProps = { disabled?: boolean; - passwordPlaceholder?: string; }; export const SettingsIntegrationPostgreSQLConnectionForm = ({ disabled, - passwordPlaceholder, }: SettingsIntegrationPostgreSQLConnectionFormProps) => { const { control } = useFormContext(); @@ -46,13 +44,26 @@ export const SettingsIntegrationPostgreSQLConnectionForm = ({ return ( {[ - { name: 'dbname' as const, label: 'Database Name' }, - { name: 'host' as const, label: 'Host' }, - { name: 'port' as const, label: 'Port' }, - { name: 'username' as const, label: 'Username' }, - { name: 'password' as const, label: 'Password', type: 'password' }, - { name: 'schema' as const, label: 'Schema' }, - ].map(({ name, label, type }) => ( + { + name: 'dbname' as const, + label: 'Database Name', + placeholder: 'default', + }, + { name: 'host' as const, label: 'Host', placeholder: 'host' }, + { name: 'port' as const, label: 'Port', placeholder: '5432' }, + { + name: 'username' as const, + label: 'Username', + placeholder: 'username', + }, + { + name: 'password' as const, + label: 'Password', + type: 'password', + placeholder: '••••••', + }, + { name: 'schema' as const, label: 'Schema', placeholder: 'public' }, + ].map(({ name, label, type, placeholder }) => ( ); }} diff --git a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContent.tsx b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContent.tsx index 47e6e034ef16..7d1261e79d0f 100644 --- a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContent.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContent.tsx @@ -141,7 +141,6 @@ export const SettingsIntegrationEditDatabaseConnectionContent = ({ ) : null} diff --git a/packages/twenty-front/src/modules/ui/display/info/components/Info.tsx b/packages/twenty-front/src/modules/ui/display/info/components/Info.tsx index 66c3e961de89..22ab79bf128f 100644 --- a/packages/twenty-front/src/modules/ui/display/info/components/Info.tsx +++ b/packages/twenty-front/src/modules/ui/display/info/components/Info.tsx @@ -14,10 +14,15 @@ export type InfoProps = { }; const StyledTextContainer = styled.div` + align-items: center; display: flex; gap: ${({ theme }) => theme.spacing(2)}; `; +const StyledIconInfoCircle = styled(IconInfoCircle)` + flex-shrink: 0; +`; + const StyledInfo = styled.div>` align-items: center; border-radius: ${({ theme }) => theme.border.radius.md}; @@ -51,7 +56,7 @@ export const Info = ({ return ( - + {text} {buttonTitle && onClick && ( From 8d90c60adae339ba9d2675de7be975254912df7f Mon Sep 17 00:00:00 2001 From: Weiko Date: Thu, 2 May 2024 15:47:43 +0200 Subject: [PATCH 05/32] [calendar] hide calendar settings until implemented (#5252) ## Context Those settings are not implemented yet, we would like to move them to a different page as well. In the meantime, we are hiding them since we plan to launch calendar in the next release and this won't be implemented before. We will implement it in this https://github.com/twentyhq/twenty/issues/5140 --- .../src/pages/settings/accounts/SettingsAccountsCalendars.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx index 9c823a4f861a..234b545406b0 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx @@ -24,6 +24,7 @@ import { } from '~/generated-metadata/graphql'; export const SettingsAccountsCalendars = () => { + const calendarSettingsEnabled = false; const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const { records: accounts } = useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.ConnectedAccount, @@ -101,7 +102,7 @@ export const SettingsAccountsCalendars = () => { /> - {!!calendarChannels.length && ( + {!!calendarChannels.length && calendarSettingsEnabled && ( <>
Date: Thu, 2 May 2024 15:50:40 +0200 Subject: [PATCH 06/32] Fix sync metadata script (#5253) While troubleshooting self-hosting migration, we run into issues with sync-metadata script introduced by recent changes --- .vscode/launch.json | 18 ++++++++++++++++++ .../comparators/workspace-field.comparator.ts | 5 ++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 8c8d67934e0d..2a13f9e89cdb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -33,6 +33,24 @@ "internalConsoleOptions": "openOnSessionStart", "console": "internalConsole", "cwd": "${workspaceFolder}/packages/twenty-server/" + }, + { + "name": "twenty-server - command debug example", + "type": "node", + "request": "launch", + "runtimeExecutable": "npx", + "runtimeVersion": "18", + "runtimeArgs": [ + "nx", + "run", + "twenty-server:command", + "my-command", + "--my-parameter value", + ], + "outputCapture": "std", + "internalConsoleOptions": "openOnSessionStart", + "console": "internalConsole", + "cwd": "${workspaceFolder}/packages/twenty-server/" } ] } \ No newline at end of file diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field.comparator.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field.comparator.ts index fb64f47fad92..9da521e0246b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field.comparator.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field.comparator.ts @@ -23,6 +23,9 @@ const commonFieldPropertiesToIgnore = [ 'objectMetadataId', 'isActive', 'options', + 'settings', + 'joinColumn', + 'gate', ]; const fieldPropertiesToStringify = ['defaultValue'] as const; @@ -73,7 +76,7 @@ export class WorkspaceFieldComparator { standardObjectMetadata.fields, { shouldIgnoreProperty: (property, originalMetadata) => { - if (['options', 'gate'].includes(property)) { + if (commonFieldPropertiesToIgnore.includes(property)) { return true; } From f802964de4c30f901ec9613f4becc470f5ae2f04 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Thu, 2 May 2024 15:55:11 +0200 Subject: [PATCH 07/32] Bump to 0.10.6 --- packages/twenty-docs/package.json | 2 +- packages/twenty-emails/package.json | 2 +- packages/twenty-front/package.json | 2 +- packages/twenty-server/package.json | 2 +- packages/twenty-ui/package.json | 2 +- packages/twenty-website/package.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/twenty-docs/package.json b/packages/twenty-docs/package.json index a619ef1acf4d..71d8c2384700 100644 --- a/packages/twenty-docs/package.json +++ b/packages/twenty-docs/package.json @@ -1,6 +1,6 @@ { "name": "twenty-docs", - "version": "0.10.5", + "version": "0.10.6", "private": true, "scripts": { "nx": "NX_DEFAULT_PROJECT=twenty-docs node ../../node_modules/nx/bin/nx.js", diff --git a/packages/twenty-emails/package.json b/packages/twenty-emails/package.json index cf9786413e9d..21a7e115b243 100644 --- a/packages/twenty-emails/package.json +++ b/packages/twenty-emails/package.json @@ -1,6 +1,6 @@ { "name": "twenty-emails", - "version": "0.10.5", + "version": "0.10.6", "description": "", "author": "", "private": true, diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index 499d096f09e3..7820370f9b18 100644 --- a/packages/twenty-front/package.json +++ b/packages/twenty-front/package.json @@ -1,6 +1,6 @@ { "name": "twenty-front", - "version": "0.10.5", + "version": "0.10.6", "private": true, "type": "module", "scripts": { diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 2b84699dd342..fe774b6035aa 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -1,6 +1,6 @@ { "name": "twenty-server", - "version": "0.10.5", + "version": "0.10.6", "description": "", "author": "", "private": true, diff --git a/packages/twenty-ui/package.json b/packages/twenty-ui/package.json index 7a2fdfe41566..f1bc0847ec5a 100644 --- a/packages/twenty-ui/package.json +++ b/packages/twenty-ui/package.json @@ -1,6 +1,6 @@ { "name": "twenty-ui", - "version": "0.10.5", + "version": "0.10.6", "type": "module", "main": "./src/index.ts", "exports": { diff --git a/packages/twenty-website/package.json b/packages/twenty-website/package.json index 12cf976e48ea..e304f4865512 100644 --- a/packages/twenty-website/package.json +++ b/packages/twenty-website/package.json @@ -1,6 +1,6 @@ { "name": "twenty-website", - "version": "0.10.5", + "version": "0.10.6", "private": true, "scripts": { "nx": "NX_DEFAULT_PROJECT=twenty-website node ../../node_modules/nx/bin/nx.js", From 5128ea3ffbb48fcd8682d5fa27c25d3c5529cc19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Thu, 2 May 2024 16:15:36 +0200 Subject: [PATCH 08/32] fix: fix storybook build script not found by Chromatic (#5235) --- .../workflows/actions/task-cache/action.yaml | 4 +- .github/workflows/ci-chromatic.yaml | 39 ----------- .github/workflows/ci-front.yaml | 58 +++++++++++++--- .gitignore | 1 + nx.json | 29 ++++++-- packages/twenty-front/package.json | 1 + packages/twenty-front/project.json | 68 +++++++++++++++---- .../components/RightDrawerEmailThread.tsx | 2 +- .../usePrepareFindManyActivitiesQuery.ts | 4 +- .../__stories__/PhoneFieldDisplay.stories.tsx | 2 +- .../__stories__/PhoneFieldInput.stories.tsx | 2 +- .../button/components/FloatingIconButton.tsx | 13 ++-- 12 files changed, 145 insertions(+), 78 deletions(-) delete mode 100644 .github/workflows/ci-chromatic.yaml diff --git a/.github/workflows/actions/task-cache/action.yaml b/.github/workflows/actions/task-cache/action.yaml index f97f2653a6c1..30b4487c9b54 100644 --- a/.github/workflows/actions/task-cache/action.yaml +++ b/.github/workflows/actions/task-cache/action.yaml @@ -24,6 +24,6 @@ runs: path: | .cache .nx/cache - key: tasks-cache-${{ inputs.tag }}-${{ steps.tasks-key.outputs.key }}${{ inputs.suffix }}-${{ github.sha }} + key: tasks-cache-${{ github.ref_name }}-${{ inputs.tag }}-${{ steps.tasks-key.outputs.key }}${{ inputs.suffix }}-${{ github.sha }} restore-keys: | - tasks-cache-${{ inputs.tag }}-${{ steps.tasks-key.outputs.key }}${{ inputs.suffix }}- \ No newline at end of file + tasks-cache-${{ github.ref_name }}-${{ inputs.tag }}-${{ steps.tasks-key.outputs.key }}${{ inputs.suffix }}- \ No newline at end of file diff --git a/.github/workflows/ci-chromatic.yaml b/.github/workflows/ci-chromatic.yaml deleted file mode 100644 index fd8eb3c346af..000000000000 --- a/.github/workflows/ci-chromatic.yaml +++ /dev/null @@ -1,39 +0,0 @@ -name: 'CI Chromatic' - -on: - push: - branches: - - main - paths: - - 'package.json' - - 'packages/twenty-front/**' - pull_request: - types: [labeled] - paths: - - 'package.json' - - 'packages/twenty-front/**' -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - chromatic-deployment: - if: contains(github.event.pull_request.labels.*.name, 'run-chromatic') || github.event_name == 'push' - runs-on: ubuntu-latest - env: - REACT_APP_SERVER_BASE_URL: http://127.0.0.1:3000 - CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Install dependencies - uses: ./.github/workflows/actions/yarn-install - - name: Front / Write .env - run: | - cd packages/twenty-front - touch .env - echo "REACT_APP_SERVER_BASE_URL: $REACT_APP_SERVER_BASE_URL" >> .env - - name: Publish to Chromatic - run: | - npx nx run twenty-front:chromatic:ci diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index af27198d48d3..56fc154bd0cc 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -17,19 +17,37 @@ concurrency: cancel-in-progress: true jobs: + front-sb-build: + runs-on: ubuntu-latest + env: + REACT_APP_SERVER_BASE_URL: http://localhost:3000 + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.11.0 + with: + access_token: ${{ github.token }} + - name: Fetch local actions + uses: actions/checkout@v4 + - name: Install dependencies + uses: ./.github/workflows/actions/yarn-install + - name: Front / Restore Storybook Task Cache + uses: ./.github/workflows/actions/task-cache + with: + tag: scope:frontend + tasks: storybook:build + - name: Front / Write .env + run: npx nx reset:env twenty-front + - name: Front / Build storybook + run: npx nx storybook:build twenty-front front-sb-test: runs-on: ci-8-cores + needs: front-sb-build strategy: matrix: storybook_scope: [pages, modules] env: REACT_APP_SERVER_BASE_URL: http://localhost:3000 - STORYBOOK_SCOPE: ${{ matrix.storybook_scope }} steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.11.0 - with: - access_token: ${{ github.token }} - name: Fetch local actions uses: actions/checkout@v4 - name: Install dependencies @@ -41,14 +59,36 @@ jobs: with: tag: scope:frontend tasks: storybook:build - suffix: _${{ matrix.storybook_scope }} - name: Front / Write .env run: npx nx reset:env twenty-front - name: Run storybook tests + run: npx nx storybook:test twenty-front --configuration=ci --scope=${{ matrix.storybook_scope }} + front-chromatic-deployment: + if: contains(github.event.pull_request.labels.*.name, 'run-chromatic') || github.event_name == 'push' + needs: front-sb-build + runs-on: ubuntu-latest + env: + REACT_APP_SERVER_BASE_URL: http://127.0.0.1:3000 + CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install dependencies + uses: ./.github/workflows/actions/yarn-install + - name: Front / Restore Storybook Task Cache + uses: ./.github/workflows/actions/task-cache + with: + tag: scope:frontend + tasks: storybook:build + - name: Front / Write .env + run: | + cd packages/twenty-front + touch .env + echo "REACT_APP_SERVER_BASE_URL: $REACT_APP_SERVER_BASE_URL" >> .env + - name: Publish to Chromatic run: | - npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \ - "npx nx storybook:static twenty-front" \ - "npx wait-on tcp:6006 && npx nx storybook:test twenty-front" + npx nx run twenty-front:chromatic:ci front-task: runs-on: ubuntu-latest strategy: diff --git a/.gitignore b/.gitignore index a3d911f02b6f..8cd3356e88a6 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ storybook-static *.tsbuildinfo .eslintcache .cache +.nyc_output diff --git a/nx.json b/nx.json index 1eab0f5bafcf..f3d34e7346de 100644 --- a/nx.json +++ b/nx.json @@ -112,19 +112,38 @@ "storybook:test": { "executor": "nx:run-commands", "options": { - "cwd": "{projectRoot}", "commands": [ - "test-storybook --url {options.url} --maxWorkers=3 --coverage", - "nyc report --reporter=lcov --reporter=text-summary -t coverage/storybook --report-dir coverage/storybook --check-coverage" + "test-storybook -c {args.configDir} --url {args.url} --maxWorkers=3 --coverage", + "nyc report --reporter=lcov --reporter=text-summary -t {args.coverageDir} --report-dir {args.coverageDir} --check-coverage" ], - "parallel": false + "parallel": false, + "configDir": "{projectRoot}/.storybook", + "coverageDir": "{projectRoot}/coverage/storybook", + "url": "http://localhost:6006", + "port": 6006 + }, + "configurations": { + "ci": { + "commands": [ + { + "prefix": "[SB]", + "command": "nx storybook:static {projectName} --port={args.port}", + "forwardAllArgs": false + }, + { + "command": "npx wait-on tcp:{args.port} && nx storybook:test {projectName}", + "forwardAllArgs": false + } + ], + "parallel": true + } } }, "chromatic": { "executor": "nx:run-commands", "options": { "cwd": "{projectRoot}", - "command": "cross-var chromatic --project-token=$CHROMATIC_PROJECT_TOKEN --build-script-name=storybook:build --exit-zero-on-changes={args.ci}", + "command": "cross-var chromatic --project-token=$CHROMATIC_PROJECT_TOKEN --build-script-name=build-storybook --exit-zero-on-changes={args.ci}", "ci": false }, "configurations": { diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index 7820370f9b18..30c1a212739a 100644 --- a/packages/twenty-front/package.json +++ b/packages/twenty-front/package.json @@ -6,6 +6,7 @@ "scripts": { "build": "npx vite build && sh ./scripts/inject-runtime-env.sh", "build:sourcemaps": "VITE_BUILD_SOURCEMAP=true NODE_OPTIONS=--max-old-space-size=4096 npx nx build", + "build-storybook": "cd ../.. && npx nx storybook:build twenty-front", "start:prod": "NODE_ENV=production npx vite --host", "tsup": "npx tsup" }, diff --git a/packages/twenty-front/project.json b/packages/twenty-front/project.json index 7d32dd328573..b0fa26f4bb36 100644 --- a/packages/twenty-front/project.json +++ b/packages/twenty-front/project.json @@ -60,23 +60,74 @@ }, "test": {}, "storybook:build": {}, + "storybook:build:scope": { + "executor": "nx:run-commands", + "options": { + "command": "STORYBOOK_SCOPE={args.scope} nx storybook:build twenty-front", + "scope": "all" + }, + "configurations": { + "docs": { "scope": "ui-docs" }, + "modules": { "scope": "modules" }, + "pages": { "scope": "pages" } + } + }, "storybook:dev": { "options": { "port": 6006 } }, + "storybook:dev:scope": { + "executor": "nx:run-commands", + "options": { + "command": "STORYBOOK_SCOPE={args.scope} nx storybook:dev twenty-front", + "scope": "all" + }, + "configurations": { + "docs": { "scope": "ui-docs" }, + "modules": { "scope": "modules" }, + "pages": { "scope": "pages" } + } + }, "storybook:static": { "options": { "buildTarget": "twenty-front:storybook:build", "port": 6006 } }, + "storybook:static:scope": { + "executor": "nx:run-commands", + "options": { + "command": "STORYBOOK_SCOPE={args.scope} nx storybook:static twenty-front", + "scope": "all" + }, + "configurations": { + "docs": { "scope": "ui-docs" }, + "modules": { "scope": "modules" }, + "pages": { "scope": "pages" } + } + }, "storybook:test": { "options": { - "url": "http://localhost:6006" + "url": "http://localhost:6006", + "port": 6006 }, "configurations": { - "docs": { "env": { "STORYBOOK_SCOPE": "ui-docs" } }, - "modules": { "env": { "STORYBOOK_SCOPE": "modules" } }, - "pages": { "env": { "STORYBOOK_SCOPE": "pages" } } + "ci": { + "commands": [ + { + "prefix": "[SB]", + "command": "nx storybook:static {projectName} --port={args.port}", + "forwardAllArgs": false + }, + { + "command": "STORYBOOK_SCOPE={args.scope} npx wait-on tcp:{args.port} && nx storybook:test {projectName}", + "forwardAllArgs": false + } + ], + "parallel": true + }, + "docs": { "scope": "ui-docs" }, + "modules": { "scope": "modules" }, + "pages": { "scope": "pages" } } }, "graphql:generate": { @@ -96,15 +147,8 @@ } }, "chromatic": { - "executor": "nx:run-commands", - "options": { - "cwd": "{projectRoot}", - "command": "cross-var chromatic --project-token=$CHROMATIC_PROJECT_TOKEN --build-script-name=storybook:build" - }, "configurations": { - "ci": { - "args": ["--exit-zero-on-changes"] - } + "ci": {} } } } diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx b/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx index 40360a96c557..f668c08ad53f 100644 --- a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx +++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx @@ -43,7 +43,7 @@ export const RightDrawerEmailThread = () => { useRegisterClickOutsideListenerCallback({ callbackId: - 'EmailThreadClickOutsideCallBack-' + thread.id ?? 'no-thread-id', + 'EmailThreadClickOutsideCallBack-' + (thread.id ?? 'no-thread-id'), callbackFunction: useRecoilCallback( ({ set }) => () => { diff --git a/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts b/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts index a7cd55f7e7fc..7f5d9c492c8a 100644 --- a/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts +++ b/packages/twenty-front/src/modules/activities/hooks/usePrepareFindManyActivitiesQuery.ts @@ -97,9 +97,7 @@ export const usePrepareFindManyActivitiesQuery = () => { }; const filteredActivities = [ - ...activities.filter( - (activity) => !shouldActivityBeExcluded?.(activity) ?? true, - ), + ...activities.filter((activity) => !shouldActivityBeExcluded?.(activity)), ].sort((a, b) => { return a.createdAt > b.createdAt ? -1 : 1; }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx index c656e7805858..0d4d07d71b9c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx @@ -31,7 +31,7 @@ const meta: Meta = { fieldDefinition: { fieldMetadataId: 'phone', label: 'Phone', - type: FieldMetadataType.Text, + type: FieldMetadataType.Phone, iconName: 'IconPhone', metadata: { fieldName: 'phone', diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx index c38b58a2d687..761ed49ce005 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx @@ -45,7 +45,7 @@ const PhoneFieldInputWithContext = ({ fieldDefinition={{ fieldMetadataId: 'phone', label: 'Phone', - type: FieldMetadataType.Text, + type: FieldMetadataType.Phone, iconName: 'IconPhone', metadata: { fieldName: 'phone', diff --git a/packages/twenty-front/src/modules/ui/input/button/components/FloatingIconButton.tsx b/packages/twenty-front/src/modules/ui/input/button/components/FloatingIconButton.tsx index 4a22cd1c4561..d281a74601d4 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/FloatingIconButton.tsx +++ b/packages/twenty-front/src/modules/ui/input/button/components/FloatingIconButton.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useTheme } from '@emotion/react'; +import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { IconComponent } from 'twenty-ui'; @@ -87,10 +87,13 @@ const StyledButton = styled.button< `; }} - &:hover { - background: ${({ theme, isActive }) => - !!isActive ?? theme.background.transparent.lighter}; - } + ${({ theme, isActive }) => + isActive && + css` + &:hover { + background: ${theme.background.transparent.lighter}; + } + `} &:active { background: ${({ theme, disabled }) => From f9c19c839bb8b096c8e6e47e257478c6dc3966c4 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Thu, 2 May 2024 17:13:15 +0200 Subject: [PATCH 09/32] Build stripe integration on backend side (#5246) Adding stripe integration by making the server logic independent of the input fields: - query factories (remote server, foreign data wrapper, foreign table) to loop on fields and values without hardcoding the names of the fields - adding stripe input and type - add the logic to handle static schema. Simply creating a big object to store into the server Additional work: - rename username field to user. This is the input intended for postgres user mapping and we now need a matching by name --------- Co-authored-by: Thomas Trompette --- .../src/generated-metadata/gql.ts | 4 +- .../src/generated-metadata/graphql.ts | 42 ++--- .../twenty-front/src/generated/graphql.tsx | 10 +- .../fragments/databaseConnectionFragment.ts | 2 +- ...tingsIntegrationDatabaseConnectionForm.tsx | 8 +- .../utils/editDatabaseConnection.ts | 4 +- ...ttingsIntegrationNewDatabaseConnection.tsx | 2 +- .../src/testing/mock-data/remote-servers.ts | 2 +- .../typeorm-seeds/core/feature-flags.ts | 5 + .../factories/factories.ts | 4 +- .../foreign-data-wrapper-query.factory.ts | 105 ----------- ...reign-data-wrapper-server-query.factory.ts | 69 +++++++ .../feature-flag/feature-flag.entity.ts | 1 + .../dtos/create-remote-server.input.ts | 2 +- .../dtos/update-remote-server.input.ts | 2 +- .../remote-server/dtos/user-mapping-dto.ts | 4 +- .../remote-server/remote-server.entity.ts | 12 +- .../remote-server/remote-server.module.ts | 4 +- .../remote-server/remote-server.service.ts | 26 +-- .../distant-table/distant-table.service.ts | 36 +++- .../util/stripe-distant-tables.util.ts | 91 +++++++++ .../remote-table/dtos/remote-table.dto.ts | 5 +- .../remote-table/remote-table.service.ts | 26 ++- .../utils/udt-name-mapper.util.ts | 3 + .../user-mapping-options.ts} | 4 +- ...ld-update-remote-server-raw-query.utils.ts | 176 +++++++----------- .../utils/validate-remote-server-type.util.ts | 2 + .../workspace-migration.entity.ts | 14 +- .../workspace-migration-runner.service.ts | 8 +- .../commands/add-standard-id.command.ts | 2 + 30 files changed, 394 insertions(+), 281 deletions(-) delete mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/distant-table/util/stripe-distant-tables.util.ts rename packages/twenty-server/src/engine/metadata-modules/remote-server/{utils/user-mapping-options.utils.ts => types/user-mapping-options.ts} (92%) diff --git a/packages/twenty-front/src/generated-metadata/gql.ts b/packages/twenty-front/src/generated-metadata/gql.ts index 25ca29826657..8d2ccc33726f 100644 --- a/packages/twenty-front/src/generated-metadata/gql.ts +++ b/packages/twenty-front/src/generated-metadata/gql.ts @@ -13,7 +13,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/ * Therefore it is highly recommended to use the babel or swc plugin for production. */ const documents = { - "\n fragment RemoteServerFields on RemoteServer {\n id\n createdAt\n foreignDataWrapperId\n foreignDataWrapperOptions\n foreignDataWrapperType\n userMappingOptions {\n username\n }\n updatedAt\n schema\n }\n": types.RemoteServerFieldsFragmentDoc, + "\n fragment RemoteServerFields on RemoteServer {\n id\n createdAt\n foreignDataWrapperId\n foreignDataWrapperOptions\n foreignDataWrapperType\n userMappingOptions {\n user\n }\n updatedAt\n schema\n }\n": types.RemoteServerFieldsFragmentDoc, "\n fragment RemoteTableFields on RemoteTable {\n id\n name\n schema\n status\n }\n": types.RemoteTableFieldsFragmentDoc, "\n \n mutation createServer($input: CreateRemoteServerInput!) {\n createOneRemoteServer(input: $input) {\n ...RemoteServerFields\n }\n }\n": types.CreateServerDocument, "\n mutation deleteServer($input: RemoteServerIdInput!) {\n deleteOneRemoteServer(input: $input) {\n id\n }\n }\n": types.DeleteServerDocument, @@ -50,7 +50,7 @@ export function graphql(source: string): unknown; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n fragment RemoteServerFields on RemoteServer {\n id\n createdAt\n foreignDataWrapperId\n foreignDataWrapperOptions\n foreignDataWrapperType\n userMappingOptions {\n username\n }\n updatedAt\n schema\n }\n"): (typeof documents)["\n fragment RemoteServerFields on RemoteServer {\n id\n createdAt\n foreignDataWrapperId\n foreignDataWrapperOptions\n foreignDataWrapperType\n userMappingOptions {\n username\n }\n updatedAt\n schema\n }\n"]; +export function graphql(source: "\n fragment RemoteServerFields on RemoteServer {\n id\n createdAt\n foreignDataWrapperId\n foreignDataWrapperOptions\n foreignDataWrapperType\n userMappingOptions {\n user\n }\n updatedAt\n schema\n }\n"): (typeof documents)["\n fragment RemoteServerFields on RemoteServer {\n id\n createdAt\n foreignDataWrapperId\n foreignDataWrapperOptions\n foreignDataWrapperType\n userMappingOptions {\n user\n }\n updatedAt\n schema\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 8cc49611486d..591d37e4acf8 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -799,7 +799,7 @@ export type RemoteServer = { id: Scalars['ID']['output']; schema?: Maybe; updatedAt: Scalars['DateTime']['output']; - userMappingOptions?: Maybe; + userMappingOptions?: Maybe; }; export type RemoteServerIdInput = { @@ -815,7 +815,7 @@ export type RemoteTable = { __typename?: 'RemoteTable'; id?: Maybe; name: Scalars['String']['output']; - schema: Scalars['String']['output']; + schema?: Maybe; status: RemoteTableStatus; }; @@ -1057,17 +1057,17 @@ export type UserExists = { export type UserMappingOptions = { password?: InputMaybe; - username?: InputMaybe; + user?: InputMaybe; }; export type UserMappingOptionsUpdateInput = { password?: InputMaybe; - username?: InputMaybe; + user?: InputMaybe; }; -export type UserMappingOptionsUsername = { - __typename?: 'UserMappingOptionsUsername'; - username?: Maybe; +export type UserMappingOptionsUser = { + __typename?: 'UserMappingOptionsUser'; + user?: Maybe; }; export type UserWorkspace = { @@ -1253,16 +1253,16 @@ export type RelationEdge = { node: Relation; }; -export type RemoteServerFieldsFragment = { __typename?: 'RemoteServer', id: string, createdAt: any, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: any, schema?: string | null, userMappingOptions?: { __typename?: 'UserMappingOptionsUsername', username?: string | null } | null }; +export type RemoteServerFieldsFragment = { __typename?: 'RemoteServer', id: string, createdAt: any, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: any, schema?: string | null, userMappingOptions?: { __typename?: 'UserMappingOptionsUser', user?: string | null } | null }; -export type RemoteTableFieldsFragment = { __typename?: 'RemoteTable', id?: any | null, name: string, schema: string, status: RemoteTableStatus }; +export type RemoteTableFieldsFragment = { __typename?: 'RemoteTable', id?: any | null, name: string, schema?: string | null, status: RemoteTableStatus }; export type CreateServerMutationVariables = Exact<{ input: CreateRemoteServerInput; }>; -export type CreateServerMutation = { __typename?: 'Mutation', createOneRemoteServer: { __typename?: 'RemoteServer', id: string, createdAt: any, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: any, schema?: string | null, userMappingOptions?: { __typename?: 'UserMappingOptionsUsername', username?: string | null } | null } }; +export type CreateServerMutation = { __typename?: 'Mutation', createOneRemoteServer: { __typename?: 'RemoteServer', id: string, createdAt: any, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: any, schema?: string | null, userMappingOptions?: { __typename?: 'UserMappingOptionsUser', user?: string | null } | null } }; export type DeleteServerMutationVariables = Exact<{ input: RemoteServerIdInput; @@ -1276,42 +1276,42 @@ export type SyncRemoteTableMutationVariables = Exact<{ }>; -export type SyncRemoteTableMutation = { __typename?: 'Mutation', syncRemoteTable: { __typename?: 'RemoteTable', id?: any | null, name: string, schema: string, status: RemoteTableStatus } }; +export type SyncRemoteTableMutation = { __typename?: 'Mutation', syncRemoteTable: { __typename?: 'RemoteTable', id?: any | null, name: string, schema?: string | null, status: RemoteTableStatus } }; export type UnsyncRemoteTableMutationVariables = Exact<{ input: RemoteTableInput; }>; -export type UnsyncRemoteTableMutation = { __typename?: 'Mutation', unsyncRemoteTable: { __typename?: 'RemoteTable', id?: any | null, name: string, schema: string, status: RemoteTableStatus } }; +export type UnsyncRemoteTableMutation = { __typename?: 'Mutation', unsyncRemoteTable: { __typename?: 'RemoteTable', id?: any | null, name: string, schema?: string | null, status: RemoteTableStatus } }; export type UpdateServerMutationVariables = Exact<{ input: UpdateRemoteServerInput; }>; -export type UpdateServerMutation = { __typename?: 'Mutation', updateOneRemoteServer: { __typename?: 'RemoteServer', id: string, createdAt: any, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: any, schema?: string | null, userMappingOptions?: { __typename?: 'UserMappingOptionsUsername', username?: string | null } | null } }; +export type UpdateServerMutation = { __typename?: 'Mutation', updateOneRemoteServer: { __typename?: 'RemoteServer', id: string, createdAt: any, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: any, schema?: string | null, userMappingOptions?: { __typename?: 'UserMappingOptionsUser', user?: string | null } | null } }; export type GetManyDatabaseConnectionsQueryVariables = Exact<{ input: RemoteServerTypeInput; }>; -export type GetManyDatabaseConnectionsQuery = { __typename?: 'Query', findManyRemoteServersByType: Array<{ __typename?: 'RemoteServer', id: string, createdAt: any, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: any, schema?: string | null, userMappingOptions?: { __typename?: 'UserMappingOptionsUsername', username?: string | null } | null }> }; +export type GetManyDatabaseConnectionsQuery = { __typename?: 'Query', findManyRemoteServersByType: Array<{ __typename?: 'RemoteServer', id: string, createdAt: any, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: any, schema?: string | null, userMappingOptions?: { __typename?: 'UserMappingOptionsUser', user?: string | null } | null }> }; export type GetManyRemoteTablesQueryVariables = Exact<{ input: RemoteServerIdInput; }>; -export type GetManyRemoteTablesQuery = { __typename?: 'Query', findAvailableRemoteTablesByServerId: Array<{ __typename?: 'RemoteTable', id?: any | null, name: string, schema: string, status: RemoteTableStatus }> }; +export type GetManyRemoteTablesQuery = { __typename?: 'Query', findAvailableRemoteTablesByServerId: Array<{ __typename?: 'RemoteTable', id?: any | null, name: string, schema?: string | null, status: RemoteTableStatus }> }; export type GetOneDatabaseConnectionQueryVariables = Exact<{ input: RemoteServerIdInput; }>; -export type GetOneDatabaseConnectionQuery = { __typename?: 'Query', findOneRemoteServerById: { __typename?: 'RemoteServer', id: string, createdAt: any, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: any, schema?: string | null, userMappingOptions?: { __typename?: 'UserMappingOptionsUsername', username?: string | null } | null } }; +export type GetOneDatabaseConnectionQuery = { __typename?: 'Query', findOneRemoteServerById: { __typename?: 'RemoteServer', id: string, createdAt: any, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: any, schema?: string | null, userMappingOptions?: { __typename?: 'UserMappingOptionsUser', user?: string | null } | null } }; export type CreateOneObjectMetadataItemMutationVariables = Exact<{ input: CreateOneObjectInput; @@ -1372,16 +1372,16 @@ export type ObjectMetadataItemsQueryVariables = Exact<{ export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', edges: Array<{ __typename?: 'objectEdge', node: { __typename?: 'object', id: any, dataSourceId: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, createdAt: any, updatedAt: any, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, fields: { __typename?: 'ObjectFieldsConnection', edges: Array<{ __typename?: 'fieldEdge', node: { __typename?: 'field', id: any, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isNullable?: boolean | null, createdAt: any, updatedAt: any, defaultValue?: any | null, options?: any | null, fromRelationMetadata?: { __typename?: 'relation', id: any, relationType: RelationMetadataType, toFieldMetadataId: string, toObjectMetadata: { __typename?: 'object', id: any, dataSourceId: string, nameSingular: string, namePlural: string, isSystem: boolean, isRemote: boolean } } | null, toRelationMetadata?: { __typename?: 'relation', id: any, relationType: RelationMetadataType, fromFieldMetadataId: string, fromObjectMetadata: { __typename?: 'object', id: any, dataSourceId: string, nameSingular: string, namePlural: string, isSystem: boolean, isRemote: boolean } } | null, relationDefinition?: { __typename?: 'RelationDefinition', direction: RelationDefinitionType, sourceObjectMetadata: { __typename?: 'object', id: any, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'field', id: any, name: string }, targetObjectMetadata: { __typename?: 'object', id: any, nameSingular: string, namePlural: string }, targetFieldMetadata: { __typename?: 'field', id: any, name: string } } | null } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } }; -export const RemoteServerFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteServerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}},{"kind":"Field","name":{"kind":"Name","value":"userMappingOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}}]}}]} as unknown as DocumentNode; +export const RemoteServerFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteServerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}},{"kind":"Field","name":{"kind":"Name","value":"userMappingOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}}]}}]} as unknown as DocumentNode; export const RemoteTableFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteTableFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteTable"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]} as unknown as DocumentNode; -export const CreateServerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"createServer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateRemoteServerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneRemoteServer"},"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":"RemoteServerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteServerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}},{"kind":"Field","name":{"kind":"Name","value":"userMappingOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}}]}}]} as unknown as DocumentNode; +export const CreateServerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"createServer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateRemoteServerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneRemoteServer"},"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":"RemoteServerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteServerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}},{"kind":"Field","name":{"kind":"Name","value":"userMappingOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}}]}}]} as unknown as DocumentNode; export const DeleteServerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"deleteServer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServerIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneRemoteServer"},"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":"id"}}]}}]}}]} as unknown as DocumentNode; export const SyncRemoteTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"syncRemoteTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteTableInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"syncRemoteTable"},"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":"RemoteTableFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteTableFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteTable"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]} as unknown as DocumentNode; export const UnsyncRemoteTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"unsyncRemoteTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteTableInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unsyncRemoteTable"},"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":"RemoteTableFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteTableFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteTable"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]} as unknown as DocumentNode; -export const UpdateServerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"updateServer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateRemoteServerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneRemoteServer"},"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":"RemoteServerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteServerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}},{"kind":"Field","name":{"kind":"Name","value":"userMappingOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}}]}}]} as unknown as DocumentNode; -export const GetManyDatabaseConnectionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetManyDatabaseConnections"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServerTypeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyRemoteServersByType"},"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":"RemoteServerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteServerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}},{"kind":"Field","name":{"kind":"Name","value":"userMappingOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}}]}}]} as unknown as DocumentNode; +export const UpdateServerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"updateServer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateRemoteServerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneRemoteServer"},"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":"RemoteServerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteServerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}},{"kind":"Field","name":{"kind":"Name","value":"userMappingOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}}]}}]} as unknown as DocumentNode; +export const GetManyDatabaseConnectionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetManyDatabaseConnections"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServerTypeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyRemoteServersByType"},"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":"RemoteServerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteServerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}},{"kind":"Field","name":{"kind":"Name","value":"userMappingOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}}]}}]} as unknown as DocumentNode; export const GetManyRemoteTablesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetManyRemoteTables"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServerIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAvailableRemoteTablesByServerId"},"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":"RemoteTableFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteTableFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteTable"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]} as unknown as DocumentNode; -export const GetOneDatabaseConnectionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOneDatabaseConnection"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServerIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneRemoteServerById"},"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":"RemoteServerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteServerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}},{"kind":"Field","name":{"kind":"Name","value":"userMappingOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}}]}}]} as unknown as DocumentNode; +export const GetOneDatabaseConnectionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOneDatabaseConnection"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServerIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneRemoteServerById"},"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":"RemoteServerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteServerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}},{"kind":"Field","name":{"kind":"Name","value":"userMappingOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}}]}}]} as unknown as DocumentNode; export const CreateOneObjectMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneObjectMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOneObjectInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneObject"},"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":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}}]}}]}}]} as unknown as DocumentNode; export const CreateOneFieldMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneFieldMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOneFieldMetadataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneField"},"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":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}}]}}]} as unknown as DocumentNode; export const CreateOneRelationMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneRelationMetadata"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOneRelationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneRelation"},"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":"id"}},{"kind":"Field","name":{"kind":"Name","value":"relationType"}},{"kind":"Field","name":{"kind":"Name","value":"fromObjectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"toObjectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"fromFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"toFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 82c3f360a1fc..decc9540e40a 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -577,14 +577,14 @@ export type RemoteServer = { id: Scalars['ID']; schema?: Maybe; updatedAt: Scalars['DateTime']; - userMappingOptions?: Maybe; + userMappingOptions?: Maybe; }; export type RemoteTable = { __typename?: 'RemoteTable'; id?: Maybe; name: Scalars['String']; - schema: Scalars['String']; + schema?: Maybe; status: RemoteTableStatus; }; @@ -772,9 +772,9 @@ export type UserExists = { exists: Scalars['Boolean']; }; -export type UserMappingOptionsUsername = { - __typename?: 'UserMappingOptionsUsername'; - username?: Maybe; +export type UserMappingOptionsUser = { + __typename?: 'UserMappingOptionsUser'; + user?: Maybe; }; export type UserWorkspace = { diff --git a/packages/twenty-front/src/modules/databases/graphql/fragments/databaseConnectionFragment.ts b/packages/twenty-front/src/modules/databases/graphql/fragments/databaseConnectionFragment.ts index 053aab7694b1..67d81b3cd906 100644 --- a/packages/twenty-front/src/modules/databases/graphql/fragments/databaseConnectionFragment.ts +++ b/packages/twenty-front/src/modules/databases/graphql/fragments/databaseConnectionFragment.ts @@ -8,7 +8,7 @@ export const DATABASE_CONNECTION_FRAGMENT = gql` foreignDataWrapperOptions foreignDataWrapperType userMappingOptions { - username + user } updatedAt schema diff --git a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionForm.tsx b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionForm.tsx index 1ecfd0d5a55f..91848ee6e6b6 100644 --- a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionForm.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionForm.tsx @@ -8,7 +8,7 @@ export const settingsIntegrationPostgreSQLConnectionFormSchema = z.object({ dbname: z.string().min(1), host: z.string().min(1), port: z.preprocess((val) => parseInt(val as string), z.number().positive()), - username: z.string().min(1), + user: z.string().min(1), password: z.string().min(1), schema: z.string().min(1), }); @@ -52,9 +52,9 @@ export const SettingsIntegrationPostgreSQLConnectionForm = ({ { name: 'host' as const, label: 'Host', placeholder: 'host' }, { name: 'port' as const, label: 'Port', placeholder: '5432' }, { - name: 'username' as const, - label: 'Username', - placeholder: 'username', + name: 'user' as const, + label: 'User', + placeholder: 'user', }, { name: 'password' as const, diff --git a/packages/twenty-front/src/modules/settings/integrations/database-connection/utils/editDatabaseConnection.ts b/packages/twenty-front/src/modules/settings/integrations/database-connection/utils/editDatabaseConnection.ts index d79c65d53bd2..bcb9d6103d60 100644 --- a/packages/twenty-front/src/modules/settings/integrations/database-connection/utils/editDatabaseConnection.ts +++ b/packages/twenty-front/src/modules/settings/integrations/database-connection/utils/editDatabaseConnection.ts @@ -28,7 +28,7 @@ export const getFormDefaultValuesFromConnection = ({ dbname: connection.foreignDataWrapperOptions.dbname, host: connection.foreignDataWrapperOptions.host, port: connection.foreignDataWrapperOptions.port, - username: connection.userMappingOptions?.username || undefined, + user: connection.userMappingOptions?.user || undefined, schema: connection.schema || undefined, password: '', }; @@ -51,7 +51,7 @@ export const formatValuesForUpdate = ({ const formattedValues = { userMappingOptions: pickBy( { - username: formValues.username, + user: formValues.user, password: formValues.password, }, identity, diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx index 9a68e502a34f..f061f52a29e5 100644 --- a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx @@ -41,7 +41,7 @@ const createRemoteServerInputSchema = newConnectionSchema }, userMappingOptions: { password: values.password, - username: values.username, + user: values.user, }, schema: values.schema, })); diff --git a/packages/twenty-front/src/testing/mock-data/remote-servers.ts b/packages/twenty-front/src/testing/mock-data/remote-servers.ts index 295973d2fcb5..f048392a0784 100644 --- a/packages/twenty-front/src/testing/mock-data/remote-servers.ts +++ b/packages/twenty-front/src/testing/mock-data/remote-servers.ts @@ -12,7 +12,7 @@ export const mockedRemoteServers = [ foreignDataWrapperType: 'postgres_fdw', userMappingOptions: { __typename: 'UserMappingOptionsDTO', - username: 'twenty', + user: 'twenty', }, updatedAt: '2024-04-30T13:41:25.858Z', schema: 'public', diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts index a8b6a89be0dd..ad4a970b634f 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts @@ -45,6 +45,11 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: true, }, + { + key: FeatureFlagKeys.IsStripeIntegrationEnabled, + workspaceId: workspaceId, + value: true, + }, ]) .execute(); }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts index db1255540ef1..e10fab44e300 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts @@ -1,4 +1,4 @@ -import { ForeignDataWrapperQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory'; +import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory'; import { ArgsAliasFactory } from './args-alias.factory'; import { ArgsStringFactory } from './args-string.factory'; @@ -30,5 +30,5 @@ export const workspaceQueryBuilderFactories = [ UpdateOneQueryFactory, UpdateManyQueryFactory, DeleteManyQueryFactory, - ForeignDataWrapperQueryFactory, + ForeignDataWrapperServerQueryFactory, ]; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory.ts deleted file mode 100644 index 56f0903498b7..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { isDefined } from 'class-validator'; - -import { - ForeignDataWrapperOptions, - RemoteServerType, -} from 'src/engine/metadata-modules/remote-server/remote-server.entity'; -import { UserMappingOptions } from 'src/engine/metadata-modules/remote-server/utils/user-mapping-options.utils'; - -@Injectable() -export class ForeignDataWrapperQueryFactory { - createForeignDataWrapper( - foreignDataWrapperId: string, - foreignDataWrapperType: RemoteServerType, - foreignDataWrapperOptions: ForeignDataWrapperOptions, - ) { - const [name, options] = this.buildNameAndOptionsFromType( - foreignDataWrapperType, - foreignDataWrapperOptions, - ); - - return `CREATE SERVER "${foreignDataWrapperId}" FOREIGN DATA WRAPPER ${name} OPTIONS (${options})`; - } - - updateForeignDataWrapper({ - foreignDataWrapperId, - foreignDataWrapperOptions, - }: { - foreignDataWrapperId: string; - foreignDataWrapperOptions: Partial< - ForeignDataWrapperOptions - >; - }) { - const options = this.buildUpdateOptions(foreignDataWrapperOptions); - - return `ALTER SERVER "${foreignDataWrapperId}" OPTIONS (${options})`; - } - - createUserMapping( - foreignDataWrapperId: string, - userMappingOptions: UserMappingOptions, - ) { - // CURRENT_USER works for now since we are using only one user. But if we switch to a user per workspace, we need to change this. - return `CREATE USER MAPPING IF NOT EXISTS FOR CURRENT_USER SERVER "${foreignDataWrapperId}" OPTIONS (user '${userMappingOptions.username}', password '${userMappingOptions.password}')`; - } - - updateUserMapping( - foreignDataWrapperId: string, - userMappingOptions: Partial, - ) { - const options = this.buildUpdateUserMappingOptions(userMappingOptions); - - // CURRENT_USER works for now since we are using only one user. But if we switch to a user per workspace, we need to change this. - return `ALTER USER MAPPING FOR CURRENT_USER SERVER "${foreignDataWrapperId}" OPTIONS (${options})`; - } - - private buildNameAndOptionsFromType( - type: RemoteServerType, - options: ForeignDataWrapperOptions, - ) { - switch (type) { - case RemoteServerType.POSTGRES_FDW: - return ['postgres_fdw', this.buildPostgresFDWQueryOptions(options)]; - default: - throw new Error('Foreign data wrapper type not supported'); - } - } - - private buildUpdateOptions( - options: Partial>, - ) { - const rawQuerySetStatements: string[] = []; - - Object.entries(options).forEach(([key, value]) => { - if (isDefined(value)) { - rawQuerySetStatements.push(`SET ${key} '${value}'`); - } - }); - - return rawQuerySetStatements.join(', '); - } - - private buildUpdateUserMappingOptions( - userMappingOptions?: Partial, - ) { - const setStatements: string[] = []; - - if (isDefined(userMappingOptions?.username)) { - setStatements.push(`SET user '${userMappingOptions?.username}'`); - } - - if (isDefined(userMappingOptions?.password)) { - setStatements.push(`SET password '${userMappingOptions?.password}'`); - } - - return setStatements.join(', '); - } - - private buildPostgresFDWQueryOptions( - foreignDataWrapperOptions: ForeignDataWrapperOptions, - ) { - return `dbname '${foreignDataWrapperOptions.dbname}', host '${foreignDataWrapperOptions.host}', port '${foreignDataWrapperOptions.port}'`; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory.ts new file mode 100644 index 000000000000..13199d01e779 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory.ts @@ -0,0 +1,69 @@ +import { Injectable } from '@nestjs/common'; + +import { + ForeignDataWrapperOptions, + RemoteServerType, +} from 'src/engine/metadata-modules/remote-server/remote-server.entity'; +import { UserMappingOptions } from 'src/engine/metadata-modules/remote-server/types/user-mapping-options'; + +@Injectable() +export class ForeignDataWrapperServerQueryFactory { + createForeignDataWrapperServer( + foreignDataWrapperId: string, + foreignDataWrapperType: RemoteServerType, + foreignDataWrapperOptions: ForeignDataWrapperOptions, + ) { + const options = this.buildQueryOptions(foreignDataWrapperOptions, false); + + return `CREATE SERVER "${foreignDataWrapperId}" FOREIGN DATA WRAPPER ${foreignDataWrapperType} OPTIONS (${options})`; + } + + updateForeignDataWrapperServer({ + foreignDataWrapperId, + foreignDataWrapperOptions, + }: { + foreignDataWrapperId: string; + foreignDataWrapperOptions: Partial< + ForeignDataWrapperOptions + >; + }) { + const options = this.buildQueryOptions(foreignDataWrapperOptions, true); + + return `ALTER SERVER "${foreignDataWrapperId}" OPTIONS (${options})`; + } + + createUserMapping( + foreignDataWrapperId: string, + userMappingOptions: UserMappingOptions, + ) { + const options = this.buildQueryOptions(userMappingOptions, false); + + // CURRENT_USER works for now since we are using only one user. But if we switch to a user per workspace, we need to change this. + return `CREATE USER MAPPING IF NOT EXISTS FOR CURRENT_USER SERVER "${foreignDataWrapperId}" OPTIONS (${options})`; + } + + updateUserMapping( + foreignDataWrapperId: string, + userMappingOptions: Partial, + ) { + const options = this.buildQueryOptions(userMappingOptions, true); + + // CURRENT_USER works for now since we are using only one user. But if we switch to a user per workspace, we need to change this. + return `ALTER USER MAPPING FOR CURRENT_USER SERVER "${foreignDataWrapperId}" OPTIONS (${options})`; + } + + private buildQueryOptions( + options: + | ForeignDataWrapperOptions + | Partial> + | UserMappingOptions + | Partial, + isUpdate: boolean, + ) { + const prefix = isUpdate ? 'SET ' : ''; + + return Object.entries(options) + .map(([key, value]) => `${prefix}${key} '${value}'`) + .join(', '); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.entity.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.entity.ts index 396eb8572d43..566063005ddf 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.entity.ts @@ -21,6 +21,7 @@ export enum FeatureFlagKeys { IsEventObjectEnabled = 'IS_EVENT_OBJECT_ENABLED', IsAirtableIntegrationEnabled = 'IS_AIRTABLE_INTEGRATION_ENABLED', IsPostgreSQLIntegrationEnabled = 'IS_POSTGRESQL_INTEGRATION_ENABLED', + IsStripeIntegrationEnabled = 'IS_STRIPE_INTEGRATION_ENABLED', IsMultiSelectEnabled = 'IS_MULTI_SELECT_ENABLED', } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts index eb04e7ec47d9..be2ab5d814be 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts @@ -7,7 +7,7 @@ import { ForeignDataWrapperOptions, RemoteServerType, } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; -import { UserMappingOptions } from 'src/engine/metadata-modules/remote-server/utils/user-mapping-options.utils'; +import { UserMappingOptions } from 'src/engine/metadata-modules/remote-server/types/user-mapping-options'; @InputType() export class CreateRemoteServerInput { diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/update-remote-server.input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/update-remote-server.input.ts index 2e01aae7b2ef..567c25688300 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/update-remote-server.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/update-remote-server.input.ts @@ -10,7 +10,7 @@ import { import { UserMappingOptions, UserMappingOptionsUpdateInput, -} from 'src/engine/metadata-modules/remote-server/utils/user-mapping-options.utils'; +} from 'src/engine/metadata-modules/remote-server/types/user-mapping-options'; @InputType() export class UpdateRemoteServerInput { diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/user-mapping-dto.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/user-mapping-dto.ts index b57f8bbb8507..aff0739b6464 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/user-mapping-dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/user-mapping-dto.ts @@ -2,9 +2,9 @@ import { ObjectType, Field } from '@nestjs/graphql'; import { IsOptional } from 'class-validator'; -@ObjectType('UserMappingOptionsUsername') +@ObjectType('UserMappingOptionsUser') export class UserMappingOptionsDTO { @IsOptional() @Field(() => String, { nullable: true }) - username: string; + user: string; } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts index c579319e043c..91493501bf86 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.entity.ts @@ -10,11 +10,12 @@ import { } from 'typeorm'; import { RemoteTableEntity } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.entity'; -import { UserMappingOptions } from 'src/engine/metadata-modules/remote-server/utils/user-mapping-options.utils'; +import { UserMappingOptions } from 'src/engine/metadata-modules/remote-server/types/user-mapping-options'; import { DistantTables } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/types/distant-table'; export enum RemoteServerType { POSTGRES_FDW = 'postgres_fdw', + STRIPE_FDW = 'stripe_fdw', } type PostgresForeignDataWrapperOptions = { @@ -23,10 +24,17 @@ type PostgresForeignDataWrapperOptions = { dbname: string; }; +type StripeForeignDataWrapperOptions = { + api_key: string; +}; + export type ForeignDataWrapperOptions = T extends RemoteServerType.POSTGRES_FDW ? PostgresForeignDataWrapperOptions - : never; + : T extends RemoteServerType.STRIPE_FDW + ? StripeForeignDataWrapperOptions + : never; + @Entity('remoteServer') export class RemoteServerEntity { @PrimaryGeneratedColumn('uuid') diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.module.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.module.ts index bee0089f35ad..aa42a24909b0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { ForeignDataWrapperQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory'; +import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { RemoteServerEntity } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; import { RemoteServerResolver } from 'src/engine/metadata-modules/remote-server/remote-server.resolver'; @@ -19,7 +19,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works providers: [ RemoteServerService, RemoteServerResolver, - ForeignDataWrapperQueryFactory, + ForeignDataWrapperServerQueryFactory, ], }) export class RemoteServerModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts index 07d05aa17e9c..8c56e393d926 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts @@ -20,11 +20,11 @@ import { validateObjectAgainstInjections, validateStringAgainstInjections, } from 'src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.utils'; -import { ForeignDataWrapperQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory'; +import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory'; import { RemoteTableService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.service'; import { UpdateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/update-remote-server.input'; import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; -import { updateRemoteServerRawQuery } from 'src/engine/metadata-modules/remote-server/utils/build-update-remote-server-raw-query.utils'; +import { buildUpdateRemoteServerRawQuery } from 'src/engine/metadata-modules/remote-server/utils/build-update-remote-server-raw-query.utils'; import { validateRemoteServerType } from 'src/engine/metadata-modules/remote-server/utils/validate-remote-server-type.util'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; @@ -38,7 +38,7 @@ export class RemoteServerService { @InjectDataSource('metadata') private readonly metadataDataSource: DataSource, private readonly environmentService: EnvironmentService, - private readonly foreignDataWrapperQueryFactory: ForeignDataWrapperQueryFactory, + private readonly foreignDataWrapperServerQueryFactory: ForeignDataWrapperServerQueryFactory, private readonly remoteTableService: RemoteTableService, private readonly workspaceDataSourceService: WorkspaceDataSourceService, @InjectRepository(FeatureFlagEntity, 'core') @@ -85,7 +85,7 @@ export class RemoteServerService { ); const foreignDataWrapperQuery = - this.foreignDataWrapperQueryFactory.createForeignDataWrapper( + this.foreignDataWrapperServerQueryFactory.createForeignDataWrapperServer( createdRemoteServer.foreignDataWrapperId, remoteServerInput.foreignDataWrapperType, remoteServerInput.foreignDataWrapperOptions, @@ -95,7 +95,7 @@ export class RemoteServerService { if (remoteServerInput.userMappingOptions) { const userMappingQuery = - this.foreignDataWrapperQueryFactory.createUserMapping( + this.foreignDataWrapperServerQueryFactory.createUserMapping( createdRemoteServer.foreignDataWrapperId, remoteServerInput.userMappingOptions, ); @@ -167,18 +167,20 @@ export class RemoteServerService { !isEmpty(partialRemoteServerWithUpdates.foreignDataWrapperOptions) ) { const foreignDataWrapperQuery = - this.foreignDataWrapperQueryFactory.updateForeignDataWrapper({ - foreignDataWrapperId, - foreignDataWrapperOptions: - partialRemoteServerWithUpdates.foreignDataWrapperOptions, - }); + this.foreignDataWrapperServerQueryFactory.updateForeignDataWrapperServer( + { + foreignDataWrapperId, + foreignDataWrapperOptions: + partialRemoteServerWithUpdates.foreignDataWrapperOptions, + }, + ); await entityManager.query(foreignDataWrapperQuery); } if (!isEmpty(partialRemoteServerWithUpdates.userMappingOptions)) { const userMappingQuery = - this.foreignDataWrapperQueryFactory.updateUserMapping( + this.foreignDataWrapperServerQueryFactory.updateUserMapping( foreignDataWrapperId, partialRemoteServerWithUpdates.userMappingOptions, ); @@ -254,7 +256,7 @@ export class RemoteServerService { Pick, 'workspaceId' | 'id'>, ): Promise> { const [parameters, rawQuery] = - updateRemoteServerRawQuery(remoteServerToUpdate); + buildUpdateRemoteServerRawQuery(remoteServerToUpdate); const updateResult = await this.workspaceDataSourceService.executeRawQuery( rawQuery, diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.service.ts index 4be86540991d..9905da7a5865 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.service.ts @@ -11,6 +11,7 @@ import { import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { DistantTableColumn } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/types/distant-table-column'; import { DistantTables } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/types/distant-table'; +import { STRIPE_DISTANT_TABLES } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/util/stripe-distant-tables.util'; @Injectable() export class DistantTableService { @@ -27,7 +28,9 @@ export class DistantTableService { tableName: string, ): Promise { if (!remoteServer.availableTables) { - throw new Error('Remote server available tables are not defined'); + throw new BadRequestException( + 'Remote server available tables are not defined', + ); } return remoteServer.availableTables[tableName]; @@ -47,6 +50,20 @@ export class DistantTableService { private async createAvailableTables( remoteServer: RemoteServerEntity, workspaceId: string, + ): Promise { + if (remoteServer.schema) { + return this.createAvailableTablesFromDynamicSchema( + remoteServer, + workspaceId, + ); + } + + return this.createAvailableTablesFromStaticSchema(remoteServer); + } + + private async createAvailableTablesFromDynamicSchema( + remoteServer: RemoteServerEntity, + workspaceId: string, ): Promise { if (!remoteServer.schema) { throw new BadRequestException('Remote server schema is not defined'); @@ -99,4 +116,21 @@ export class DistantTableService { return availableTables; } + + private async createAvailableTablesFromStaticSchema( + remoteServer: RemoteServerEntity, + ): Promise { + switch (remoteServer.foreignDataWrapperType) { + case RemoteServerType.STRIPE_FDW: + this.remoteServerRepository.update(remoteServer.id, { + availableTables: STRIPE_DISTANT_TABLES, + }); + + return STRIPE_DISTANT_TABLES; + default: + throw new BadRequestException( + `Type ${remoteServer.foreignDataWrapperType} does not have a static schema.`, + ); + } + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/distant-table/util/stripe-distant-tables.util.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/distant-table/util/stripe-distant-tables.util.ts new file mode 100644 index 000000000000..93d98150d241 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/distant-table/util/stripe-distant-tables.util.ts @@ -0,0 +1,91 @@ +import { DistantTables } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/types/distant-table'; + +export const STRIPE_DISTANT_TABLES: DistantTables = { + balance_transactions: [ + { columnName: 'id', dataType: 'text', udtName: 'text' }, + { columnName: 'amount', dataType: 'bigint', udtName: 'int8' }, + { columnName: 'currency', dataType: 'text', udtName: 'text' }, + { columnName: 'description', dataType: 'text', udtName: 'text' }, + { columnName: 'fee', dataType: 'bigint', udtName: 'int8' }, + { columnName: 'net', dataType: 'bigint', udtName: 'int8' }, + { columnName: 'status', dataType: 'text', udtName: 'text' }, + { columnName: 'type', dataType: 'text', udtName: 'text' }, + { columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' }, + ], + customers: [ + { columnName: 'id', dataType: 'text', udtName: 'text' }, + { columnName: 'email', dataType: 'text', udtName: 'text' }, + { columnName: 'name', dataType: 'text', udtName: 'text' }, + { columnName: 'description', dataType: 'text', udtName: 'text' }, + { columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' }, + ], + disputes: [ + { columnName: 'id', dataType: 'text', udtName: 'text' }, + { columnName: 'amount', dataType: 'bigint', udtName: 'int8' }, + { columnName: 'currency', dataType: 'text', udtName: 'text' }, + { columnName: 'charge', dataType: 'text', udtName: 'text' }, + { columnName: 'payment_intent', dataType: 'text', udtName: 'text' }, + { columnName: 'reason', dataType: 'text', udtName: 'text' }, + { columnName: 'status', dataType: 'text', udtName: 'text' }, + { columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' }, + ], + files: [ + { columnName: 'id', dataType: 'text', udtName: 'text' }, + { columnName: 'filename', dataType: 'text', udtName: 'text' }, + { columnName: 'purpose', dataType: 'text', udtName: 'text' }, + { columnName: 'title', dataType: 'text', udtName: 'text' }, + { columnName: 'size', dataType: 'bigint', udtName: 'int8' }, + { columnName: 'type', dataType: 'text', udtName: 'text' }, + { columnName: 'url', dataType: 'text', udtName: 'text' }, + { columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' }, + { columnName: 'expires_at', dataType: 'timestamp', udtName: 'timestamp' }, + ], + file_links: [ + { columnName: 'id', dataType: 'text', udtName: 'text' }, + { columnName: 'file', dataType: 'text', udtName: 'text' }, + { columnName: 'url', dataType: 'text', udtName: 'text' }, + { columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' }, + { columnName: 'expired', dataType: 'bool', udtName: 'boolean' }, + { columnName: 'expires_at', dataType: 'timestamp', udtName: 'timestamp' }, + ], + mandates: [ + { columnName: 'id', dataType: 'text', udtName: 'text' }, + { columnName: 'payment_method', dataType: 'text', udtName: 'text' }, + { columnName: 'status', dataType: 'text', udtName: 'text' }, + { columnName: 'type', dataType: 'text', udtName: 'text' }, + ], + payouts: [ + { columnName: 'id', dataType: 'text', udtName: 'text' }, + { columnName: 'amount', dataType: 'bigint', udtName: 'int8' }, + { columnName: 'currency', dataType: 'text', udtName: 'text' }, + { columnName: 'description', dataType: 'text', udtName: 'text' }, + { columnName: 'status', dataType: 'text', udtName: 'text' }, + { columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' }, + ], + refunds: [ + { columnName: 'id', dataType: 'text', udtName: 'text' }, + { columnName: 'amount', dataType: 'bigint', udtName: 'int8' }, + { columnName: 'currency', dataType: 'text', udtName: 'text' }, + { columnName: 'charge', dataType: 'text', udtName: 'text' }, + { columnName: 'payment_intent', dataType: 'text', udtName: 'text' }, + { columnName: 'reason', dataType: 'text', udtName: 'text' }, + { columnName: 'status', dataType: 'text', udtName: 'text' }, + { columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' }, + ], + topups: [ + { columnName: 'id', dataType: 'text', udtName: 'text' }, + { columnName: 'amount', dataType: 'bigint', udtName: 'int8' }, + { columnName: 'currency', dataType: 'text', udtName: 'text' }, + { columnName: 'description', dataType: 'text', udtName: 'text' }, + { columnName: 'status', dataType: 'text', udtName: 'text' }, + { columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' }, + ], + transfers: [ + { columnName: 'id', dataType: 'text', udtName: 'text' }, + { columnName: 'amount', dataType: 'bigint', udtName: 'int8' }, + { columnName: 'currency', dataType: 'text', udtName: 'text' }, + { columnName: 'description', dataType: 'text', udtName: 'text' }, + { columnName: 'destination', dataType: 'text', udtName: 'text' }, + { columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' }, + ], +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto.ts index 4c7fa5401d37..07dd57170315 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto.ts @@ -1,7 +1,7 @@ import { ObjectType, Field, registerEnumType } from '@nestjs/graphql'; import { IDField } from '@ptc-org/nestjs-query-graphql'; -import { IsEnum } from 'class-validator'; +import { IsEnum, IsOptional } from 'class-validator'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @@ -27,6 +27,7 @@ export class RemoteTableDTO { @Field(() => RemoteTableStatus) status: RemoteTableStatus; - @Field(() => String) + @IsOptional() + @Field(() => String, { nullable: true }) schema?: string; } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts index ca098290a89b..ae2d34bcfc22 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts @@ -26,6 +26,7 @@ import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { + ReferencedTable, WorkspaceMigrationForeignColumnDefinition, WorkspaceMigrationForeignTable, WorkspaceMigrationTableActionType, @@ -338,6 +339,11 @@ export class RemoteTableService { remoteServer: RemoteServerEntity, distantTableColumns: DistantTableColumn[], ) { + const referencedTable: ReferencedTable = this.buildReferencedTable( + remoteServer, + remoteTableInput, + ); + const workspaceMigration = await this.workspaceMigrationService.createCustomMigration( generateMigrationName(`create-foreign-table-${localTableName}`), @@ -355,8 +361,7 @@ export class RemoteTableService { distantColumnName: column.columnName, }) satisfies WorkspaceMigrationForeignColumnDefinition, ), - referencedTableName: remoteTableInput.name, - referencedTableSchema: remoteServer.schema, + referencedTable, foreignDataWrapperId: remoteServer.foreignDataWrapperId, } satisfies WorkspaceMigrationForeignTable, }, @@ -430,4 +435,21 @@ export class RemoteTableService { } } } + + private buildReferencedTable( + remoteServer: RemoteServerEntity, + remoteTableInput: RemoteTableInput, + ): ReferencedTable { + switch (remoteServer.foreignDataWrapperType) { + case RemoteServerType.POSTGRES_FDW: + return { + table_name: remoteTableInput.name, + schema_name: remoteServer.schema, + }; + case RemoteServerType.STRIPE_FDW: + return { object: remoteTableInput.name }; + default: + throw new BadRequestException('Foreign data wrapper not supported'); + } + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util.ts index 4b18e072a7da..cdf403490795 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util.ts @@ -7,6 +7,9 @@ export const mapUdtNameToFieldType = (udtName: string): FieldMetadataType => { case 'uuid': return FieldMetadataType.UUID; case 'varchar': + case 'text': + case 'bigint': + case 'int8': return FieldMetadataType.TEXT; case 'bool': return FieldMetadataType.BOOLEAN; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/user-mapping-options.utils.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/types/user-mapping-options.ts similarity index 92% rename from packages/twenty-server/src/engine/metadata-modules/remote-server/utils/user-mapping-options.utils.ts rename to packages/twenty-server/src/engine/metadata-modules/remote-server/types/user-mapping-options.ts index 9a7bc1006968..a48fdee7f5f8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/user-mapping-options.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/types/user-mapping-options.ts @@ -7,7 +7,7 @@ import { IsOptional } from 'class-validator'; export class UserMappingOptions { @IsOptional() @Field(() => String, { nullable: true }) - username: string; + user: string; @IsOptional() @Field(() => String, { nullable: true }) @@ -18,7 +18,7 @@ export class UserMappingOptions { export class UserMappingOptionsUpdateInput { @IsOptional() @Field(() => String, { nullable: true }) - username?: string; + user?: string; @IsOptional() @Field(() => String, { nullable: true }) diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/build-update-remote-server-raw-query.utils.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/build-update-remote-server-raw-query.utils.ts index 89899770ab43..c4189e2537b7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/build-update-remote-server-raw-query.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/build-update-remote-server-raw-query.utils.ts @@ -1,144 +1,106 @@ import { BadRequestException } from '@nestjs/common'; -import { isDefined } from 'class-validator'; - import { + ForeignDataWrapperOptions, RemoteServerEntity, RemoteServerType, } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; -import { UserMappingOptions } from 'src/engine/metadata-modules/remote-server/utils/user-mapping-options.utils'; +import { UserMappingOptions } from 'src/engine/metadata-modules/remote-server/types/user-mapping-options'; export type DeepPartial = { [P in keyof T]?: DeepPartial; }; -const buildUserMappingOptionsQuery = ( - parameters: any[], - parametersPositions: object, - userMappingOptions: DeepPartial, -): string | null => { - const shouldUpdateUserMappingOptionsPassword = isDefined( - userMappingOptions?.password, - ); - - if (shouldUpdateUserMappingOptionsPassword) { - parameters.push(userMappingOptions?.password); - parametersPositions['password'] = parameters.length; +export const buildUpdateRemoteServerRawQuery = ( + remoteServerToUpdate: DeepPartial> & + Pick, 'workspaceId' | 'id'>, +): [any[], string] => { + const options: string[] = []; + + const [parameters, parametersPositions] = + buildParametersAndPositions(remoteServerToUpdate); + + if (remoteServerToUpdate.userMappingOptions) { + const userMappingOptionsQuery = buildJsonRawQuery( + remoteServerToUpdate.userMappingOptions, + parametersPositions, + 'userMappingOptions', + ); + + options.push(userMappingOptionsQuery); } - const shouldUpdateUserMappingOptionsUsername = isDefined( - userMappingOptions?.username, - ); + if (remoteServerToUpdate.foreignDataWrapperOptions) { + const foreignDataWrapperOptionsQuery = buildJsonRawQuery( + remoteServerToUpdate.foreignDataWrapperOptions, + parametersPositions, + 'foreignDataWrapperOptions', + ); - if (shouldUpdateUserMappingOptionsUsername) { - parameters.push(userMappingOptions?.username); - parametersPositions['username'] = parameters.length; + options.push(foreignDataWrapperOptionsQuery); } - if ( - shouldUpdateUserMappingOptionsPassword || - shouldUpdateUserMappingOptionsUsername - ) { - return `"userMappingOptions" = jsonb_set(${ - shouldUpdateUserMappingOptionsPassword && - shouldUpdateUserMappingOptionsUsername - ? `jsonb_set( - "userMappingOptions", - '{username}', - to_jsonb($${parametersPositions['username']}::text) - ), - '{password}', - to_jsonb($${parametersPositions['password']}::text) - ` - : shouldUpdateUserMappingOptionsPassword - ? `"userMappingOptions", - '{password}', - to_jsonb($${parametersPositions['password']}::text) - ` - : `"userMappingOptions", - '{username}', - to_jsonb($${parametersPositions['username']}::text) - ` - })`; + if (options.length < 1) { + throw new BadRequestException('No fields to update'); } - return null; + const rawQuery = `UPDATE metadata."remoteServer" SET ${options.join( + ', ', + )} WHERE "id"= $1 RETURNING *`; + + return [parameters, rawQuery]; }; -// TO DO This only works for postgres_fdw type for now, lets make it more generic when we have a different type -export const updateRemoteServerRawQuery = ( +const buildParametersAndPositions = ( remoteServerToUpdate: DeepPartial> & Pick, 'workspaceId' | 'id'>, -): [any[], string] => { +): [any[], object] => { const parameters: any[] = [remoteServerToUpdate.id]; const parametersPositions = {}; - const options: string[] = []; - if (remoteServerToUpdate.userMappingOptions) { - const userMappingOptionsQuery = buildUserMappingOptionsQuery( - parameters, - parametersPositions, - remoteServerToUpdate.userMappingOptions, + Object.entries(remoteServerToUpdate.userMappingOptions).forEach( + ([key, value]) => { + parameters.push(value); + parametersPositions[key] = parameters.length; + }, ); - - if (userMappingOptionsQuery) options.push(userMappingOptionsQuery); } - const shouldUpdateFdwDbname = isDefined( - remoteServerToUpdate.foreignDataWrapperOptions?.dbname, - ); - - if (shouldUpdateFdwDbname) { - parameters.push(remoteServerToUpdate?.foreignDataWrapperOptions?.dbname); - parametersPositions['dbname'] = parameters.length; - } - - const shouldUpdateFdwHost = isDefined( - remoteServerToUpdate.foreignDataWrapperOptions?.host, - ); - - if (shouldUpdateFdwHost) { - parameters.push(remoteServerToUpdate?.foreignDataWrapperOptions?.host); - parametersPositions['host'] = parameters.length; + if (remoteServerToUpdate.foreignDataWrapperOptions) { + Object.entries(remoteServerToUpdate.foreignDataWrapperOptions).forEach( + ([key, value]) => { + parameters.push(value); + parametersPositions[key] = parameters.length; + }, + ); } - const shouldUpdateFdwPort = isDefined( - remoteServerToUpdate.foreignDataWrapperOptions?.port, - ); + return [parameters, parametersPositions]; +}; - if (shouldUpdateFdwPort) { - parameters.push(remoteServerToUpdate?.foreignDataWrapperOptions?.port); - parametersPositions['port'] = parameters.length; - } +const buildJsonRawQuery = ( + options: + | Partial + | Partial>, + parametersPositions: object, + objectName: string, +): string => { + const buildJsonSet = ( + opts: + | Partial + | Partial>, + ): string => { + const [[firstKey, _], ...followingOptions] = Object.entries(opts); - if (shouldUpdateFdwDbname || shouldUpdateFdwHost || shouldUpdateFdwPort) { - const fwdOptionsQuery = `"foreignDataWrapperOptions" = jsonb_set(${ - shouldUpdateFdwDbname && shouldUpdateFdwHost && shouldUpdateFdwPort - ? `jsonb_set(jsonb_set("foreignDataWrapperOptions", '{dbname}', to_jsonb($${parametersPositions['dbname']}::text)), '{host}', to_jsonb($${parametersPositions['host']}::text)), '{port}', to_jsonb($${parametersPositions['port']}::text)` - : shouldUpdateFdwDbname && shouldUpdateFdwHost - ? `jsonb_set("foreignDataWrapperOptions", '{dbname}', to_jsonb($${parametersPositions['dbname']}::text)), '{host}', to_jsonb($${parametersPositions['host']}::text)` - : shouldUpdateFdwDbname && shouldUpdateFdwPort - ? `jsonb_set("foreignDataWrapperOptions", '{dbname}', to_jsonb($${parametersPositions['dbname']}::text)), '{port}', to_jsonb($${parametersPositions['port']}::text)` - : shouldUpdateFdwHost && shouldUpdateFdwPort - ? `jsonb_set("foreignDataWrapperOptions", '{host}', to_jsonb($${parametersPositions['host']}::text)), '{port}', to_jsonb($${parametersPositions['port']}::text)` - : shouldUpdateFdwDbname - ? `"foreignDataWrapperOptions", '{dbname}', to_jsonb($${parametersPositions['dbname']}::text)` - : shouldUpdateFdwHost - ? `"foreignDataWrapperOptions", '{host}', to_jsonb($${parametersPositions['host']}::text)` - : `"foreignDataWrapperOptions", '{port}', to_jsonb($${parametersPositions['port']}::text)` - })`; - - options.push(fwdOptionsQuery); - } + let query = `jsonb_set("${objectName}", '{${firstKey}}', to_jsonb($${parametersPositions[firstKey]}::text))`; - if (options.length < 1) { - throw new BadRequestException('No fields to update'); - } + followingOptions.forEach(([key, _]) => { + query = `jsonb_set(${query}, '{${key}}', to_jsonb($${parametersPositions[key]}::text))`; + }); - const rawQuery = `UPDATE metadata."remoteServer" SET ${options.join( - ', ', - )} WHERE "id"= $1 RETURNING *`; + return query; + }; - return [parameters, rawQuery]; + return `"${objectName}" = ${buildJsonSet(options)}`; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-type.util.ts index 36921e2d1a3c..7a158e6a7a36 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-type.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-type.util.ts @@ -32,6 +32,8 @@ const getFeatureFlagKey = (remoteServerType: RemoteServerType) => { switch (remoteServerType) { case RemoteServerType.POSTGRES_FDW: return FeatureFlagKeys.IsPostgreSQLIntegrationEnabled; + case RemoteServerType.STRIPE_FDW: + return FeatureFlagKeys.IsStripeIntegrationEnabled; default: throw new BadRequestException( `Type ${remoteServerType} is not supported.`, diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts index ad8068883a33..a524f1d98a82 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts @@ -67,10 +67,20 @@ export type WorkspaceMigrationForeignColumnDefinition = distantColumnName: string; }; +type ReferencedObject = { + object: string; +}; + +type ReferencedTableWithSchema = { + table_name: string; + schema_name: string; +}; + +export type ReferencedTable = ReferencedObject | ReferencedTableWithSchema; + export type WorkspaceMigrationForeignTable = { columns: WorkspaceMigrationForeignColumnDefinition[]; - referencedTableName: string; - referencedTableSchema: string; + referencedTable: ReferencedObject | ReferencedTableWithSchema; foreignDataWrapperId: string; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts index 2f09916252ef..074bcae2ad22 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts @@ -495,8 +495,14 @@ export class WorkspaceMigrationRunnerService { ) .join(', '); + let serverOptions = ''; + + Object.entries(foreignTable.referencedTable).forEach(([key, value]) => { + serverOptions += ` ${key} '${value}'`; + }); + await queryRunner.query( - `CREATE FOREIGN TABLE ${schemaName}."${name}" (${foreignTableColumns}) SERVER "${foreignTable.foreignDataWrapperId}" OPTIONS (schema_name '${foreignTable.referencedTableSchema}', table_name '${foreignTable.referencedTableName}')`, + `CREATE FOREIGN TABLE ${schemaName}."${name}" (${foreignTableColumns}) SERVER "${foreignTable.foreignDataWrapperId}" OPTIONS (${serverOptions})`, ); await queryRunner.query(` diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command.ts index d1d6d3ef9529..e34269b72e7a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command.ts @@ -59,6 +59,7 @@ export class AddStandardIdCommand extends CommandRunner { IS_AIRTABLE_INTEGRATION_ENABLED: true, IS_POSTGRESQL_INTEGRATION_ENABLED: true, IS_MULTI_SELECT_ENABLED: false, + IS_STRIPE_INTEGRATION_ENABLED: false, }, ); const standardFieldMetadataCollection = this.standardFieldFactory.create( @@ -74,6 +75,7 @@ export class AddStandardIdCommand extends CommandRunner { IS_AIRTABLE_INTEGRATION_ENABLED: true, IS_POSTGRESQL_INTEGRATION_ENABLED: true, IS_MULTI_SELECT_ENABLED: false, + IS_STRIPE_INTEGRATION_ENABLED: false, }, ); From fe758e193f2fe0a6e5d35e671d5bf0892e8390ac Mon Sep 17 00:00:00 2001 From: Weiko Date: Thu, 2 May 2024 17:36:57 +0200 Subject: [PATCH 10/32] fix workspace-member deletion with existing attachments/documents (#5232) ## Context We have a non-nullable constraint on authorId in attachments and documents, until we have soft-deletion we need to handle deletion of workspace-members and their attachments/documents. This PR introduces pre-hooks to deleteOne/deleteMany This is called when a user deletes a workspace-member from the members page Next: needs to be done on user level as well. This is called when users try to delete their own accounts. I've seen other issues such as re-creating a user with a previously used email failing. --- .../workspace-pre-query-hook.config.ts | 6 +++ .../workspace-pre-query-hook.module.ts | 2 + .../workspace-query-runner.service.ts | 16 ++++++++ .../metadata-to-repository.mapping.ts | 4 ++ .../utils/global-exception-handler.util.ts | 2 + .../src/engine/utils/graphql-errors.util.ts | 8 ++++ .../repositories/comment.repository.ts | 21 ++++++++++ .../repositories/attachment.repository.ts | 21 ++++++++++ .../blocklist-update-many.pre-query.hook.ts | 4 +- .../message-find-one.pre-query-hook.ts | 4 +- ...space-member-delete-many.pre-query.hook.ts | 14 +++++++ ...kspace-member-delete-one.pre-query.hook.ts | 41 +++++++++++++++++++ .../workspace-member-query-hook.module.ts | 27 ++++++++++++ 13 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 packages/twenty-server/src/modules/activity/repositories/comment.repository.ts create mode 100644 packages/twenty-server/src/modules/attachment/repositories/attachment.repository.ts create mode 100644 packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook.ts create mode 100644 packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook.ts create mode 100644 packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.config.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.config.ts index 0a5d8d9cac6d..b0bc2bc88f3b 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.config.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.config.ts @@ -6,6 +6,8 @@ import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/query-hoo import { BlocklistCreateManyPreQueryHook } from 'src/modules/connected-account/query-hooks/blocklist/blocklist-create-many.pre-query.hook'; import { BlocklistUpdateManyPreQueryHook } from 'src/modules/connected-account/query-hooks/blocklist/blocklist-update-many.pre-query.hook'; import { BlocklistUpdateOnePreQueryHook } from 'src/modules/connected-account/query-hooks/blocklist/blocklist-update-one.pre-query.hook'; +import { WorkspaceMemberDeleteOnePreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook'; +import { WorkspaceMemberDeleteManyPreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook'; // TODO: move to a decorator export const workspacePreQueryHooks: WorkspaceQueryHook = { @@ -22,4 +24,8 @@ export const workspacePreQueryHooks: WorkspaceQueryHook = { updateMany: [BlocklistUpdateManyPreQueryHook.name], updateOne: [BlocklistUpdateOnePreQueryHook.name], }, + workspaceMember: { + deleteOne: [WorkspaceMemberDeleteOnePreQueryHook.name], + deleteMany: [WorkspaceMemberDeleteManyPreQueryHook.name], + }, }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module.ts index fa718b6fef46..7c367e7b2e3a 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.module.ts @@ -4,12 +4,14 @@ import { MessagingQueryHookModule } from 'src/modules/messaging/query-hooks/mess import { WorkspacePreQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service'; import { CalendarQueryHookModule } from 'src/modules/calendar/query-hooks/calendar-query-hook.module'; import { ConnectedAccountQueryHookModule } from 'src/modules/connected-account/query-hooks/connected-account-query-hook.module'; +import { WorkspaceMemberQueryHookModule } from 'src/modules/workspace-member/query-hooks/workspace-member-query-hook.module'; @Module({ imports: [ MessagingQueryHookModule, CalendarQueryHookModule, ConnectedAccountQueryHookModule, + WorkspaceMemberQueryHookModule, ], providers: [WorkspacePreQueryHookService], exports: [WorkspacePreQueryHookService], diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts index 630c4a0f5dba..8fbee2a95f47 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts @@ -436,6 +436,14 @@ export class WorkspaceQueryRunnerService { atMost: maximumRecordAffected, }); + await this.workspacePreQueryHookService.executePreHooks( + userId, + workspaceId, + objectMetadataItem.nameSingular, + 'deleteMany', + args, + ); + const result = await this.execute(query, workspaceId); const parsedResults = ( @@ -495,6 +503,14 @@ export class WorkspaceQueryRunnerService { ); // TODO END + await this.workspacePreQueryHookService.executePreHooks( + userId, + workspaceId, + objectMetadataItem.nameSingular, + 'deleteOne', + args, + ); + const result = await this.execute(query, workspaceId); const parsedResults = ( diff --git a/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts b/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts index fc5c95188153..9974aceade39 100644 --- a/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts +++ b/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts @@ -14,6 +14,8 @@ import { MessageThreadRepository } from 'src/modules/messaging/repositories/mess import { MessageRepository } from 'src/modules/messaging/repositories/message.repository'; import { PersonRepository } from 'src/modules/person/repositories/person.repository'; import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository'; +import { AttachmentRepository } from 'src/modules/attachment/repositories/attachment.repository'; +import { CommentRepository } from 'src/modules/activity/repositories/comment.repository'; export const metadataToRepositoryMapping = { AuditLogObjectMetadata: AuditLogRepository, @@ -34,4 +36,6 @@ export const metadataToRepositoryMapping = { PersonObjectMetadata: PersonRepository, TimelineActivityObjectMetadata: TimelineActivityRepository, WorkspaceMemberObjectMetadata: WorkspaceMemberRepository, + AttachmentObjectMetadata: AttachmentRepository, + CommentObjectMetadata: CommentRepository, }; diff --git a/packages/twenty-server/src/engine/utils/global-exception-handler.util.ts b/packages/twenty-server/src/engine/utils/global-exception-handler.util.ts index 12f32656f86c..4a6dac8d81b3 100644 --- a/packages/twenty-server/src/engine/utils/global-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/utils/global-exception-handler.util.ts @@ -9,6 +9,7 @@ import { ValidationError, NotFoundError, ConflictError, + MethodNotAllowedError, } from 'src/engine/utils/graphql-errors.util'; import { ExceptionHandlerService } from 'src/engine/integrations/exception-handler/exception-handler.service'; @@ -17,6 +18,7 @@ const graphQLPredefinedExceptions = { 401: AuthenticationError, 403: ForbiddenError, 404: NotFoundError, + 405: MethodNotAllowedError, 409: ConflictError, }; diff --git a/packages/twenty-server/src/engine/utils/graphql-errors.util.ts b/packages/twenty-server/src/engine/utils/graphql-errors.util.ts index 069a03ee356f..22733566dee7 100644 --- a/packages/twenty-server/src/engine/utils/graphql-errors.util.ts +++ b/packages/twenty-server/src/engine/utils/graphql-errors.util.ts @@ -142,6 +142,14 @@ export class NotFoundError extends BaseGraphQLError { } } +export class MethodNotAllowedError extends BaseGraphQLError { + constructor(message: string, extensions?: Record) { + super(message, 'METHOD_NOT_ALLOWED', extensions); + + Object.defineProperty(this, 'name', { value: 'MethodNotAllowedError' }); + } +} + export class ConflictError extends BaseGraphQLError { constructor(message: string, extensions?: Record) { super(message, 'CONFLICT', extensions); diff --git a/packages/twenty-server/src/modules/activity/repositories/comment.repository.ts b/packages/twenty-server/src/modules/activity/repositories/comment.repository.ts new file mode 100644 index 000000000000..e6bf07b2b8a5 --- /dev/null +++ b/packages/twenty-server/src/modules/activity/repositories/comment.repository.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; + +@Injectable() +export class CommentRepository { + constructor( + private readonly workspaceDataSourceService: WorkspaceDataSourceService, + ) {} + + async deleteByAuthorId(authorId: string, workspaceId: string): Promise { + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + await this.workspaceDataSourceService.executeRawQuery( + `DELETE FROM ${dataSourceSchema}."comment" WHERE "authorId" = $1`, + [authorId], + workspaceId, + ); + } +} diff --git a/packages/twenty-server/src/modules/attachment/repositories/attachment.repository.ts b/packages/twenty-server/src/modules/attachment/repositories/attachment.repository.ts new file mode 100644 index 000000000000..2d340d2cba0d --- /dev/null +++ b/packages/twenty-server/src/modules/attachment/repositories/attachment.repository.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; + +@Injectable() +export class AttachmentRepository { + constructor( + private readonly workspaceDataSourceService: WorkspaceDataSourceService, + ) {} + + async deleteByAuthorId(authorId: string, workspaceId: string): Promise { + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + await this.workspaceDataSourceService.executeRawQuery( + `DELETE FROM ${dataSourceSchema}."attachment" WHERE "authorId" = $1`, + [authorId], + workspaceId, + ); + } +} diff --git a/packages/twenty-server/src/modules/connected-account/query-hooks/blocklist/blocklist-update-many.pre-query.hook.ts b/packages/twenty-server/src/modules/connected-account/query-hooks/blocklist/blocklist-update-many.pre-query.hook.ts index b36ff3e967fb..1ce12a5350df 100644 --- a/packages/twenty-server/src/modules/connected-account/query-hooks/blocklist/blocklist-update-many.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/connected-account/query-hooks/blocklist/blocklist-update-many.pre-query.hook.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, MethodNotAllowedException } from '@nestjs/common'; import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; @@ -7,6 +7,6 @@ export class BlocklistUpdateManyPreQueryHook implements WorkspacePreQueryHook { constructor() {} async execute(): Promise { - throw new Error('Method not implemented.'); + throw new MethodNotAllowedException('Method not allowed.'); } } diff --git a/packages/twenty-server/src/modules/messaging/query-hooks/message/message-find-one.pre-query-hook.ts b/packages/twenty-server/src/modules/messaging/query-hooks/message/message-find-one.pre-query-hook.ts index fb8585815d7c..c5ef241588cf 100644 --- a/packages/twenty-server/src/modules/messaging/query-hooks/message/message-find-one.pre-query-hook.ts +++ b/packages/twenty-server/src/modules/messaging/query-hooks/message/message-find-one.pre-query-hook.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable, MethodNotAllowedException } from '@nestjs/common'; import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; @@ -11,6 +11,6 @@ export class MessageFindOnePreQueryHook implements WorkspacePreQueryHook { _workspaceId: string, _payload: FindOneResolverArgs, ): Promise { - throw new BadRequestException('Method not implemented.'); + throw new MethodNotAllowedException('Method not allowed.'); } } diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook.ts new file mode 100644 index 000000000000..e0650b7e2548 --- /dev/null +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook.ts @@ -0,0 +1,14 @@ +import { Injectable, MethodNotAllowedException } from '@nestjs/common'; + +import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; + +@Injectable() +export class WorkspaceMemberDeleteManyPreQueryHook + implements WorkspacePreQueryHook +{ + constructor() {} + + async execute(): Promise { + throw new MethodNotAllowedException('Method not allowed.'); + } +} diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook.ts new file mode 100644 index 000000000000..637686747fe8 --- /dev/null +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface'; +import { DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; + +import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; +import { CommentRepository } from 'src/modules/activity/repositories/comment.repository'; +import { CommentObjectMetadata } from 'src/modules/activity/standard-objects/comment.object-metadata'; +import { AttachmentRepository } from 'src/modules/attachment/repositories/attachment.repository'; +import { AttachmentObjectMetadata } from 'src/modules/attachment/standard-objects/attachment.object-metadata'; + +@Injectable() +export class WorkspaceMemberDeleteOnePreQueryHook + implements WorkspacePreQueryHook +{ + constructor( + @InjectObjectMetadataRepository(AttachmentObjectMetadata) + private readonly attachmentRepository: AttachmentRepository, + @InjectObjectMetadataRepository(CommentObjectMetadata) + private readonly commentRepository: CommentRepository, + ) {} + + // There is no need to validate the user's access to the workspace member since we don't have permission yet. + async execute( + userId: string, + workspaceId: string, + payload: DeleteOneResolverArgs, + ): Promise { + const workspaceMemberId = payload.id; + + await this.attachmentRepository.deleteByAuthorId( + workspaceMemberId, + workspaceId, + ); + + await this.commentRepository.deleteByAuthorId( + workspaceMemberId, + workspaceId, + ); + } +} diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts new file mode 100644 index 000000000000..ce3496541fd4 --- /dev/null +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts @@ -0,0 +1,27 @@ +import { Module } from '@nestjs/common'; + +import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; +import { CommentObjectMetadata } from 'src/modules/activity/standard-objects/comment.object-metadata'; +import { AttachmentObjectMetadata } from 'src/modules/attachment/standard-objects/attachment.object-metadata'; +import { WorkspaceMemberDeleteManyPreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook'; +import { WorkspaceMemberDeleteOnePreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook'; + +@Module({ + imports: [ + ObjectMetadataRepositoryModule.forFeature([ + AttachmentObjectMetadata, + CommentObjectMetadata, + ]), + ], + providers: [ + { + provide: WorkspaceMemberDeleteOnePreQueryHook.name, + useClass: WorkspaceMemberDeleteOnePreQueryHook, + }, + { + provide: WorkspaceMemberDeleteManyPreQueryHook.name, + useClass: WorkspaceMemberDeleteManyPreQueryHook, + }, + ], +}) +export class WorkspaceMemberQueryHookModule {} From 1430a6745c3dfcdf9d0f76d5ab70326d537482e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Fri, 3 May 2024 09:38:03 +0200 Subject: [PATCH 11/32] Quick job update (#5265) --- .../src/content/jobs/senior-software-eng.md | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/twenty-website/src/content/jobs/senior-software-eng.md b/packages/twenty-website/src/content/jobs/senior-software-eng.md index c8d60fbd67e2..04cf16537371 100644 --- a/packages/twenty-website/src/content/jobs/senior-software-eng.md +++ b/packages/twenty-website/src/content/jobs/senior-software-eng.md @@ -1,10 +1,27 @@ # Senior Software Engineer -📮 **Apply:** Send LinkedIn or resume + a few words, by email to founders(at)twenty.com +📮 **Apply:** Send LinkedIn or resume + a few words to founders(at)twenty.com 📍 **Location:** Paris, France (on-site or hybrid, no full remote) 💰 **Salary range:** €65k-75k + 0,2%-0,3% equity 🏝️ **Benefits:** ~35 PTO days + 100% health insurance + 50% transportation +## Role + +**You:** +- 5+ years experience in a startup or leading tech company +- Open to working full-stack, with a strong expertise on either side +- Care about Open Source, enjoys interacting with the community +- Entrepreneurial and owner mindset + +**Example tasks:** +- Designing and implementing a permission system that support row-level and column-level conditions + that's highly efficient + intuitive for users +- Designing and implementing a mechanism for people to build full-fledge apps that would still run securely in our multi-tenant cloud +- Designing and implementing a cross-platform layout builder to let users create their own page layout + +**Stack:** +- Backend: Typescript, Node, NestJS, GraphQL +- Frontend: Typescript, React, Apollo +- AWS, Pulumi, Postgres, Redis ## Product @@ -16,33 +33,18 @@ We value universal principles and common patterns over feature lists. And Open S ## Company -Twenty is an Open Source and Public Benefits Company. +Twenty is an Open Source + Public Benefits Company. - One of the fastest growing open source project ever launched with 200+ contributors and 10k+ stars in less than 12 months - Backed by YC and some of the best investors/founders in the world -- Operating in a the biggest software market (crm). +- Operating in a the biggest market in software (CRM) - Engineering-driven culture, with a passion for craft and quality -Android, Wordpress or VScode all lead their respective market by a large margin not because they came first but because Open Source is the best second-mover advantage in large markets. +Android, WordPress or VSCode all lead their respective market because being Open Source is the best second-mover advantage. Our bet is that the same shift will happen to the CRM Market. And given that we're #1, we should be in a good position to take the lead. But the road ahead will be long and steep! -We're betting that one day a large Open Source software will have greater usage than all proprietary CRMs combined. Given that we're #1, we're in a good position to take that lead. But the road ahead will be long and steep! +## Apply -## Role - -**You:** -- 5+ years experience in a startup or leading tech company -- Open to working full-stack, with a strong expertise on either side -- Care about Open Source, enjoys interacting with the community -- Entrepreneurial and owner mindset - -**Example tasks:** -- Designing and implementing a permission system that support row-level and column-level conditions + that's highly efficient + intuitive for users -- Designing and implementing a mechanism for people to build full-fledge apps that would still run securely in our multi-tenant cloud -- Designing and implementing a cross-platform layout builder to let users create their own page layout - -**Stack:** -- Backend: Typescript, Node, NestJS, GraphQL -- Frontend: Typescript, React, Apollo -- AWS, Pulumi, Postgres, Redis +Send LinkedIn or resume + a few words, by email to founders(at)twenty.com +We reply quickly and to all candidates. If we think it could be a fit, we'll reply by email with a link for you to book a 30 minutes interview (mostly non-technical), eventually followed by 2 hours technical interview (coding + architecture). From 30ffe0160e51e34359561c19f3cdc9961753381b Mon Sep 17 00:00:00 2001 From: Weiko Date: Fri, 3 May 2024 10:30:47 +0200 Subject: [PATCH 12/32] Fix token validation on graphql IntrospectionQuery (#5255) ## Context We recently introduced a change that now throws a 401 if the token is invalid or expired. The first implementation is using an allow list and 'IntrospectionQuery' was missing so the playground was broken. The check has been updated and we now only check the excludedOperations list if a token is not present. This is because some operations can be both used as loggedIn and loggedOut so we want to validate the token for those sometimes (and set the workspace, user, cache version, etc). Still not a very clean solution imho. --- .../src/engine/middlewares/user-workspace.middleware.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/twenty-server/src/engine/middlewares/user-workspace.middleware.ts b/packages/twenty-server/src/engine/middlewares/user-workspace.middleware.ts index a45c6f6fce56..eb7f36b83287 100644 --- a/packages/twenty-server/src/engine/middlewares/user-workspace.middleware.ts +++ b/packages/twenty-server/src/engine/middlewares/user-workspace.middleware.ts @@ -14,6 +14,7 @@ export class UserWorkspaceMiddleware implements NestMiddleware { async use(req: Request, res: Response, next: NextFunction) { const body = req.body; + const excludedOperations = [ 'GetClientConfig', 'GetCurrentUser', @@ -24,12 +25,12 @@ export class UserWorkspaceMiddleware implements NestMiddleware { 'Verify', 'SignUp', 'RenewToken', + 'IntrospectionQuery', ]; if ( - body && - body.operationName && - excludedOperations.includes(body.operationName) + !this.tokenService.isTokenPresent(req) && + (!body?.operationName || excludedOperations.includes(body.operationName)) ) { return next(); } From 50421863d4b3b57e824761f692c5864f8e389c51 Mon Sep 17 00:00:00 2001 From: Weiko Date: Fri, 3 May 2024 14:52:20 +0200 Subject: [PATCH 13/32] Fix filter transform with logic operators (#5269) Various fixes - Remote objects are read-only for now, we already hide and block most of the write actions but the button that allows you to add a new record in an empty collection was still visible. - CreatedAt is not mandatory on remote objects (at least for now) so it was breaking the show page, it now checks if createdAt exists and is not null before trying to display the human readable format `Added x days ago` - The filters are overwritten in query-runner-args.factory.ts to handle NUMBER field type, this was only working with filters like ``` { "id": { "in": [ 1 ] } ``` but not with more depth such as ``` "and": [ {}, { "id": { "in": [ 1 ] } } ] ``` - Fixes CREATE FOREIGN TABLE raw query which was missing ",". --- .../components/RecordTableWithWrappers.tsx | 42 +++++++------- .../components/ShowPageSummaryCard.tsx | 6 +- .../factories/query-runner-args.factory.ts | 56 ++++++++++++------- .../workspace-migration-runner.service.ts | 8 +-- 4 files changed, 67 insertions(+), 45 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx index 3431933e4f3d..fd62043add38 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx @@ -81,6 +81,8 @@ export const RecordTableWithWrappers = ({ const objectLabel = foundObjectMetadataItem?.labelSingular; + const isRemote = foundObjectMetadataItem?.isRemote ?? false; + return ( @@ -113,25 +115,27 @@ export const RecordTableWithWrappers = ({ recordTableId={recordTableId} tableBodyRef={tableBodyRef} /> - {!isRecordTableInitialLoading && numberOfTableRows === 0 && ( - - - - - Add your first {objectLabel} - - - Use our API or add your first {objectLabel} manually - - -