diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index b61506709f93..506a6dce0e5e 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -43,7 +43,8 @@ jobs: - name: Front / Build storybook run: npx nx storybook:build twenty-front front-sb-test: - runs-on: ci-8-cores + runs-on: shipfox-8vcpu-ubuntu-2204 + timeout-minutes: 60 needs: front-sb-build strategy: matrix: @@ -68,7 +69,8 @@ jobs: - name: Run storybook tests run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }} front-sb-test-performance: - runs-on: ci-8-cores + runs-on: shipfox-8vcpu-ubuntu-2204 + timeout-minutes: 60 env: REACT_APP_SERVER_BASE_URL: http://localhost:3000 NX_REJECT_UNKNOWN_LOCAL_CACHE: 0 diff --git a/Makefile b/Makefile index 764dbd1531d4..af42f19d9fb0 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,12 @@ -docker-dev-build: - make -C packages/twenty-docker dev-build - -docker-dev-up: - make -C packages/twenty-docker dev-up - -docker-dev-start: - make -C packages/twenty-docker dev-start - -docker-dev-stop: - make -C packages/twenty-docker dev-stop - -docker-dev-sh: - make -C packages/twenty-docker dev-sh - postgres-on-docker: - make -C packages/twenty-postgres provision-on-docker - -postgres-on-macos-arm: - make -C packages/twenty-postgres provision-on-macos-arm - -postgres-on-macos-intel: - make -C packages/twenty-postgres provision-on-macos-intel - -postgres-on-linux: - make -C packages/twenty-postgres provision-on-linux + docker run \ + --name twenty_postgres \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_DB=default \ + -v twenty_db_data:/var/lib/postgresql/data \ + -p 5432:5432 \ + twentycrm/twenty-postgres:latest + +redis-on-docker: + docker run -d --name twenty_redis -p 6379:6379 redis/redis-stack-server:latest \ No newline at end of file diff --git a/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md b/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md index d4f5a4d0ab99..9f1f55ae767e 100644 --- a/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md +++ b/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md @@ -23,4 +23,9 @@ Your turn 馃憞 禄 12-October-2024 by [Ionfinisher](https://oss.gg/Ionfinisher) poster Link: [poster](https://x.com/ion_finisher/status/1845168965963628802) 禄 14-October-2024 by [AliYar-Khan](https://oss.gg/AliYar-Khan) poster Link: [poster](https://x.com/Mr_Programmer14/status/1845888855183884352) + +禄 16-October-2024 by [Harsh BHat](https://oss.gg/harshsbhat) poster Link: [poster](https://x.com/HarshBhatX/status/1846233330435477531) + +禄 17-October-2024 by [Atharva Deshmukh](https://oss.gg/Atharva-3000) poster Link: [poster](https://x.com/0x_atharva/status/1846915861191577697) + --- diff --git a/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md b/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md index ceee0fa8e4da..d67c49b64154 100644 --- a/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md +++ b/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md @@ -24,5 +24,9 @@ Your turn 馃憞 禄 13-October-2024 by [Ionfinisher](https://oss.gg/Ionfinisher) Logo Link: [logo](https://drive.google.com/file/d/1l9vE8CIjW9KfdioI5WKzxrdmvO8LR4j7/view?usp=drive_link) 禄 tweet Link: [tweet](https://x.com/ion_finisher/status/1845466470429442163) +禄 16-October-2024 by [harshsbhat](https://oss.gg/harshsbhat) Logo Link: [logo](https://drive.google.com/file/d/1jmqwNvlSyWSY1-pCG63TAtDvCoVa8xg-/view?usp=sharing) 禄 tweet Link: [tweet](https://x.com/HarshBhatX/status/1846234658712772977) + +禄 17-October-2024 by [shlok-py](https://oss.gg/shlok-py) Logo Link: [logo](https://drive.google.com/file/d/1BakHRLJul6DcNbLyeOXgJO9Ap4DpUxO9/view?usp=sharing) 禄 tweet Link: [tweet](https://x.com/koirala_shlok/status/1846910669658247201) + --- diff --git a/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md b/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md index 168e311b9a32..a0e1421bfd99 100644 --- a/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md +++ b/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md @@ -43,3 +43,6 @@ Your turn 馃憞 禄 13-October-2024 by Yash Parmar 禄 Link to Tweet: https://x.com/yashp3020/status/1845720834716959009 + +禄 16-October-2024 by Harsh Bhat +禄 Link to Tweet: https://x.com/HarshBhatX/status/1846252536241508392 diff --git a/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md b/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md index 508210ae5a6e..07d2e067c708 100644 --- a/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md +++ b/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md @@ -25,4 +25,7 @@ Your turn 馃憞 禄 13-October-2024 by Ali Yar Khan 禄 Link to Tweet: https://x.com/Mr_Programmer14/status/1845530448245711197 + +禄 16-October-2024 by Harsh Bhat +禄 Link to Tweet: https://x.com/HarshBhatX/status/1846075312691413066 --- diff --git a/oss-gg/twenty-side-quest/4-meme-magic.md b/oss-gg/twenty-side-quest/4-meme-magic.md index b5cb3263d218..041c73ef081e 100644 --- a/oss-gg/twenty-side-quest/4-meme-magic.md +++ b/oss-gg/twenty-side-quest/4-meme-magic.md @@ -31,4 +31,7 @@ Your turn 馃憞 禄 14-October-2024 by Yash Parmar 禄 Link to Tweet: [https://x.com/yashp3020/status/1845108226527994222](https://x.com/yashp3020/status/1845720142702842093) + +禄 16-October-2024 by Harsh Bhat +禄 Link to Tweet: https://x.com/HarshBhatX/status/1844698253104709899 --- diff --git a/oss-gg/twenty-side-quest/5-gif-magic.md b/oss-gg/twenty-side-quest/5-gif-magic.md index 20467fef4784..320ffa9015db 100644 --- a/oss-gg/twenty-side-quest/5-gif-magic.md +++ b/oss-gg/twenty-side-quest/5-gif-magic.md @@ -29,5 +29,9 @@ Your turn 馃憞 禄 13-October-2024 by Nabhag Motivaras 禄 Link to gif: https://giphy.com/gifs/twenty-twentycrm-opensourcecrm-wCcsmnJuzzzGrfuf9B +禄 15-October-2024 by Ali Yar Khan +禄 Link to gif: https://giphy.com/gifs/Q3f7T107wSsMJlT7aj +禄 16-October-2024 by Harsh Bhat +禄 Link to gif: https://giphy.com/gifs/oss-twentycrm-mgoYSDrjIalUL7XJzm --- diff --git a/package.json b/package.json index a4dc90df92ac..f86d4c1b9d08 100644 --- a/package.json +++ b/package.json @@ -347,7 +347,7 @@ "version": "0.2.1", "nx": {}, "scripts": { - "start": "npx nx run-many -t start -p twenty-server twenty-front" + "start": "npx nx run-many -t start worker -p twenty-server twenty-front" }, "workspaces": { "packages": [ diff --git a/packages/twenty-docker/.env.example b/packages/twenty-docker/.env.example index 59d8d03f93a7..c1a7a9d3bae8 100644 --- a/packages/twenty-docker/.env.example +++ b/packages/twenty-docker/.env.example @@ -3,10 +3,9 @@ TAG=latest # POSTGRES_ADMIN_PASSWORD=replace_me_with_a_strong_password PG_DATABASE_HOST=db:5432 +REDIS_URL=redis://redis:6379 SERVER_URL=http://localhost:3000 -# REDIS_HOST=redis -# REDIS_PORT=6379 # Use openssl rand -base64 32 for each secret # ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access diff --git a/packages/twenty-docker/docker-compose.yml b/packages/twenty-docker/docker-compose.yml index b2efc1a168e4..8800f4f3f3b9 100644 --- a/packages/twenty-docker/docker-compose.yml +++ b/packages/twenty-docker/docker-compose.yml @@ -25,8 +25,7 @@ services: PG_DATABASE_URL: postgres://twenty:twenty@${PG_DATABASE_HOST}/default SERVER_URL: ${SERVER_URL} FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL} - REDIS_PORT: ${REDIS_PORT:-6379} - REDIS_HOST: ${REDIS_HOST:-redis} + REDIS_URL: ${REDIS_URL:-redis://localhost:6379} ENABLE_DB_MIGRATIONS: "true" @@ -59,8 +58,7 @@ services: PG_DATABASE_URL: postgres://twenty:twenty@${PG_DATABASE_HOST}/default SERVER_URL: ${SERVER_URL} FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL} - REDIS_PORT: ${REDIS_PORT:-6379} - REDIS_HOST: ${REDIS_HOST:-redis} + REDIS_URL: ${REDIS_URL:-redis://localhost:6379} ENABLE_DB_MIGRATIONS: "false" # it already runs on the server diff --git a/packages/twenty-docker/k8s/manifests/deployment-server.yaml b/packages/twenty-docker/k8s/manifests/deployment-server.yaml index b1229d649bbb..99e5c60132ed 100644 --- a/packages/twenty-docker/k8s/manifests/deployment-server.yaml +++ b/packages/twenty-docker/k8s/manifests/deployment-server.yaml @@ -41,10 +41,8 @@ spec: value: "https://crm.example.com:443" - name: "PG_DATABASE_URL" value: "postgres://twenty:twenty@twenty-db.twentycrm.svc.cluster.local/default" - - name: "REDIS_HOST" - value: "twentycrm-redis.twentycrm.svc.cluster.local" - - name: "REDIS_PORT" - value: 6379 + - name: "REDIS_URL" + value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379" - name: ENABLE_DB_MIGRATIONS value: "true" - name: SIGN_IN_PREFILLED diff --git a/packages/twenty-docker/k8s/manifests/deployment-worker.yaml b/packages/twenty-docker/k8s/manifests/deployment-worker.yaml index b3a7e07a19aa..92d0322e5930 100644 --- a/packages/twenty-docker/k8s/manifests/deployment-worker.yaml +++ b/packages/twenty-docker/k8s/manifests/deployment-worker.yaml @@ -40,10 +40,8 @@ spec: value: "bull-mq" - name: "CACHE_STORAGE_TYPE" value: "redis" - - name: "REDIS_HOST" - value: "twentycrm-redis.twentycrm.svc.cluster.local" - - name: "REDIS_PORT" - value: 6379 + - name: "REDIS_URL" + value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379" - name: ACCESS_TOKEN_SECRET valueFrom: secretKeyRef: diff --git a/packages/twenty-docker/k8s/terraform/deployment-server.tf b/packages/twenty-docker/k8s/terraform/deployment-server.tf index 1868b17624da..0f643f5c6d80 100644 --- a/packages/twenty-docker/k8s/terraform/deployment-server.tf +++ b/packages/twenty-docker/k8s/terraform/deployment-server.tf @@ -61,12 +61,8 @@ resource "kubernetes_deployment" "twentycrm_server" { value = "postgres://twenty:${var.twentycrm_pgdb_admin_password}@${kubernetes_service.twentycrm_db.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local/default" } env { - name = "REDIS_HOST" - value = "${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local" - } - env { - name = "REDIS_PORT" - value = 6379 + name = "REDIS_URL" + value = "redis://${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local:6379" } env { name = "ENABLE_DB_MIGRATIONS" diff --git a/packages/twenty-docker/k8s/terraform/deployment-worker.tf b/packages/twenty-docker/k8s/terraform/deployment-worker.tf index 78e5ea6dcc1d..163f02c4977e 100644 --- a/packages/twenty-docker/k8s/terraform/deployment-worker.tf +++ b/packages/twenty-docker/k8s/terraform/deployment-worker.tf @@ -59,13 +59,8 @@ resource "kubernetes_deployment" "twentycrm_worker" { } env { - name = "REDIS_HOST" - value = "${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local" - } - - env { - name = "REDIS_PORT" - value = 6379 + name = "REDIS_URL" + value = "redis://${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local:6379" } env { diff --git a/packages/twenty-docker/twenty-postgres-spilo/Dockerfile b/packages/twenty-docker/twenty-postgres-spilo/Dockerfile index 21e107c477bb..a87a8a97ec12 100644 --- a/packages/twenty-docker/twenty-postgres-spilo/Dockerfile +++ b/packages/twenty-docker/twenty-postgres-spilo/Dockerfile @@ -1,6 +1,5 @@ ARG POSTGRES_VERSION=15 ARG SPILO_VERSION=3.2-p1 -ARG PG_GRAPHQL_VERSION=1.5.6 ARG WRAPPERS_VERSION=0.2.0 # Build the mysql_fdw extension @@ -38,10 +37,9 @@ WORKDIR /build/openssl RUN ./config && make && make install -# Extend the Spilo image with the pg_graphql and mysql_fdw extensions +# Extend the Spilo image with the mysql_fdw extensions FROM ghcr.io/zalando/spilo-${POSTGRES_VERSION}:${SPILO_VERSION} ARG POSTGRES_VERSION -ARG PG_GRAPHQL_VERSION ARG WRAPPERS_VERSION ARG TARGETARCH @@ -63,14 +61,6 @@ RUN curl -L "https://github.com/supabase/wrappers/releases/download/v${WRAPPERS_ COPY --from=build-libssl /usr/local/lib/libssl* /usr/local/lib/libcrypto* /usr/lib/ COPY --from=build-libssl /usr/local/lib/engines-1.1 /usr/lib/engines-1.1 -# Copy pg_graphql -COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${POSTGRES_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql--${PG_GRAPHQL_VERSION}.sql \ - /usr/share/postgresql/${POSTGRES_VERSION}/extension -COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${POSTGRES_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.control \ - /usr/share/postgresql/${POSTGRES_VERSION}/extension -COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${POSTGRES_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.so \ - /usr/lib/postgresql/${POSTGRES_VERSION}/lib/pg_graphql.so - # Copy mysql_fdw COPY --from=build-mysql_fdw /mysql_fdw/mysql_fdw.so \ /usr/lib/postgresql/${POSTGRES_VERSION}/lib/mysql_fdw.so diff --git a/packages/twenty-docker/twenty-postgres/Dockerfile b/packages/twenty-docker/twenty-postgres/Dockerfile index 5647a6cd35ac..9c9b96398e66 100644 --- a/packages/twenty-docker/twenty-postgres/Dockerfile +++ b/packages/twenty-docker/twenty-postgres/Dockerfile @@ -3,7 +3,6 @@ ARG IMAGE_TAG='15.5.0-debian-11-r15' FROM bitnami/postgresql:${IMAGE_TAG} ARG PG_MAIN_VERSION=15 -ARG PG_GRAPHQL_VERSION=1.5.6 ARG WRAPPERS_VERSION=0.2.0 ARG TARGETARCH @@ -26,14 +25,6 @@ RUN set -eux; \ RUN apt update && apt install build-essential git curl default-libmysqlclient-dev -y -# Install precompiled pg_graphql extensions -COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql--${PG_GRAPHQL_VERSION}.sql \ - /opt/bitnami/postgresql/share/extension/ -COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.control \ - /opt/bitnami/postgresql/share/extension/ -COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.so \ - /opt/bitnami/postgresql/lib/ - # Install precompiled supabase wrappers extensions RUN curl -L "https://github.com/supabase/wrappers/releases/download/v${WRAPPERS_VERSION}/wrappers-v${WRAPPERS_VERSION}-pg${PG_MAIN_VERSION}-${TARGETARCH}-linux-gnu.deb" -o wrappers.deb RUN dpkg --install wrappers.deb diff --git a/packages/twenty-docker/twenty-website/Dockerfile b/packages/twenty-docker/twenty-website/Dockerfile new file mode 100644 index 000000000000..e3b7420ff76a --- /dev/null +++ b/packages/twenty-docker/twenty-website/Dockerfile @@ -0,0 +1,29 @@ +FROM node:18.17.1-alpine as twenty-website-build + + +WORKDIR /app + +COPY ./package.json . +COPY ./yarn.lock . +COPY ./.yarnrc.yml . +COPY ./.yarn/releases /app/.yarn/releases +COPY ./tools/eslint-rules /app/tools/eslint-rules +COPY ./packages/twenty-website/package.json /app/packages/twenty-website/package.json + +RUN yarn + +COPY ./packages/twenty-website /app/packages/twenty-website +RUN npx nx build twenty-website + +FROM node:18.17.1-alpine as twenty-website + +WORKDIR /app/packages/twenty-website + +COPY --from=twenty-website-build /app /app + +WORKDIR /app/packages/twenty-website + +LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty +LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the website." + +CMD ["/bin/sh", "-c", "npx nx start"] \ No newline at end of file diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 5950c81d5cc1..7f053fc6b10f 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -141,6 +141,7 @@ export enum CaptchaDriverType { export type ClientConfig = { __typename?: 'ClientConfig'; + analyticsEnabled: Scalars['Boolean']; api: ApiConfig; authProviders: AuthProviders; billing: Billing; @@ -1599,7 +1600,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; -export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; +export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>; @@ -2765,6 +2766,7 @@ export const GetClientConfigDocument = gql` signInPrefilled signUpDisabled debugMode + analyticsEnabled support { supportDriver supportFrontChatId diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx index feeba5aabc61..4b61fa58eadb 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx @@ -4,9 +4,9 @@ import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-action export const SingleRecordActionMenuEntriesSetter = () => { const actionEffects = [ + ManageFavoritesActionEffect, ExportRecordsActionEffect, DeleteRecordsActionEffect, - ManageFavoritesActionEffect, ]; return ( <> diff --git a/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx index 15d371f9f44a..202b58b963e5 100644 --- a/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx @@ -11,6 +11,7 @@ export const GotoHotkeys = () => { return nonSystemActiveObjectMetadataItems.map((objectMetadataItem) => ( diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index 7a7de0807f1b..ae13d831fb7a 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -32,6 +32,8 @@ import { import { isDefined } from '~/utils/isDefined'; import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates'; +import { DateFormat } from '@/localization/constants/DateFormat'; +import { TimeFormat } from '@/localization/constants/TimeFormat'; import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState'; import { detectDateFormat } from '@/localization/utils/detectDateFormat'; import { detectTimeFormat } from '@/localization/utils/detectTimeFormat'; @@ -143,12 +145,12 @@ export const useAuth = () => { ? getDateFormatFromWorkspaceDateFormat( user.workspaceMember.dateFormat, ) - : detectDateFormat(), + : DateFormat[detectDateFormat()], timeFormat: isDefined(user.workspaceMember.timeFormat) ? getTimeFormatFromWorkspaceTimeFormat( user.workspaceMember.timeFormat, ) - : detectTimeFormat(), + : TimeFormat[detectTimeFormat()], }); } diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx index 9eccbeb98e10..ed06d3f0ee69 100644 --- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx +++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx @@ -1,23 +1,24 @@ -import { useEffect } from 'react'; -import { useRecoilState, useSetRecoilState } from 'recoil'; - import { apiConfigState } from '@/client-config/states/apiConfigState'; import { authProvidersState } from '@/client-config/states/authProvidersState'; import { billingState } from '@/client-config/states/billingState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState'; import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState'; +import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState'; import { isClientConfigLoadedState } from '@/client-config/states/isClientConfigLoadedState'; import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState'; import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState'; import { sentryConfigState } from '@/client-config/states/sentryConfigState'; import { supportChatState } from '@/client-config/states/supportChatState'; +import { useEffect } from 'react'; +import { useRecoilState, useSetRecoilState } from 'recoil'; import { useGetClientConfigQuery } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; export const ClientConfigProviderEffect = () => { const setAuthProviders = useSetRecoilState(authProvidersState); const setIsDebugMode = useSetRecoilState(isDebugModeState); + const setIsAnalyticsEnabled = useSetRecoilState(isAnalyticsEnabledState); const setIsSignInPrefilled = useSetRecoilState(isSignInPrefilledState); const setIsSignUpDisabled = useSetRecoilState(isSignUpDisabledState); @@ -50,6 +51,7 @@ export const ClientConfigProviderEffect = () => { magicLink: false, }); setIsDebugMode(data?.clientConfig.debugMode); + setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled); setIsSignInPrefilled(data?.clientConfig.signInPrefilled); setIsSignUpDisabled(data?.clientConfig.signUpDisabled); @@ -84,6 +86,7 @@ export const ClientConfigProviderEffect = () => { setCaptchaProvider, setChromeExtensionId, setApiConfig, + setIsAnalyticsEnabled, ]); return <>; diff --git a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts index e702acefa4f1..9a060b0d7b2b 100644 --- a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts @@ -16,6 +16,7 @@ export const GET_CLIENT_CONFIG = gql` signInPrefilled signUpDisabled debugMode + analyticsEnabled support { supportDriver supportFrontChatId diff --git a/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts b/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts new file mode 100644 index 000000000000..50c0f5c89c25 --- /dev/null +++ b/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const isAnalyticsEnabledState = createState({ + key: 'isAnalyticsEnabled', + defaultValue: false, +}); diff --git a/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx b/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx index cf106211405b..b975799fd499 100644 --- a/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx +++ b/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx @@ -2,17 +2,13 @@ import { useFilteredObjectMetadataItemsForWorkspaceFavorites } from '@/navigatio import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems'; import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; -import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; -import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; -import { View } from '@/views/types/View'; export const WorkspaceFavorites = () => { - const { records: views } = usePrefetchedData(PrefetchKey.AllViews); - const { activeObjectMetadataItems: objectMetadataItemsToDisplay } = useFilteredObjectMetadataItemsForWorkspaceFavorites(); const loading = useIsPrefetchLoading(); + if (loading) { return ; } @@ -21,7 +17,6 @@ export const WorkspaceFavorites = () => { ); diff --git a/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts b/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts new file mode 100644 index 000000000000..a1c7f2af3b72 --- /dev/null +++ b/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts @@ -0,0 +1,11 @@ +import { DateFormat } from '@/localization/constants/DateFormat'; + +type DateFormatWithoutYear = { + [K in keyof typeof DateFormat]: string; +}; +export const DATE_FORMAT_WITHOUT_YEAR: DateFormatWithoutYear = { + SYSTEM: 'SYSTEM', + MONTH_FIRST: 'MMM d', + DAY_FIRST: 'd MMM', + YEAR_FIRST: 'MMM d', +}; diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts b/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts index 2b641f302a63..b267622bf0cc 100644 --- a/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts +++ b/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts @@ -1,8 +1,7 @@ -import { DateFormat } from '@/localization/constants/DateFormat'; import { detectDateFormat } from '@/localization/utils/detectDateFormat'; describe('detectDateFormat', () => { - it('should return DateFormat.MONTH_FIRST if the detected format starts with month', () => { + it('should return MONTH_FIRST if the detected format starts with month', () => { // Mock the Intl.DateTimeFormat to return a specific format const mockDateTimeFormat = jest.fn().mockReturnValue({ formatToParts: () => [ @@ -16,10 +15,10 @@ describe('detectDateFormat', () => { const result = detectDateFormat(); - expect(result).toBe(DateFormat.MONTH_FIRST); + expect(result).toBe('MONTH_FIRST'); }); - it('should return DateFormat.DAY_FIRST if the detected format starts with day', () => { + it('should return DAY_FIRST if the detected format starts with day', () => { // Mock the Intl.DateTimeFormat to return a specific format const mockDateTimeFormat = jest.fn().mockReturnValue({ formatToParts: () => [ @@ -32,10 +31,10 @@ describe('detectDateFormat', () => { const result = detectDateFormat(); - expect(result).toBe(DateFormat.DAY_FIRST); + expect(result).toBe('DAY_FIRST'); }); - it('should return DateFormat.YEAR_FIRST if the detected format starts with year', () => { + it('should return YEAR_FIRST if the detected format starts with year', () => { // Mock the Intl.DateTimeFormat to return a specific format const mockDateTimeFormat = jest.fn().mockReturnValue({ formatToParts: () => [ @@ -48,10 +47,10 @@ describe('detectDateFormat', () => { const result = detectDateFormat(); - expect(result).toBe(DateFormat.YEAR_FIRST); + expect(result).toBe('YEAR_FIRST'); }); - it('should return DateFormat.MONTH_FIRST by default if the detected format does not match any specific order', () => { + it('should return MONTH_FIRST by default if the detected format does not match any specific order', () => { // Mock the Intl.DateTimeFormat to return a specific format const mockDateTimeFormat = jest.fn().mockReturnValue({ formatToParts: () => [ @@ -64,6 +63,6 @@ describe('detectDateFormat', () => { const result = detectDateFormat(); - expect(result).toBe(DateFormat.MONTH_FIRST); + expect(result).toBe('MONTH_FIRST'); }); }); diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts b/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts index 6433495789ee..9445068a5f7f 100644 --- a/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts +++ b/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts @@ -1,8 +1,7 @@ -import { TimeFormat } from '@/localization/constants/TimeFormat'; import { detectTimeFormat } from '@/localization/utils/detectTimeFormat'; describe('detectTimeFormat', () => { - it('should return TimeFormat.HOUR_12 if the hour format is 12-hour', () => { + it('should return HOUR_12 if the hour format is 12-hour', () => { // Mock the resolvedOptions method to return hour12 as true const mockResolvedOptions = jest.fn(() => ({ hour12: true })); Intl.DateTimeFormat = jest.fn().mockImplementation(() => ({ @@ -11,11 +10,11 @@ describe('detectTimeFormat', () => { const result = detectTimeFormat(); - expect(result).toBe(TimeFormat.HOUR_12); + expect(result).toBe('HOUR_12'); expect(mockResolvedOptions).toHaveBeenCalled(); }); - it('should return TimeFormat.HOUR_24 if the hour format is 24-hour', () => { + it('should return HOUR_24 if the hour format is 24-hour', () => { // Mock the resolvedOptions method to return hour12 as false const mockResolvedOptions = jest.fn(() => ({ hour12: false })); Intl.DateTimeFormat = jest.fn().mockImplementation(() => ({ @@ -24,7 +23,7 @@ describe('detectTimeFormat', () => { const result = detectTimeFormat(); - expect(result).toBe(TimeFormat.HOUR_24); + expect(result).toBe('HOUR_24'); expect(mockResolvedOptions).toHaveBeenCalled(); }); }); diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js b/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js new file mode 100644 index 000000000000..4caee3aedf0d --- /dev/null +++ b/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js @@ -0,0 +1,90 @@ +import { detectDateFormat } from '@/localization/utils/detectDateFormat'; +import { formatDateISOStringToDateTimeSimplified } from '@/localization/utils/formatDateISOStringToDateTimeSimplified'; +import { formatInTimeZone } from 'date-fns-tz'; +// Mock the imported modules +jest.mock('@/localization/utils/detectDateFormat'); +jest.mock('date-fns-tz'); + +describe('formatDateISOStringToDateTimeSimplified', () => { + const mockDate = new Date('2023-08-15T10:30:00Z'); + const mockTimeZone = 'America/New_York'; + const mockTimeFormat = 'HH:mm'; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should format the date correctly when DATE_FORMAT is MONTH_FIRST', () => { + detectDateFormat.mockReturnValue('MONTH_FIRST'); + formatInTimeZone.mockReturnValue('Oct 15 路 06:30'); + + const result = formatDateISOStringToDateTimeSimplified( + mockDate, + mockTimeZone, + mockTimeFormat, + ); + + expect(detectDateFormat).toHaveBeenCalled(); + expect(formatInTimeZone).toHaveBeenCalledWith( + mockDate, + mockTimeZone, + 'MMM d 路 HH:mm', + ); + expect(result).toBe('Oct 15 路 06:30'); + }); + + it('should format the date correctly when DATE_FORMAT is DAY_FIRST', () => { + detectDateFormat.mockReturnValue('DAY_FIRST'); + formatInTimeZone.mockReturnValue('15 Oct 路 06:30'); + + const result = formatDateISOStringToDateTimeSimplified( + mockDate, + mockTimeZone, + mockTimeFormat, + ); + + expect(detectDateFormat).toHaveBeenCalled(); + expect(formatInTimeZone).toHaveBeenCalledWith( + mockDate, + mockTimeZone, + 'd MMM 路 HH:mm', + ); + expect(result).toBe('15 Oct 路 06:30'); + }); + + it('should use the provided time format', () => { + detectDateFormat.mockReturnValue('MONTH_FIRST'); + formatInTimeZone.mockReturnValue('Oct 15 路 6:30 AM'); + + const result = formatDateISOStringToDateTimeSimplified( + mockDate, + mockTimeZone, + 'h:mm aa', + ); + + expect(formatInTimeZone).toHaveBeenCalledWith( + mockDate, + mockTimeZone, + 'MMM d 路 h:mm aa', + ); + expect(result).toBe('Oct 15 路 6:30 AM'); + }); + + it('should handle different time zones', () => { + detectDateFormat.mockReturnValue('MONTH_FIRST'); + formatInTimeZone.mockReturnValue('Oct 16 路 02:30'); + + const result = formatDateISOStringToDateTimeSimplified( + mockDate, + 'Asia/Tokyo', + mockTimeFormat, + ); + + expect(formatInTimeZone).toHaveBeenCalledWith( + mockDate, + 'Asia/Tokyo', + 'MMM d 路 HH:mm', + ); + expect(result).toBe('Oct 16 路 02:30'); + }); +}); diff --git a/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts b/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts index b503ef826e60..e38b018df445 100644 --- a/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts @@ -1,6 +1,6 @@ import { DateFormat } from '@/localization/constants/DateFormat'; -export const detectDateFormat = (): DateFormat => { +export const detectDateFormat = (): keyof typeof DateFormat => { const date = new Date(); const formatter = new Intl.DateTimeFormat(navigator.language); const parts = formatter.formatToParts(date); @@ -9,9 +9,9 @@ export const detectDateFormat = (): DateFormat => { .filter((part) => ['year', 'month', 'day'].includes(part.type)) .map((part) => part.type); - if (partOrder[0] === 'month') return DateFormat.MONTH_FIRST; - if (partOrder[0] === 'day') return DateFormat.DAY_FIRST; - if (partOrder[0] === 'year') return DateFormat.YEAR_FIRST; + if (partOrder[0] === 'month') return 'MONTH_FIRST'; + if (partOrder[0] === 'day') return 'DAY_FIRST'; + if (partOrder[0] === 'year') return 'YEAR_FIRST'; - return DateFormat.MONTH_FIRST; + return 'MONTH_FIRST'; }; diff --git a/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts b/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts index 01bad17167a5..d6d914d83637 100644 --- a/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts @@ -1,14 +1,14 @@ import { TimeFormat } from '@/localization/constants/TimeFormat'; import { isDefined } from '~/utils/isDefined'; -export const detectTimeFormat = () => { +export const detectTimeFormat = (): keyof typeof TimeFormat => { const isHour12 = Intl.DateTimeFormat(navigator.language, { hour: 'numeric', }).resolvedOptions().hour12; if (isDefined(isHour12) && isHour12) { - return TimeFormat.HOUR_12; + return 'HOUR_12'; } - return TimeFormat.HOUR_24; + return 'HOUR_24'; }; diff --git a/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts b/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts new file mode 100644 index 000000000000..c96d9f2f885d --- /dev/null +++ b/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts @@ -0,0 +1,18 @@ +import { DATE_FORMAT_WITHOUT_YEAR } from '@/localization/constants/DateFormatWithoutYear'; +import { TimeFormat } from '@/localization/constants/TimeFormat'; +import { detectDateFormat } from '@/localization/utils/detectDateFormat'; +import { formatInTimeZone } from 'date-fns-tz'; + +export const formatDateISOStringToDateTimeSimplified = ( + date: Date, + timeZone: string, + timeFormat: TimeFormat, +) => { + const simplifiedDateFormat = DATE_FORMAT_WITHOUT_YEAR[detectDateFormat()]; + + return formatInTimeZone( + date, + timeZone, + `${simplifiedDateFormat} 路 ${timeFormat}`, + ); +}; diff --git a/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts b/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts index f32bdbb93355..09293fbb8ec8 100644 --- a/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts @@ -7,7 +7,7 @@ export const getDateFormatFromWorkspaceDateFormat = ( ) => { switch (workspaceDateFormat) { case WorkspaceMemberDateFormatEnum.System: - return detectDateFormat(); + return DateFormat[detectDateFormat()]; case WorkspaceMemberDateFormatEnum.MonthFirst: return DateFormat.MONTH_FIRST; case WorkspaceMemberDateFormatEnum.DayFirst: diff --git a/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts b/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts index f6aebb43779b..7519d0cb4068 100644 --- a/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts @@ -7,7 +7,7 @@ export const getTimeFormatFromWorkspaceTimeFormat = ( ) => { switch (workspaceTimeFormat) { case WorkspaceMemberTimeFormatEnum.System: - return detectTimeFormat(); + return TimeFormat[detectTimeFormat()]; case WorkspaceMemberTimeFormatEnum.Hour_24: return TimeFormat.HOUR_24; case WorkspaceMemberTimeFormatEnum.Hour_12: diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx new file mode 100644 index 000000000000..8c7f1e3ceda2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx @@ -0,0 +1,84 @@ +import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; +import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; +import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; +import { NavigationDrawerItemsCollapsedContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer'; +import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem'; +import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState'; +import { View } from '@/views/types/View'; +import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews'; +import { useLocation } from 'react-router-dom'; +import { useIcons } from 'twenty-ui'; + +export type NavigationDrawerItemForObjectMetadataItemProps = { + objectMetadataItem: ObjectMetadataItem; +}; + +export const NavigationDrawerItemForObjectMetadataItem = ({ + objectMetadataItem, +}: NavigationDrawerItemForObjectMetadataItemProps) => { + const { records: views } = usePrefetchedData(PrefetchKey.AllViews); + + const objectMetadataViews = getObjectMetadataItemViews( + objectMetadataItem.id, + views, + ); + + const { getIcon } = useIcons(); + const currentPath = useLocation().pathname; + const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView(); + + const lastVisitedViewId = getLastVisitedViewIdFromObjectMetadataItemId( + objectMetadataItem.id, + ); + + const viewId = lastVisitedViewId ?? objectMetadataViews[0]?.id; + + const navigationPath = `/objects/${objectMetadataItem.namePlural}${ + viewId ? `?view=${viewId}` : '' + }`; + + const isActive = currentPath === `/objects/${objectMetadataItem.namePlural}`; + const shouldSubItemsBeDisplayed = isActive && objectMetadataViews.length > 1; + + const sortedObjectMetadataViews = [...objectMetadataViews].sort( + (viewA, viewB) => + viewA.key === 'INDEX' ? -1 : viewA.position - viewB.position, + ); + + const selectedSubItemIndex = sortedObjectMetadataViews.findIndex( + (view) => viewId === view.id, + ); + + const subItemArrayLength = sortedObjectMetadataViews.length; + + return ( + + + {shouldSubItemsBeDisplayed && + sortedObjectMetadataViews.map((view, index) => ( + + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx index fb17b643078f..b17ad4c310e2 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx @@ -5,9 +5,6 @@ import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; -import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; -import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; -import { View } from '@/views/types/View'; export const NavigationDrawerOpenedSection = () => { const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); @@ -15,7 +12,6 @@ export const NavigationDrawerOpenedSection = () => { (item) => !item.isRemote, ); - const { records: views } = usePrefetchedData(PrefetchKey.AllViews); const loading = useIsPrefetchLoading(); const currentObjectNamePlural = useParams().objectNamePlural; @@ -49,7 +45,6 @@ export const NavigationDrawerOpenedSection = () => { ) diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx index 5e666e0cf542..f90a160b57c4 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx @@ -1,18 +1,13 @@ -import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; +import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; -import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper'; -import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem'; +import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; +import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection'; -import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState'; -import { View } from '@/views/types/View'; -import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews'; -import { useLocation } from 'react-router-dom'; +import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; +import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; -import { useIcons } from 'twenty-ui'; -import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; -import { NavigationDrawerItemsCollapsedContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer'; + const ORDERED_STANDARD_OBJECTS = [ 'person', 'company', @@ -21,111 +16,59 @@ const ORDERED_STANDARD_OBJECTS = [ 'note', ]; +const StyledObjectsMetaDataItemsWrapper = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.betweenSiblingsGap}; + width: 100%; + margin-bottom: ${({ theme }) => theme.spacing(3)}; + flex: 1; + overflow-y: auto; +`; + export const NavigationDrawerSectionForObjectMetadataItems = ({ sectionTitle, isRemote, - views, objectMetadataItems, }: { sectionTitle: string; isRemote: boolean; - views: View[]; objectMetadataItems: ObjectMetadataItem[]; }) => { const { toggleNavigationSection, isNavigationSectionOpenState } = useNavigationSection('Objects' + (isRemote ? 'Remote' : 'Workspace')); const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState); - const { getIcon } = useIcons(); - const currentPath = useLocation().pathname; - const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView(); - - const renderObjectMetadataItems = () => { - return [ - ...objectMetadataItems - .filter((item) => ORDERED_STANDARD_OBJECTS.includes(item.nameSingular)) - .sort((objectMetadataItemA, objectMetadataItemB) => { - const indexA = ORDERED_STANDARD_OBJECTS.indexOf( - objectMetadataItemA.nameSingular, - ); - const indexB = ORDERED_STANDARD_OBJECTS.indexOf( - objectMetadataItemB.nameSingular, - ); - if (indexA === -1 || indexB === -1) { - return objectMetadataItemA.nameSingular.localeCompare( - objectMetadataItemB.nameSingular, - ); - } - return indexA - indexB; - }), - ...objectMetadataItems - .filter((item) => !ORDERED_STANDARD_OBJECTS.includes(item.nameSingular)) - .sort((objectMetadataItemA, objectMetadataItemB) => { - return new Date(objectMetadataItemA.createdAt) < - new Date(objectMetadataItemB.createdAt) - ? 1 - : -1; - }), - ].map((objectMetadataItem) => { - const objectMetadataViews = getObjectMetadataItemViews( - objectMetadataItem.id, - views, - ); - const lastVisitedViewId = getLastVisitedViewIdFromObjectMetadataItemId( - objectMetadataItem.id, - ); - const viewId = lastVisitedViewId ?? objectMetadataViews[0]?.id; - - const navigationPath = `/objects/${objectMetadataItem.namePlural}${ - viewId ? `?view=${viewId}` : '' - }`; - - const isActive = - currentPath === `/objects/${objectMetadataItem.namePlural}`; - const shouldSubItemsBeDisplayed = - isActive && objectMetadataViews.length > 1; - - const sortedObjectMetadataViews = [...objectMetadataViews].sort( - (viewA, viewB) => - viewA.key === 'INDEX' ? -1 : viewA.position - viewB.position, + const sortedStandardObjectMetadataItems = [...objectMetadataItems] + .filter((item) => ORDERED_STANDARD_OBJECTS.includes(item.nameSingular)) + .sort((objectMetadataItemA, objectMetadataItemB) => { + const indexA = ORDERED_STANDARD_OBJECTS.indexOf( + objectMetadataItemA.nameSingular, ); - - const selectedSubItemIndex = sortedObjectMetadataViews.findIndex( - (view) => viewId === view.id, + const indexB = ORDERED_STANDARD_OBJECTS.indexOf( + objectMetadataItemB.nameSingular, ); + if (indexA === -1 || indexB === -1) { + return objectMetadataItemA.nameSingular.localeCompare( + objectMetadataItemB.nameSingular, + ); + } + return indexA - indexB; + }); - const subItemArrayLength = sortedObjectMetadataViews.length; - - return ( - - - {shouldSubItemsBeDisplayed && - sortedObjectMetadataViews.map((view, index) => ( - - ))} - - ); + const sortedCustomObjectMetadataItems = [...objectMetadataItems] + .filter((item) => !ORDERED_STANDARD_OBJECTS.includes(item.nameSingular)) + .sort((objectMetadataItemA, objectMetadataItemB) => { + return new Date(objectMetadataItemA.createdAt) < + new Date(objectMetadataItemB.createdAt) + ? 1 + : -1; }); - }; + + const objectMetadataItemsForNavigationItems = [ + ...sortedStandardObjectMetadataItems, + ...sortedCustomObjectMetadataItems, + ]; return ( objectMetadataItems.length > 0 && ( @@ -136,7 +79,19 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({ onClick={() => toggleNavigationSection()} /> - {isNavigationSectionOpen && renderObjectMetadataItems()} + + + {isNavigationSectionOpen && + objectMetadataItemsForNavigationItems.map( + (objectMetadataItem) => ( + + ), + )} + + ) ); diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper.tsx index 2127db1fc604..91a22ca5ab1e 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper.tsx @@ -6,9 +6,6 @@ import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; -import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; -import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; -import { View } from '@/views/types/View'; export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({ isRemote, @@ -21,8 +18,6 @@ export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({ const filteredActiveObjectMetadataItems = activeObjectMetadataItems.filter( (item) => (isRemote ? item.isRemote : !item.isRemote), ); - - const { records: views } = usePrefetchedData(PrefetchKey.AllViews); const loading = useIsPrefetchLoading(); if (loading && isDefined(currentUser)) { @@ -33,7 +28,6 @@ export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({ ); diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsRelationPickerEffect.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsRelationPickerEffect.tsx deleted file mode 100644 index 1d2d1ecd74b9..000000000000 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsRelationPickerEffect.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useEffect } from 'react'; - -import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker'; - -export const ObjectMetadataItemsRelationPickerEffect = ({ - relationPickerScopeId, -}: { - relationPickerScopeId?: string; -} = {}) => { - const { setSearchQuery } = useRelationPicker({ relationPickerScopeId }); - - const computeFilterFields = (relationPickerType: string) => { - if (relationPickerType === 'company') { - return ['name']; - } - - if (['workspaceMember', 'person'].includes(relationPickerType)) { - return ['name.firstName', 'name.lastName']; - } - - return ['name']; - }; - - useEffect(() => { - setSearchQuery({ computeFilterFields }); - }, [setSearchQuery]); - - return <>; -}; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts index db3506c2d12c..0ab2a8229854 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts @@ -11,7 +11,7 @@ export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({ pagedObjectMetadataItems?.objects.edges.map((object) => ({ ...object.node, fields: object.node.fields.edges.map((field) => field.node), - indexMetadatas: object.node.indexMetadatas.edges.map((index) => ({ + indexMetadatas: object.node.indexMetadatas?.edges.map((index) => ({ ...index.node, indexFieldMetadatas: index.node.indexFieldMetadatas?.edges.map( (indexField) => indexField.node, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts index 175f84554f19..7c1f90162597 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts @@ -13,10 +13,11 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useQuery, WatchQueryFetchPolicy } from '@apollo/client'; import { useMemo } from 'react'; import { useRecoilValue } from 'recoil'; +import { isDefined } from '~/utils/isDefined'; import { logError } from '~/utils/logError'; export type UseSearchRecordsParams = ObjectMetadataItemIdentifier & - RecordGqlOperationVariables & { + Pick & { onError?: (error?: Error) => void; skip?: boolean; recordGqlFields?: RecordGqlOperationGqlRecordFields; @@ -29,6 +30,7 @@ export const useSearchRecords = ({ searchInput, limit, skip, + filter, recordGqlFields, fetchPolicy, }: UseSearchRecordsParams) => { @@ -45,10 +47,14 @@ export const useSearchRecords = ({ const { data, loading, error, previousData } = useQuery(searchRecordsQuery, { skip: - skip || !objectMetadataItem || !currentWorkspaceMember || !searchInput, + skip || + !objectMetadataItem || + !currentWorkspaceMember || + !isDefined(searchInput), variables: { search: searchInput, limit: limit, + filter: filter, }, fetchPolicy: fetchPolicy, onError: (error) => { diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index ff408eb407de..e9487ea6eb23 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -4,6 +4,8 @@ import { useContext, useRef } from 'react'; import { useRecoilCallback, useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; +import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader'; +import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; @@ -19,31 +21,26 @@ import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/get import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useScrollRestoration } from '~/hooks/useScrollRestoration'; -export type RecordBoardProps = { - recordBoardId: string; -}; - const StyledContainer = styled.div` - border-top: 1px solid ${({ theme }) => theme.border.color.light}; - overflow: auto; display: flex; flex: 1; flex-direction: row; min-height: calc(100% - 1px); + height: 100%; `; -const StyledWrapper = styled.div` +const StyledColumnContainer = styled.div` + display: flex; +`; + +const StyledContainerContainer = styled.div` display: flex; flex-direction: column; - height: 100%; - overflow: hidden; - position: relative; - width: 100%; `; -const StyledBoardHeader = styled.div` - position: relative; - z-index: 1; +const StyledBoardContentContainer = styled.div` + display: flex; + flex-direction: column; `; const RecordBoardScrollRestoreEffect = () => { @@ -51,8 +48,8 @@ const RecordBoardScrollRestoreEffect = () => { return null; }; -export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => { - const { updateOneRecord, selectFieldMetadataItem } = +export const RecordBoard = () => { + const { updateOneRecord, selectFieldMetadataItem, recordBoardId } = useContext(RecordBoardContext); const boardRef = useRef(null); @@ -75,7 +72,7 @@ export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => { useScopedHotkeys([Key.Escape], resetRecordSelection, TableHotkeyScope.Table); - const onDragEnd: OnDragEndResponder = useRecoilCallback( + const handleDragEnd: OnDragEndResponder = useRecoilCallback( ({ snapshot }) => (result) => { if (!result.destination) return; @@ -146,27 +143,32 @@ export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => { onColumnsChange={() => {}} onFieldsChange={() => {}} > - - - - - - {columnIds.map((columnId) => ( - - ))} - - - - - - + + + + + + + + + {columnIds.map((columnId) => ( + + ))} + + + + + + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx new file mode 100644 index 000000000000..59e1acf47967 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx @@ -0,0 +1,34 @@ +import { useRecoilValue } from 'recoil'; + +import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { RecordBoardColumnHeaderWrapper } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper'; +import styled from '@emotion/styled'; + +const StyledHeaderContainer = styled.div` + display: flex; + flex-direction: row; + height: 40px; + z-index: 10; + + overflow: visible; + width: 100%; + + &.header-sticky { + position: sticky; + top: 0; + } +`; + +export const RecordBoardHeader = () => { + const { columnIdsState } = useRecordBoardStates(); + + const columnIds = useRecoilValue(columnIdsState); + + return ( + + {columnIds.map((columnId) => ( + + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardStickyHeaderEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardStickyHeaderEffect.tsx new file mode 100644 index 000000000000..5544a7801a2f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardStickyHeaderEffect.tsx @@ -0,0 +1,22 @@ +import { useEffect } from 'react'; + +import { useScrollTopValue } from '@/ui/utilities/scroll/hooks/useScrollTopValue'; + +export const RecordBoardStickyHeaderEffect = () => { + const scrollTop = useScrollTopValue('recordBoard'); + + // TODO: move this outside because it might cause way too many re-renders for other hooks + useEffect(() => { + if (scrollTop > 0) { + document + .getElementById('record-board-header') + ?.classList.add('header-sticky'); + } else { + document + .getElementById('record-board-header') + ?.classList.remove('header-sticky'); + } + }, [scrollTop]); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/contexts/RecordBoardContext.ts b/packages/twenty-front/src/modules/object-record/record-board/contexts/RecordBoardContext.ts index d8d7f1490738..266e1a445460 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/contexts/RecordBoardContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/contexts/RecordBoardContext.ts @@ -16,6 +16,7 @@ type RecordBoardContextProps = { updateOneRecordInput: Partial>; }) => void; deleteOneRecord: (idToDelete: string) => Promise; + recordBoardId: string; }; export const RecordBoardContext = createContext( diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx index 70660329bdcc..8fe97824425b 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx @@ -4,7 +4,6 @@ import { useRecoilValue } from 'recoil'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardColumnCardsContainer } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer'; -import { RecordBoardColumnHeader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeader'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; const StyledColumn = styled.div<{ isFirstColumn: boolean }>` @@ -18,7 +17,12 @@ const StyledColumn = styled.div<{ isFirstColumn: boolean }>` min-width: 200px; padding: ${({ theme }) => theme.spacing(2)}; + + padding-top: 0px; + position: relative; + + min-height: 100%; `; type RecordBoardColumnProps = { @@ -61,12 +65,13 @@ export const RecordBoardColumn = ({ isFirstColumn: isFirstColumn, isLastColumn: isLastColumn, recordCount: recordIds.length, + columnId: recordBoardColumnId, + recordIds, }} > {(droppableProvided) => ( - theme.spacing(2)}; width: 100%; `; @@ -45,6 +44,7 @@ const StyledHeaderActions = styled.div` margin-left: auto; `; const StyledHeaderContainer = styled.div` + background: ${({ theme }) => theme.background.primary}; display: flex; justify-content: space-between; width: 100%; @@ -59,13 +59,29 @@ const StyledRightContainer = styled.div` display: flex; `; +const StyledColumn = styled.div<{ isFirstColumn: boolean }>` + background-color: ${({ theme }) => theme.background.primary}; + border-left: 1px solid + ${({ theme, isFirstColumn }) => + isFirstColumn ? 'none' : theme.border.color.light}; + display: flex; + flex-direction: column; + max-width: 200px; + min-width: 200px; + + padding: ${({ theme }) => theme.spacing(2)}; + + position: relative; +`; + export const RecordBoardColumnHeader = () => { + const { columnDefinition, isFirstColumn, recordCount } = useContext( + RecordBoardColumnContext, + ); const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false); const [isHeaderHovered, setIsHeaderHovered] = useState(false); + const { objectMetadataItem } = useContext(RecordBoardContext); - const { columnDefinition, recordCount } = useContext( - RecordBoardColumnContext, - ); const { setHotkeyScopeAndMemorizePreviousScope, @@ -94,7 +110,8 @@ export const RecordBoardColumnHeader = () => { handleNewButtonClick, handleCreateSuccess, handleEntitySelect, - } = useColumnNewCardActions(columnDefinition.id); + } = useColumnNewCardActions(columnDefinition?.id ?? ''); + const { isOpportunitiesCompanyFieldDisabled } = useIsOpportunitiesCompanyFieldDisabled(); @@ -103,7 +120,7 @@ export const RecordBoardColumnHeader = () => { !isOpportunitiesCompanyFieldDisabled; return ( - <> + setIsHeaderHovered(true)} onMouseLeave={() => setIsHeaderHovered(false)} @@ -181,6 +198,6 @@ export const RecordBoardColumnHeader = () => { position="first" /> ))} - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx new file mode 100644 index 000000000000..63f25794bd9d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx @@ -0,0 +1,48 @@ +import { isDefined } from 'twenty-ui'; + +import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { RecordBoardColumnHeader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeader'; +import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; +import { useRecoilValue } from 'recoil'; + +type RecordBoardColumnHeaderWrapperProps = { + columnId: string; +}; + +export const RecordBoardColumnHeaderWrapper = ({ + columnId, +}: RecordBoardColumnHeaderWrapperProps) => { + const { + isFirstColumnFamilyState, + isLastColumnFamilyState, + columnsFamilySelector, + recordIdsByColumnIdFamilyState, + } = useRecordBoardStates(); + + const columnDefinition = useRecoilValue(columnsFamilySelector(columnId)); + + const isFirstColumn = useRecoilValue(isFirstColumnFamilyState(columnId)); + + const isLastColumn = useRecoilValue(isLastColumnFamilyState(columnId)); + + const recordIds = useRecoilValue(recordIdsByColumnIdFamilyState(columnId)); + + if (!isDefined(columnDefinition)) { + return null; + } + + return ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext.ts index 8a9ced3eb00b..f37c5c5cb3b3 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext.ts @@ -7,6 +7,8 @@ type RecordBoardColumnContextProps = { isFirstColumn: boolean; isLastColumn: boolean; recordCount: number; + columnId: string; + recordIds: string[]; }; export const RecordBoardColumnContext = diff --git a/packages/twenty-front/src/modules/object-record/record-board/scopes/RecordBoardScope.tsx b/packages/twenty-front/src/modules/object-record/record-board/scopes/RecordBoardScope.tsx index 0de4d6025714..37c605185528 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/scopes/RecordBoardScope.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/scopes/RecordBoardScope.tsx @@ -12,6 +12,7 @@ type RecordBoardScopeProps = { onColumnsChange: (column: RecordBoardColumnDefinition[]) => void; }; +/** @deprecated */ export const RecordBoardScope = ({ children, recordBoardScopeId, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx index 7e3e93ec2c48..e416e50ce88a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx @@ -18,6 +18,7 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis import { FieldMetadataType } from '~/generated-metadata/graphql'; import { moveArrayItem } from '~/utils/array/moveArrayItem'; import { toSpliced } from '~/utils/array/toSpliced'; +import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly'; const StyledDropdownMenu = styled(DropdownMenu)` left: -1px; @@ -190,7 +191,11 @@ export const MultiItemFieldInput = ({ }) : undefined } - onChange={(event) => handleOnChange(event.target.value)} + onChange={(event) => + handleOnChange( + turnIntoEmptyStringIfWhitespacesOnly(event.target.value), + ) + } onEnter={handleSubmitInput} rightComponent={ - - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx index 50ce4ed693d0..9aecee3e6160 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx @@ -2,8 +2,6 @@ import styled from '@emotion/styled'; import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { RecordIndexBoardContainer } from '@/object-record/record-index/components/RecordIndexBoardContainer'; import { RecordIndexBoardDataLoader } from '@/object-record/record-index/components/RecordIndexBoardDataLoader'; import { RecordIndexBoardDataLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardDataLoaderEffect'; @@ -45,12 +43,13 @@ const StyledContainer = styled.div` flex-direction: column; height: 100%; width: 100%; - overflow: auto; + + overflow: hidden; `; -const StyledContainerWithPadding = styled.div<{ fullHeight?: boolean }>` - height: ${({ fullHeight }) => (fullHeight ? '100%' : 'auto')}; - padding-left: ${({ theme }) => theme.table.horizontalCellPadding}; +const StyledContainerWithPadding = styled.div` + height: calc(100% - 40px); + width: 100%; `; export const RecordIndexContainer = () => { @@ -58,17 +57,12 @@ export const RecordIndexContainer = () => { recordIndexViewTypeState, ); - const { objectNamePlural, recordIndexId } = useContext( - RecordIndexRootPropsContext, - ); - - const { objectNameSingular } = useObjectNameSingularFromPlural({ + const { objectNamePlural, - }); - - const { objectMetadataItem } = useObjectMetadataItem({ + recordIndexId, + objectMetadataItem, objectNameSingular, - }); + } = useContext(RecordIndexRootPropsContext); const { columnDefinitions, filterDefinitions, sortDefinitions } = useColumnDefinitionsFromFieldMetadata(objectMetadataItem); @@ -120,69 +114,56 @@ export const RecordIndexContainer = () => { > - - + + } + onCurrentViewChange={(view) => { + if (!view) { + return; } - onCurrentViewChange={(view) => { - if (!view) { - return; - } - - onViewFieldsChange(view.viewFields); - setTableFilters( - mapViewFiltersToFilters( - view.viewFilters, - filterDefinitions, - ), - ); - setRecordIndexFilters( - mapViewFiltersToFilters( - view.viewFilters, - filterDefinitions, - ), - ); - setTableSorts( - mapViewSortsToSorts(view.viewSorts, sortDefinitions), - ); - setRecordIndexSorts( - mapViewSortsToSorts(view.viewSorts, sortDefinitions), - ); - setRecordIndexViewType(view.type); - setRecordIndexViewKanbanFieldMetadataIdState( - view.kanbanFieldMetadataId, - ); - setRecordIndexIsCompactModeActive(view.isCompact); - }} - /> - - - + onViewFieldsChange(view.viewFields); + setTableFilters( + mapViewFiltersToFilters(view.viewFilters, filterDefinitions), + ); + setRecordIndexFilters( + mapViewFiltersToFilters(view.viewFilters, filterDefinitions), + ); + setTableSorts( + mapViewSortsToSorts(view.viewSorts, sortDefinitions), + ); + setRecordIndexSorts( + mapViewSortsToSorts(view.viewSorts, sortDefinitions), + ); + setRecordIndexViewType(view.type); + setRecordIndexViewKanbanFieldMetadataIdState( + view.kanbanFieldMetadataId, + ); + setRecordIndexIsCompactModeActive(view.isCompact); + }} + /> + + {recordIndexViewType === ViewType.Table && ( <> - + )} {recordIndexViewType === ViewType.Kanban && ( - + { + const { recordIndexId, objectNameSingular } = useContext( + RecordIndexRootPropsContext, + ); + + const viewBarId = recordIndexId; -export const RecordIndexTableContainerEffect = ({ - objectNameSingular, - recordTableId, - viewBarId, -}: RecordIndexTableContainerEffectProps) => { const { setAvailableTableColumns, setOnEntityCountChange, @@ -28,7 +25,7 @@ export const RecordIndexTableContainerEffect = ({ setOnToggleColumnFilter, setOnToggleColumnSort, } = useRecordTable({ - recordTableId, + recordTableId: recordIndexId, }); const setContextStoreTargetedRecordIds = useSetRecoilState( diff --git a/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexRootPropsContext.ts b/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexRootPropsContext.ts index 6de7cd552657..1546fd30a34b 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexRootPropsContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexRootPropsContext.ts @@ -1,3 +1,4 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { createRootPropsContext } from '~/utils/createRootPropsContext'; export type RecordIndexRootPropsContextProps = { @@ -6,6 +7,7 @@ export type RecordIndexRootPropsContextProps = { onCreateRecord: () => void; objectNamePlural: string; objectNameSingular: string; + objectMetadataItem: ObjectMetadataItem; recordIndexId: string; }; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx index 81e0c37c76e7..432ff14fe5b9 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx @@ -4,7 +4,6 @@ import { useCallback, useContext } from 'react'; import { useRecoilValue } from 'recoil'; import { IconForbid, IconPencil, IconPlus } from 'twenty-ui'; -import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; @@ -209,7 +208,6 @@ export const RecordDetailRelationSection = ({ /> ) : ( <> - { deleteCombinedViewFilter( tableFilters.find( (filter) => - filter.definition.label === 'Deleted at' && + filter.definition.label === 'Deleted' && filter.operand === 'isNotEmpty', )?.id ?? '', ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx index 9550dac39bb5..506b186e7ca6 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx @@ -9,7 +9,6 @@ import { RecordTableContext } from '@/object-record/record-table/contexts/Record import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2'; import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState'; -import { isRecordTableScrolledTopComponentState } from '@/object-record/record-table/states/isRecordTableScrolledTopComponentState'; import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState'; import { useScrollLeftValue } from '@/ui/utilities/scroll/hooks/useScrollLeftValue'; import { useScrollTopValue } from '@/ui/utilities/scroll/hooks/useScrollTopValue'; @@ -41,16 +40,12 @@ export const RecordTableBodyEffect = () => { const tableLastRowVisible = useRecoilValue(tableLastRowVisibleState); const scrollTop = useScrollTopValue('recordTableWithWrappers'); - const setIsRecordTableScrolledTop = useSetRecoilComponentState( - isRecordTableScrolledTopComponentState, - ); const setHasRecordTableFetchedAllRecordsComponents = useSetRecoilComponentState(hasRecordTableFetchedAllRecordsComponentStateV2); // TODO: move this outside because it might cause way too many re-renders for other hooks useEffect(() => { - setIsRecordTableScrolledTop(scrollTop === 0); if (scrollTop > 0) { document .getElementById('record-table-header') @@ -60,7 +55,7 @@ export const RecordTableBodyEffect = () => { .getElementById('record-table-header') ?.classList.remove('header-sticky'); } - }, [scrollTop, setIsRecordTableScrolledTop]); + }, [scrollTop]); const scrollLeft = useScrollLeftValue('recordTableWithWrappers'); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx index 556e1e9845d5..b17b50a237cc 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx @@ -8,10 +8,7 @@ import { RecordTableHeaderCheckboxColumn } from '@/object-record/record-table/re import { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn'; import { RecordTableHeaderLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn'; -const StyledTableHead = styled.thead<{ - isScrolledTop?: boolean; - isScrolledLeft?: boolean; -}>` +const StyledTableHead = styled.thead` cursor: pointer; th:nth-of-type(1) { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx index ff3fc98580ee..66cbddd5ed14 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx @@ -26,7 +26,6 @@ const StyledColumnHeaderCell = styled.th<{ isResizing?: boolean; }>` border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; - border-top: 1px solid ${({ theme }) => theme.border.color.light}; color: ${({ theme }) => theme.font.color.tertiary}; padding: 0; text-align: left; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx index 942e8865b557..5bcfd65d67cd 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx @@ -17,7 +17,6 @@ const StyledColumnHeaderCell = styled.th` background-color: ${({ theme }) => theme.background.primary}; border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; border-right: transparent; - border-top: 1px solid ${({ theme }) => theme.border.color.light}; max-width: 30px; min-width: 30px; width: 30px; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx index 8a87fd63c560..9cf9df75ce9b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx @@ -22,7 +22,6 @@ const StyledPlusIconHeaderCell = styled.th<{ `; }}; border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; - border-top: 1px solid ${({ theme }) => theme.border.color.light}; background-color: ${({ theme }) => theme.background.primary}; border-left: none !important; color: ${({ theme }) => theme.font.color.tertiary}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledTopComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledTopComponentState.ts deleted file mode 100644 index 5a206e88b7a5..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledTopComponentState.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext'; -import { createComponentStateV2_alpha } from '@/ui/utilities/state/component-state/utils/createComponentStateV2_alpha'; - -export const isRecordTableScrolledTopComponentState = - createComponentStateV2_alpha({ - key: 'isRecordTableScrolledTopComponentState', - componentContext: RecordTableScopeInternalContext, - defaultValue: true, - }); diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx index 74c0541320b1..40c6fe337233 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx @@ -1,4 +1,3 @@ -import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect'; import { SingleEntitySelectMenuItems, SingleEntitySelectMenuItemsProps, @@ -65,9 +64,6 @@ export const SingleEntitySelectMenuItemsWithSearch = ({ return ( <> - gql` - query Search${capitalize(objectMetadataItem.namePlural)}($search: String, $limit: Int) { - ${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(searchInput: $search, limit: $limit){ + query Search${capitalize(objectMetadataItem.namePlural)}($search: String, $limit: Int, $filter: ${capitalize( + objectMetadataItem.nameSingular, + )}FilterInput) { + ${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(searchInput: $search, limit: $limit, filter: $filter){ edges { node ${mapObjectMetadataToGraphQLQuery({ objectMetadataItems, diff --git a/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx b/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx index 263b70decf0f..6b2409af45a3 100644 --- a/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx +++ b/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx @@ -80,13 +80,11 @@ describe('useFilteredSearchEntityQuery', () => { setMetadataItems(generatedMockObjectMetadataItems); return useFilteredSearchEntityQuery({ - orderByField: 'name', - filters: [{ fieldNames: ['name'], filter: 'Entity' }], - sortOrder: 'AscNullsLast', selectedIds: ['1'], limit: 10, excludeRecordIds: ['2'], objectNameSingular: 'person', + searchFilter: 'Entity', }); }, { wrapper: Wrapper }, diff --git a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts index 06ba43f92e60..9d6973385230 100644 --- a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts +++ b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts @@ -1,39 +1,26 @@ -import { isNonEmptyString } from '@sniptt/guards'; - import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier'; import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit'; -import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { useSearchRecords } from '@/object-record/hooks/useSearchRecords'; import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect'; import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; -import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables'; -import { OrderBy } from '@/types/OrderBy'; -import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields'; import { isDefined } from '~/utils/isDefined'; -type SearchFilter = { fieldNames: string[]; filter: string | number }; - // TODO: use this for all search queries, because we need selectedEntities and entitiesToSelect each time we want to search // Filtered entities to select are export const useFilteredSearchEntityQuery = ({ - orderByField, - filters, - sortOrder = 'AscNullsLast', selectedIds, limit, excludeRecordIds = [], objectNameSingular, + searchFilter, }: { - orderByField: string; - filters: SearchFilter[]; - sortOrder?: OrderBy; selectedIds: string[]; limit?: number; excludeRecordIds?: string[]; objectNameSingular: string; + searchFilter?: string; }): EntitiesForMultipleEntitySelect => { const { mapToObjectRecordIdentifier } = useMapToObjectRecordIdentifier({ objectNameSingular, @@ -46,55 +33,21 @@ export const useFilteredSearchEntityQuery = ({ const selectedIdsFilter = { id: { in: selectedIds } }; const { loading: selectedRecordsLoading, records: selectedRecords } = - useFindManyRecords({ + useSearchRecords({ objectNameSingular, filter: selectedIdsFilter, - orderBy: [{ [orderByField]: sortOrder }], skip: !selectedIds.length, + searchInput: searchFilter, }); - const searchFilters = filters.map(({ fieldNames, filter }) => { - if (!isNonEmptyString(filter)) { - return undefined; - } - - const formattedFilters = fieldNames.reduce( - (previousValue: RecordGqlOperationFilter[], fieldName) => { - const [parentFieldName, subFieldName] = fieldName.split('.'); - - if (isNonEmptyString(subFieldName)) { - // Composite field - return [ - ...previousValue, - ...generateILikeFiltersForCompositeFields(filter, parentFieldName, [ - subFieldName, - ]), - ]; - } - - return [ - ...previousValue, - { - [fieldName]: { - ilike: `%${filter}%`, - }, - }, - ]; - }, - [], - ); - - return makeOrFilterVariables(formattedFilters); - }); - const { loading: filteredSelectedRecordsLoading, records: filteredSelectedRecords, - } = useFindManyRecords({ + } = useSearchRecords({ objectNameSingular, - filter: makeAndFilterVariables([...searchFilters, selectedIdsFilter]), - orderBy: [{ [orderByField]: sortOrder }], + filter: selectedIdsFilter, skip: !selectedIds.length, + searchInput: searchFilter, }); const notFilterIds = [...selectedIds, ...excludeRecordIds]; @@ -102,11 +55,11 @@ export const useFilteredSearchEntityQuery = ({ ? { not: { id: { in: notFilterIds } } } : undefined; const { loading: recordsToSelectLoading, records: recordsToSelect } = - useFindManyRecords({ + useSearchRecords({ objectNameSingular, - filter: makeAndFilterVariables([...searchFilters, notFilter]), + filter: notFilter, limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT, - orderBy: [{ [orderByField]: sortOrder }], + searchInput: searchFilter, }); return { diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx index a4c9f5306fad..3d513dc1eff4 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx @@ -1,11 +1,10 @@ -import styled from '@emotion/styled'; - import { BlocklistItem } from '@/accounts/types/BlocklistItem'; import { SettingsAccountsBlocklistTableRow } from '@/settings/accounts/components/SettingsAccountsBlocklistTableRow'; import { Table } from '@/ui/layout/table/components/Table'; import { TableBody } from '@/ui/layout/table/components/TableBody'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableRow } from '@/ui/layout/table/components/TableRow'; +import styled from '@emotion/styled'; type SettingsAccountsBlocklistTableProps = { blocklist: BlocklistItem[]; @@ -28,7 +27,10 @@ export const SettingsAccountsBlocklistTable = ({ <> {blocklist.length > 0 && ( - + Email/Domain Added to blocklist diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx index 9a1148447a17..30cf3a37a313 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx @@ -1,4 +1,4 @@ -import { IconX } from 'twenty-ui'; +import { IconX, OverflowingTextWithTooltip } from 'twenty-ui'; import { BlocklistItem } from '@/accounts/types/BlocklistItem'; import { IconButton } from '@/ui/input/button/components/IconButton'; @@ -16,8 +16,14 @@ export const SettingsAccountsBlocklistTableRow = ({ onRemove, }: SettingsAccountsBlocklistTableRowProps) => { return ( - - {blocklistItem.handle} + + + + {blocklistItem.createdAt ? formatToHumanReadableDate(blocklistItem.createdAt) diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx index 9b50515b10b9..ca1ef939abdb 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx @@ -129,7 +129,9 @@ export const SettingsDataModelFieldSettingsFormCard = ({ fieldMetadataItem, objectMetadataItem, }: SettingsDataModelFieldSettingsFormCardProps) => { - if (!previewableTypes.includes(fieldMetadataItem.type)) return null; + if (!previewableTypes.includes(fieldMetadataItem.type)) { + return null; + } if (fieldMetadataItem.type === FieldMetadataType.Boolean) { return ( diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx index 0372ba071417..59bee20064ea 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx @@ -23,7 +23,6 @@ type SettingsDataModelFieldRelationSettingsFormCardProps = { } & Pick; const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` - display: grid; flex: 1 1 100%; `; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx index 06684997a4b2..360c9a300692 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx @@ -19,7 +19,6 @@ const StyledCard = styled(Card)` `; const StyledCardContent = styled(CardContent)` - display: grid; padding: ${({ theme }) => theme.spacing(2)}; `; diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx index f6b8aaa0ab54..931fb6990e09 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx @@ -1,6 +1,6 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { useIcons } from 'twenty-ui'; +import { OverflowingTextWithTooltip, useIcons } from 'twenty-ui'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/SettingsDataModelObjectTypeTag'; @@ -14,15 +14,17 @@ export type SettingsDataModelObjectSummaryProps = { const StyledObjectSummary = styled.div` align-items: center; display: flex; - gap: ${({ theme }) => theme.spacing(2)}; justify-content: space-between; `; const StyledObjectName = styled.div` - align-items: center; display: flex; - font-weight: ${({ theme }) => theme.font.weight.medium}; - gap: ${({ theme }) => theme.spacing(1)}; + gap: ${({ theme }) => theme.spacing(2)}; + max-width: 60%; +`; + +const StyledIconContainer = styled.div` + flex-shrink: 0; `; export const SettingsDataModelObjectSummary = ({ @@ -38,8 +40,10 @@ export const SettingsDataModelObjectSummary = ({ return ( - - {objectMetadataItem.labelPlural} + + + + diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx new file mode 100644 index 000000000000..40925c5d3830 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx @@ -0,0 +1,89 @@ +import { formatDateISOStringToDateTimeSimplified } from '@/localization/utils/formatDateISOStringToDateTimeSimplified'; +import { UserContext } from '@/users/contexts/UserContext'; +import styled from '@emotion/styled'; +import { Point } from '@nivo/line'; +import { ReactElement, useContext } from 'react'; + +const StyledTooltipContainer = styled.div` + align-items: center; + border-radius: ${({ theme }) => theme.border.radius.md}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + display: flex; + width: 128px; + flex-direction: column; + justify-content: center; + background: ${({ theme }) => theme.background.transparent.secondary}; + box-shadow: ${({ theme }) => theme.boxShadow.light}; + backdrop-filter: ${({ theme }) => theme.blur.medium}; +`; + +const StyledTooltipDateContainer = styled.div` + align-items: flex-start; + align-self: stretch; + display: flex; + justify-content: center; + font-weight: ${({ theme }) => theme.font.weight.medium}; + font-family: ${({ theme }) => theme.font.family}; + gap: ${({ theme }) => theme.spacing(2)}; + color: ${({ theme }) => theme.font.color.secondary}; + padding: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledTooltipDataRow = styled.div` + align-items: flex-start; + align-self: stretch; + display: flex; + justify-content: space-between; + color: ${({ theme }) => theme.font.color.tertiary}; + padding: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledLine = styled.div` + background-color: ${({ theme }) => theme.border.color.medium}; + height: 1px; + width: 100%; +`; +const StyledColorPoint = styled.div<{ color: string }>` + background-color: ${({ color }) => color}; + border-radius: 50%; + height: 8px; + width: 8px; + display: inline-block; +`; +const StyledDataDefinition = styled.div` + display: flex; + align-items: center; + gap: ${({ theme }) => theme.spacing(2)}; +`; +const StyledSpan = styled.span` + color: ${({ theme }) => theme.font.color.primary}; +`; +type SettingsDevelopersWebhookTooltipProps = { + point: Point; +}; +export const SettingsDevelopersWebhookTooltip = ({ + point, +}: SettingsDevelopersWebhookTooltipProps): ReactElement => { + const { timeFormat, timeZone } = useContext(UserContext); + const windowInterval = new Date(point.data.x); + const windowIntervalDate = formatDateISOStringToDateTimeSimplified( + windowInterval, + timeZone, + timeFormat, + ); + return ( + + + {windowIntervalDate} + + + + + + {String(point.serieId)} + + {String(point.data.y)} + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx index eb2e359fff16..9626c6712eef 100644 --- a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx +++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx @@ -1,8 +1,13 @@ +import { SettingsDevelopersWebhookTooltip } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip'; +import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData'; import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState'; +import { Select } from '@/ui/input/components/Select'; +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { ResponsiveLine } from '@nivo/line'; import { Section } from '@react-email/components'; -import { useRecoilValue } from 'recoil'; +import { useState } from 'react'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { H2Title } from 'twenty-ui'; export type NivoLineInput = { @@ -14,22 +19,102 @@ export type NivoLineInput = { }>; }; const StyledGraphContainer = styled.div` - height: 200px; - width: 100%; + background-color: ${({ theme }) => theme.background.secondary}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-radius: ${({ theme }) => theme.border.radius.md}; + height: 199px; + + padding: ${({ theme }) => theme.spacing(4, 2, 2, 2)}; + width: 496px; +`; +const StyledTitleContainer = styled.div` + align-items: flex-start; + display: flex; + justify-content: space-between; `; -export const SettingsDeveloppersWebhookUsageGraph = () => { + +type SettingsDevelopersWebhookUsageGraphProps = { + webhookId: string; +}; + +export const SettingsDevelopersWebhookUsageGraph = ({ + webhookId, +}: SettingsDevelopersWebhookUsageGraphProps) => { const webhookGraphData = useRecoilValue(webhookGraphDataState); + const setWebhookGraphData = useSetRecoilState(webhookGraphDataState); + const theme = useTheme(); + + const [windowLengthGraphOption, setWindowLengthGraphOption] = useState< + '7D' | '1D' | '12H' | '4H' + >('7D'); + + const { fetchGraphData } = useGraphData(webhookId); return ( <> {webhookGraphData.length ? (
- + + + { - const objectMetadataId = variables.filter?.objectMetadataId?.eq; - const viewType = variables.filter?.type?.eq; - + graphql.query('SearchPeople', () => { return HttpResponse.json({ data: { - views: { - edges: mockedViewsData - .filter( - (view) => - view?.objectMetadataId === objectMetadataId && - view?.type === viewType, - ) - .map((view) => ({ - node: view, - cursor: null, - })), + searchPeople: { + edges: peopleMock.slice(0, 3).map((person) => ({ + node: person, + cursor: null, + })), pageInfo: { hasNextPage: false, hasPreviousPage: false, @@ -89,6 +80,85 @@ export const graphqlMocks = { }, }); }), + graphql.query('SearchCompanies', () => { + return HttpResponse.json({ + data: { + searchCompanies: { + edges: companiesMock.slice(0, 3).map((company) => ({ + node: company, + cursor: null, + })), + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }, + }, + }, + }); + }), + graphql.query('SearchOpportunities', () => { + return HttpResponse.json({ + data: { + searchOpportunities: { + edges: [], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }, + }, + }, + }); + }), + graphql.query('SearchWorkspaceMembers', () => { + return HttpResponse.json({ + data: { + searchWorkspaceMembers: { + edges: mockWorkspaceMembers.map((member) => ({ + node: { + ...member, + messageParticipants: { + edges: [], + __typename: 'MessageParticipantConnection', + }, + authoredAttachments: { + edges: [], + __typename: 'AttachmentConnection', + }, + authoredComments: { + edges: [], + __typename: 'CommentConnection', + }, + accountOwnerForCompanies: { + edges: [], + __typename: 'CompanyConnection', + }, + authoredActivities: { + edges: [], + __typename: 'ActivityConnection', + }, + favorites: { + edges: [], + __typename: 'FavoriteConnection', + }, + connectedAccounts: { + edges: [], + __typename: 'ConnectedAccountConnection', + }, + assignedActivities: { + edges: [], + __typename: 'ActivityConnection', + }, + }, + cursor: null, + })), + }, + }, + }); + }), graphql.query('FindManyViewFields', ({ variables }) => { const viewId = variables.filter.view.eq; diff --git a/packages/twenty-postgres/Makefile b/packages/twenty-postgres/Makefile deleted file mode 100644 index 9e56adb19a03..000000000000 --- a/packages/twenty-postgres/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -provision-on-docker: - @docker compose -f docker/docker-compose.yml up - -provision-on-macos-arm: - sh ./macos/arm/provision-postgres-macos-arm.sh - -provision-on-macos-intel: - sh ./macos/intel/provision-postgres-macos-intel.sh - -provision-on-linux: - sh ./linux/provision-postgres-linux.sh - diff --git a/packages/twenty-postgres/docker/docker-compose.yml b/packages/twenty-postgres/docker/docker-compose.yml deleted file mode 100644 index 8db2c32d4e0f..000000000000 --- a/packages/twenty-postgres/docker/docker-compose.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: "3.9" -services: - postgres: - container_name: twenty_postgres - image: twentycrm/twenty-postgres:latest - volumes: - - twenty_db_data:/var/lib/postgresql/data - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres - - POSTGRES_DB=default - ports: - - "5432:5432" -volumes: - twenty_db_data: - name: twenty_db_data - diff --git a/packages/twenty-postgres/init.sql b/packages/twenty-postgres/init.sql deleted file mode 100644 index 16a4788af629..000000000000 --- a/packages/twenty-postgres/init.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE DATABASE "default"; -CREATE DATABASE "test"; -CREATE USER twenty PASSWORD 'twenty'; -ALTER ROLE twenty superuser; diff --git a/packages/twenty-postgres/linux/Dockerfile b/packages/twenty-postgres/linux/Dockerfile deleted file mode 100644 index 871f87dff40c..000000000000 --- a/packages/twenty-postgres/linux/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -ARG IMAGE_TAG='15.5.0-debian-11-r15' - -FROM bitnami/postgresql:${IMAGE_TAG} - -ARG PG_MAIN_VERSION=15 -ARG PG_GRAPHQL_VERSION=1.5.6 -ARG WRAPPERS_VERSION=0.2.0 -ARG TARGETARCH - -USER root - -CMD ["tail", "-f", "/dev/null"] diff --git a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.4.2/pg_graphql--1.4.2.sql b/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.4.2/pg_graphql--1.4.2.sql deleted file mode 100644 index 60aa38168513..000000000000 --- a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.4.2/pg_graphql--1.4.2.sql +++ /dev/null @@ -1,116 +0,0 @@ -/* -This file is auto generated by pgrx. - -The ordering of items is not stable, it is driven by a dependency graph. -*/ - --- src/lib.rs:26 --- pg_graphql::_internal_resolve -CREATE FUNCTION graphql."_internal_resolve"( - "query" TEXT, /* &str */ - "variables" jsonb DEFAULT '{}', /* core::option::Option */ - "operationName" TEXT DEFAULT null, /* core::option::Option */ - "extensions" jsonb DEFAULT null /* core::option::Option */ -) RETURNS jsonb /* pgrx::datum::json::JsonB */ - -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'resolve_wrapper'; - --- src/lib.rs:21 -create or replace function graphql.exception(message text) - returns text - language plpgsql -as $$ -begin - raise exception using errcode='22000', message=message; -end; -$$; - - --- src/lib.rs:19 --- Is updated every time the schema changes -create sequence if not exists graphql.seq_schema_version as int cycle; - -create or replace function graphql.increment_schema_version() - returns event_trigger - security definer - language plpgsql -as $$ -begin - perform nextval('graphql.seq_schema_version'); -end; -$$; - -create or replace function graphql.get_schema_version() - returns int - security definer - language sql -as $$ - select last_value from graphql.seq_schema_version; -$$; - --- On DDL event, increment the schema version number -create event trigger graphql_watch_ddl - on ddl_command_end - execute procedure graphql.increment_schema_version(); - -create event trigger graphql_watch_drop - on sql_drop - execute procedure graphql.increment_schema_version(); - - --- src/lib.rs:22 --- requires: --- resolve - -create or replace function graphql.resolve( - "query" text, - "variables" jsonb default '{}', - "operationName" text default null, - "extensions" jsonb default null -) - returns jsonb - language plpgsql -as $$ -declare - res jsonb; - message_text text; -begin - begin - select graphql._internal_resolve("query" := "query", - "variables" := "variables", - "operationName" := "operationName", - "extensions" := "extensions") into res; - return res; - exception - when others then - get stacked diagnostics message_text = message_text; - return - jsonb_build_object('data', null, - 'errors', jsonb_build_array(jsonb_build_object('message', message_text))); - end; -end; -$$; - - --- src/lib.rs:20 -create function graphql.comment_directive(comment_ text) - returns jsonb - language sql - immutable -as $$ - /* - comment on column public.account.name is '@graphql.name: myField' - */ - select - coalesce( - ( - regexp_match( - comment_, - '@graphql\((.+?)\)' - ) - )[1]::jsonb, - jsonb_build_object() - ) -$$; - diff --git a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.4.2/pg_graphql.control b/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.4.2/pg_graphql.control deleted file mode 100644 index 94f95b90b6a1..000000000000 --- a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.4.2/pg_graphql.control +++ /dev/null @@ -1,6 +0,0 @@ -comment = 'pg_graphql: GraphQL support' -default_version = '1.4.2' -module_pathname = '$libdir/pg_graphql' -relocatable = false -superuser = true -schema = 'graphql' diff --git a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.4.2/pg_graphql.so b/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.4.2/pg_graphql.so deleted file mode 100755 index e5ae0c98e167..000000000000 Binary files a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.4.2/pg_graphql.so and /dev/null differ diff --git a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.1/pg_graphql--1.5.1.sql b/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.1/pg_graphql--1.5.1.sql deleted file mode 100644 index 915d4c190a37..000000000000 --- a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.1/pg_graphql--1.5.1.sql +++ /dev/null @@ -1,116 +0,0 @@ -/* -This file is auto generated by pgrx. - -The ordering of items is not stable, it is driven by a dependency graph. -*/ - --- src/lib.rs:27 --- pg_graphql::_internal_resolve -CREATE FUNCTION graphql."_internal_resolve"( - "query" TEXT, /* &str */ - "variables" jsonb DEFAULT '{}', /* core::option::Option */ - "operationName" TEXT DEFAULT null, /* core::option::Option */ - "extensions" jsonb DEFAULT null /* core::option::Option */ -) RETURNS jsonb /* pgrx::datum::json::JsonB */ - -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'resolve_wrapper'; - --- src/lib.rs:23 --- requires: --- resolve - -create or replace function graphql.resolve( - "query" text, - "variables" jsonb default '{}', - "operationName" text default null, - "extensions" jsonb default null -) - returns jsonb - language plpgsql -as $$ -declare - res jsonb; - message_text text; -begin - begin - select graphql._internal_resolve("query" := "query", - "variables" := "variables", - "operationName" := "operationName", - "extensions" := "extensions") into res; - return res; - exception - when others then - get stacked diagnostics message_text = message_text; - return - jsonb_build_object('data', null, - 'errors', jsonb_build_array(jsonb_build_object('message', message_text))); - end; -end; -$$; - - --- src/lib.rs:22 -create or replace function graphql.exception(message text) - returns text - language plpgsql -as $$ -begin - raise exception using errcode='22000', message=message; -end; -$$; - - --- src/lib.rs:20 --- Is updated every time the schema changes -create sequence if not exists graphql.seq_schema_version as int cycle; - -create or replace function graphql.increment_schema_version() - returns event_trigger - security definer - language plpgsql -as $$ -begin - perform nextval('graphql.seq_schema_version'); -end; -$$; - -create or replace function graphql.get_schema_version() - returns int - security definer - language sql -as $$ - select last_value from graphql.seq_schema_version; -$$; - --- On DDL event, increment the schema version number -create event trigger graphql_watch_ddl - on ddl_command_end - execute procedure graphql.increment_schema_version(); - -create event trigger graphql_watch_drop - on sql_drop - execute procedure graphql.increment_schema_version(); - - --- src/lib.rs:21 -create function graphql.comment_directive(comment_ text) - returns jsonb - language sql - immutable -as $$ - /* - comment on column public.account.name is '@graphql.name: myField' - */ - select - coalesce( - ( - regexp_match( - comment_, - '@graphql\((.+?)\)' - ) - )[1]::jsonb, - jsonb_build_object() - ) -$$; - diff --git a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.1/pg_graphql.control b/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.1/pg_graphql.control deleted file mode 100644 index 172c6f114234..000000000000 --- a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.1/pg_graphql.control +++ /dev/null @@ -1,6 +0,0 @@ -comment = 'pg_graphql: GraphQL support' -default_version = '1.5.1' -module_pathname = '$libdir/pg_graphql' -relocatable = false -superuser = true -schema = 'graphql' diff --git a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.1/pg_graphql.so b/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.1/pg_graphql.so deleted file mode 100755 index 458e9ae82f19..000000000000 Binary files a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.1/pg_graphql.so and /dev/null differ diff --git a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.6/pg_graphql--1.5.6.sql b/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.6/pg_graphql--1.5.6.sql deleted file mode 100644 index b5489e7cd65c..000000000000 --- a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.6/pg_graphql--1.5.6.sql +++ /dev/null @@ -1,116 +0,0 @@ -/* -This file is auto generated by pgrx. - -The ordering of items is not stable, it is driven by a dependency graph. -*/ - --- src/lib.rs:27 --- pg_graphql::_internal_resolve -CREATE FUNCTION graphql."_internal_resolve"( - "query" TEXT, /* &str */ - "variables" jsonb DEFAULT '{}', /* core::option::Option */ - "operationName" TEXT DEFAULT null, /* core::option::Option */ - "extensions" jsonb DEFAULT null /* core::option::Option */ -) RETURNS jsonb /* pgrx::datum::json::JsonB */ - -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'resolve_wrapper'; - --- src/lib.rs:22 -create or replace function graphql.exception(message text) - returns text - language plpgsql -as $$ -begin - raise exception using errcode='22000', message=message; -end; -$$; - - --- src/lib.rs:23 --- requires: --- resolve - -create or replace function graphql.resolve( - "query" text, - "variables" jsonb default '{}', - "operationName" text default null, - "extensions" jsonb default null -) - returns jsonb - language plpgsql -as $$ -declare - res jsonb; - message_text text; -begin - begin - select graphql._internal_resolve("query" := "query", - "variables" := "variables", - "operationName" := "operationName", - "extensions" := "extensions") into res; - return res; - exception - when others then - get stacked diagnostics message_text = message_text; - return - jsonb_build_object('data', null, - 'errors', jsonb_build_array(jsonb_build_object('message', message_text))); - end; -end; -$$; - - --- src/lib.rs:21 -create function graphql.comment_directive(comment_ text) - returns jsonb - language sql - immutable -as $$ - /* - comment on column public.account.name is '@graphql.name: myField' - */ - select - coalesce( - ( - regexp_match( - comment_, - '@graphql\((.+?)\)' - ) - )[1]::jsonb, - jsonb_build_object() - ) -$$; - - --- src/lib.rs:20 --- Is updated every time the schema changes -create sequence if not exists graphql.seq_schema_version as int cycle; - -create or replace function graphql.increment_schema_version() - returns event_trigger - security definer - language plpgsql -as $$ -begin - perform nextval('graphql.seq_schema_version'); -end; -$$; - -create or replace function graphql.get_schema_version() - returns int - security definer - language sql -as $$ - select last_value from graphql.seq_schema_version; -$$; - --- On DDL event, increment the schema version number -create event trigger graphql_watch_ddl - on ddl_command_end - execute procedure graphql.increment_schema_version(); - -create event trigger graphql_watch_drop - on sql_drop - execute procedure graphql.increment_schema_version(); - diff --git a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.6/pg_graphql.control b/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.6/pg_graphql.control deleted file mode 100644 index 56c20ea629f1..000000000000 --- a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.6/pg_graphql.control +++ /dev/null @@ -1,6 +0,0 @@ -comment = 'pg_graphql: GraphQL support' -default_version = '1.5.6' -module_pathname = '$libdir/pg_graphql' -relocatable = false -superuser = true -schema = 'graphql' diff --git a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.6/pg_graphql.so b/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.6/pg_graphql.so deleted file mode 100755 index 9d1667369cb1..000000000000 Binary files a/packages/twenty-postgres/linux/amd64/15/pg_graphql/1.5.6/pg_graphql.so and /dev/null differ diff --git a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.4.2/pg_graphql--1.4.2.sql b/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.4.2/pg_graphql--1.4.2.sql deleted file mode 100644 index 7a238a08c7e7..000000000000 --- a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.4.2/pg_graphql--1.4.2.sql +++ /dev/null @@ -1,116 +0,0 @@ -/* -This file is auto generated by pgrx. - -The ordering of items is not stable, it is driven by a dependency graph. -*/ - --- src/lib.rs:26 --- pg_graphql::_internal_resolve -CREATE FUNCTION graphql."_internal_resolve"( - "query" TEXT, /* &str */ - "variables" jsonb DEFAULT '{}', /* core::option::Option */ - "operationName" TEXT DEFAULT null, /* core::option::Option */ - "extensions" jsonb DEFAULT null /* core::option::Option */ -) RETURNS jsonb /* pgrx::datum::json::JsonB */ - -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'resolve_wrapper'; - --- src/lib.rs:19 --- Is updated every time the schema changes -create sequence if not exists graphql.seq_schema_version as int cycle; - -create or replace function graphql.increment_schema_version() - returns event_trigger - security definer - language plpgsql -as $$ -begin - perform nextval('graphql.seq_schema_version'); -end; -$$; - -create or replace function graphql.get_schema_version() - returns int - security definer - language sql -as $$ - select last_value from graphql.seq_schema_version; -$$; - --- On DDL event, increment the schema version number -create event trigger graphql_watch_ddl - on ddl_command_end - execute procedure graphql.increment_schema_version(); - -create event trigger graphql_watch_drop - on sql_drop - execute procedure graphql.increment_schema_version(); - - --- src/lib.rs:22 --- requires: --- resolve - -create or replace function graphql.resolve( - "query" text, - "variables" jsonb default '{}', - "operationName" text default null, - "extensions" jsonb default null -) - returns jsonb - language plpgsql -as $$ -declare - res jsonb; - message_text text; -begin - begin - select graphql._internal_resolve("query" := "query", - "variables" := "variables", - "operationName" := "operationName", - "extensions" := "extensions") into res; - return res; - exception - when others then - get stacked diagnostics message_text = message_text; - return - jsonb_build_object('data', null, - 'errors', jsonb_build_array(jsonb_build_object('message', message_text))); - end; -end; -$$; - - --- src/lib.rs:21 -create or replace function graphql.exception(message text) - returns text - language plpgsql -as $$ -begin - raise exception using errcode='22000', message=message; -end; -$$; - - --- src/lib.rs:20 -create function graphql.comment_directive(comment_ text) - returns jsonb - language sql - immutable -as $$ - /* - comment on column public.account.name is '@graphql.name: myField' - */ - select - coalesce( - ( - regexp_match( - comment_, - '@graphql\((.+?)\)' - ) - )[1]::jsonb, - jsonb_build_object() - ) -$$; - diff --git a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.4.2/pg_graphql.control b/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.4.2/pg_graphql.control deleted file mode 100644 index 94f95b90b6a1..000000000000 --- a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.4.2/pg_graphql.control +++ /dev/null @@ -1,6 +0,0 @@ -comment = 'pg_graphql: GraphQL support' -default_version = '1.4.2' -module_pathname = '$libdir/pg_graphql' -relocatable = false -superuser = true -schema = 'graphql' diff --git a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.4.2/pg_graphql.so b/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.4.2/pg_graphql.so deleted file mode 100755 index 859ca348da21..000000000000 Binary files a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.4.2/pg_graphql.so and /dev/null differ diff --git a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.1/pg_graphql--1.5.1.sql b/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.1/pg_graphql--1.5.1.sql deleted file mode 100644 index 05ad736a31cc..000000000000 --- a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.1/pg_graphql--1.5.1.sql +++ /dev/null @@ -1,116 +0,0 @@ -/* -This file is auto generated by pgrx. - -The ordering of items is not stable, it is driven by a dependency graph. -*/ - --- src/lib.rs:27 --- pg_graphql::_internal_resolve -CREATE FUNCTION graphql."_internal_resolve"( - "query" TEXT, /* &str */ - "variables" jsonb DEFAULT '{}', /* core::option::Option */ - "operationName" TEXT DEFAULT null, /* core::option::Option */ - "extensions" jsonb DEFAULT null /* core::option::Option */ -) RETURNS jsonb /* pgrx::datum::json::JsonB */ - -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'resolve_wrapper'; - --- src/lib.rs:21 -create function graphql.comment_directive(comment_ text) - returns jsonb - language sql - immutable -as $$ - /* - comment on column public.account.name is '@graphql.name: myField' - */ - select - coalesce( - ( - regexp_match( - comment_, - '@graphql\((.+?)\)' - ) - )[1]::jsonb, - jsonb_build_object() - ) -$$; - - --- src/lib.rs:20 --- Is updated every time the schema changes -create sequence if not exists graphql.seq_schema_version as int cycle; - -create or replace function graphql.increment_schema_version() - returns event_trigger - security definer - language plpgsql -as $$ -begin - perform nextval('graphql.seq_schema_version'); -end; -$$; - -create or replace function graphql.get_schema_version() - returns int - security definer - language sql -as $$ - select last_value from graphql.seq_schema_version; -$$; - --- On DDL event, increment the schema version number -create event trigger graphql_watch_ddl - on ddl_command_end - execute procedure graphql.increment_schema_version(); - -create event trigger graphql_watch_drop - on sql_drop - execute procedure graphql.increment_schema_version(); - - --- src/lib.rs:22 -create or replace function graphql.exception(message text) - returns text - language plpgsql -as $$ -begin - raise exception using errcode='22000', message=message; -end; -$$; - - --- src/lib.rs:23 --- requires: --- resolve - -create or replace function graphql.resolve( - "query" text, - "variables" jsonb default '{}', - "operationName" text default null, - "extensions" jsonb default null -) - returns jsonb - language plpgsql -as $$ -declare - res jsonb; - message_text text; -begin - begin - select graphql._internal_resolve("query" := "query", - "variables" := "variables", - "operationName" := "operationName", - "extensions" := "extensions") into res; - return res; - exception - when others then - get stacked diagnostics message_text = message_text; - return - jsonb_build_object('data', null, - 'errors', jsonb_build_array(jsonb_build_object('message', message_text))); - end; -end; -$$; - diff --git a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.1/pg_graphql.control b/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.1/pg_graphql.control deleted file mode 100644 index 172c6f114234..000000000000 --- a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.1/pg_graphql.control +++ /dev/null @@ -1,6 +0,0 @@ -comment = 'pg_graphql: GraphQL support' -default_version = '1.5.1' -module_pathname = '$libdir/pg_graphql' -relocatable = false -superuser = true -schema = 'graphql' diff --git a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.1/pg_graphql.so b/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.1/pg_graphql.so deleted file mode 100755 index c655afcf7ebf..000000000000 Binary files a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.1/pg_graphql.so and /dev/null differ diff --git a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.6/pg_graphql--1.5.6.sql b/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.6/pg_graphql--1.5.6.sql deleted file mode 100644 index 35a393c223bc..000000000000 --- a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.6/pg_graphql--1.5.6.sql +++ /dev/null @@ -1,116 +0,0 @@ -/* -This file is auto generated by pgrx. - -The ordering of items is not stable, it is driven by a dependency graph. -*/ - --- src/lib.rs:27 --- pg_graphql::_internal_resolve -CREATE FUNCTION graphql."_internal_resolve"( - "query" TEXT, /* &str */ - "variables" jsonb DEFAULT '{}', /* core::option::Option */ - "operationName" TEXT DEFAULT null, /* core::option::Option */ - "extensions" jsonb DEFAULT null /* core::option::Option */ -) RETURNS jsonb /* pgrx::datum::json::JsonB */ - -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'resolve_wrapper'; - --- src/lib.rs:21 -create function graphql.comment_directive(comment_ text) - returns jsonb - language sql - immutable -as $$ - /* - comment on column public.account.name is '@graphql.name: myField' - */ - select - coalesce( - ( - regexp_match( - comment_, - '@graphql\((.+?)\)' - ) - )[1]::jsonb, - jsonb_build_object() - ) -$$; - - --- src/lib.rs:22 -create or replace function graphql.exception(message text) - returns text - language plpgsql -as $$ -begin - raise exception using errcode='22000', message=message; -end; -$$; - - --- src/lib.rs:20 --- Is updated every time the schema changes -create sequence if not exists graphql.seq_schema_version as int cycle; - -create or replace function graphql.increment_schema_version() - returns event_trigger - security definer - language plpgsql -as $$ -begin - perform nextval('graphql.seq_schema_version'); -end; -$$; - -create or replace function graphql.get_schema_version() - returns int - security definer - language sql -as $$ - select last_value from graphql.seq_schema_version; -$$; - --- On DDL event, increment the schema version number -create event trigger graphql_watch_ddl - on ddl_command_end - execute procedure graphql.increment_schema_version(); - -create event trigger graphql_watch_drop - on sql_drop - execute procedure graphql.increment_schema_version(); - - --- src/lib.rs:23 --- requires: --- resolve - -create or replace function graphql.resolve( - "query" text, - "variables" jsonb default '{}', - "operationName" text default null, - "extensions" jsonb default null -) - returns jsonb - language plpgsql -as $$ -declare - res jsonb; - message_text text; -begin - begin - select graphql._internal_resolve("query" := "query", - "variables" := "variables", - "operationName" := "operationName", - "extensions" := "extensions") into res; - return res; - exception - when others then - get stacked diagnostics message_text = message_text; - return - jsonb_build_object('data', null, - 'errors', jsonb_build_array(jsonb_build_object('message', message_text))); - end; -end; -$$; - diff --git a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.6/pg_graphql.control b/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.6/pg_graphql.control deleted file mode 100644 index 56c20ea629f1..000000000000 --- a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.6/pg_graphql.control +++ /dev/null @@ -1,6 +0,0 @@ -comment = 'pg_graphql: GraphQL support' -default_version = '1.5.6' -module_pathname = '$libdir/pg_graphql' -relocatable = false -superuser = true -schema = 'graphql' diff --git a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.6/pg_graphql.so b/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.6/pg_graphql.so deleted file mode 100755 index 462b794534c9..000000000000 Binary files a/packages/twenty-postgres/linux/arm64/15/pg_graphql/1.5.6/pg_graphql.so and /dev/null differ diff --git a/packages/twenty-postgres/linux/build-postgres-linux.sh b/packages/twenty-postgres/linux/build-postgres-linux.sh deleted file mode 100755 index 4b6e3b21831e..000000000000 --- a/packages/twenty-postgres/linux/build-postgres-linux.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/bin/bash - -# Colors -RED=31 -GREEN=32 -BLUE=34 - -# Function to display colored output -echo_header () { - COLOR=$1 - MESSAGE=$2 - echo "\e[${COLOR}m\n=======================================================\e[0m" - echo "\e[${COLOR}m${MESSAGE}\e[0m" - echo "\e[${COLOR}m=======================================================\e[0m" -} - -# Function to handle errors -handle_error () { - echo_header $RED "Error: $1" - exit 1 -} - -cat << "EOF" -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -@@@@@@@#*+=================@@@@@%*+=========++*%@@@@@@@ -@@@@#- .+@@%=. .+@@@@@ -@@@- .*@@%- .#@@@ -@@= .=+++++++++++*#@@@= -++++++++++- %@@ -@@. %@@@@@@@@@@@@@@@+ =%@@@@@@@@@@@@= +@@ -@@. .@@@@@@@@@@@@@@+. -%@@@@@@@@@@@@@@+ +@@ -@@. .@@@@@@@@@@@@*. -#@@#:=@@@@@@@@@@@= +@@ -@@ @@@@@@@@@@#: :#@@#: -@@@@@@@@@@@= +@@ -@@#====#@@@@@@@@#- .*@@@= -@@@@@@@@@@@= +@@ -@@@@@@@@@@@@@@%- .*@@@@# -@@@@@@@@@@@= +@@ -@@@@@@@@@@@@%= +@@@@@@# -@@@@@@@@@@@= +@@ -@@@@@@@@@@@+ =@@@@@@@@# -@@@@@@@@@@@= +@@ -@@@@@@@@@+. -%@@@@@@@@@# -@@@@@@@@@@@= +@@ -@@@@@@@*. -%@@@@@@@@@@@# -@@@@@@@@@@@= +@@ -@@@@@#: :#@@@@@@@@@@@@@# -@@@@@@@@@@@+ +@@ -@@@#: :#@@@@@@@@@@@@@@@# :@@@@@@@@@@@= +@@ -@@= :+*+++++++++++*%@@@. :+++++++++- %@@ -@@ :@@@%. .#@@@ -@@- :@@@@@+: .+@@@@@ -@@@#+===================+%@@@@@@@%*++=======++*%@@@@@@@ -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -EOF - -echo_header $BLUE " DATABASE SETUP" - -PG_MAIN_VERSION=15 -PG_GRAPHQL_VERSION=1.5.6 -CARGO_PGRX_VERSION=0.11.2 -TARGETARCH=$(dpkg --print-architecture) - -# Install PostgresSQL -echo_header $GREEN "Step [1/4]: Installing PostgreSQL..." -apt update -y || handle_error "Failed to update package list." -apt install -y curl || handle_error "Failed to install curl." -apt install -y sudo || handle_error "Failed to install sudo." -apt install build-essential -y || handle_error "Failed to install build-essential." -apt install pkg-config -y || handle_error "Failed to install pkg-config." -apt install libssl-dev -y || handle_error "Failed to install libssl-dev." -apt install libreadline-dev -y || handle_error "Failed to install libreadline-dev." -apt install zlib1g-dev -y || handle_error "Failed to install zlib1g-dev." -apt install unzip -y || handle_error "Failed to install unzip." -apt install libclang-dev -y || handle_error "Failed to install libclang-dev." - -# Install pg_graphql extensions -current_directory=$(pwd) -script_directory="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" - -existing_rust_path=$(which rustc) -if [ -n "$existing_rust_path" ]; then - echo "Uninstalling existing Rust installation..." - rm -rf "$existing_rust_path" -fi - -# To force a reinstall of cargo-pgrx, pass --force to the command below -curl https://sh.rustup.rs -sSf | sh -source "$HOME/.cargo/env" || . "$HOME/.cargo/env" -cargo install --locked cargo-pgrx@$CARGO_PGRX_VERSION --force || handle_error "Failed to install cargo" -cargo pgrx init --pg$PG_MAIN_VERSION download || handle_error "Failed to init postgresql" - -# Create a temporary directory -temp_dir=$(mktemp -d) -cd "$temp_dir" - -curl -LJO https://github.com/supabase/pg_graphql/archive/refs/tags/v$PG_GRAPHQL_VERSION.zip || handle_error "Failed to download pg_graphql package." - -unzip pg_graphql-$PG_GRAPHQL_VERSION.zip - -cd "pg_graphql-$PG_GRAPHQL_VERSION" - -# Apply patches to pg_graphql files -echo "Applying patches to pg_graphql files..." -for patch_file in "/twenty/patches/pg_graphql/"*.patch; do - echo "Applying patch: $patch_file" - patch -p1 < "$patch_file" -done - -echo_header $GREEN "Step [2/4]: Building PostgreSQL service..." -cargo pgrx install --release --pg-config /opt/bitnami/postgresql/bin/pg_config || handle_error "Failed to build postgresql" - - -# Clean up the temporary directory -echo "Cleaning up..." -cd "$current_directory" -rm -rf "$temp_dir" - -# Start postgresql service -echo_header $GREEN "Step [3/4]: Starting PostgreSQL service..." -if sudo service postgresql start; then - echo "PostgreSQL service started successfully." -else - handle_error "Failed to start PostgreSQL service." -fi - -# Run the init.sql to setup database -echo_header $GREEN "Step [4/4]: Setting up database..." -cp ./init.sql /tmp/init.sql -sudo -u postgres psql -f /tmp/init.sql || handle_error "Failed to execute init.sql script." diff --git a/packages/twenty-postgres/linux/build_postgres.md b/packages/twenty-postgres/linux/build_postgres.md deleted file mode 100644 index b727e05fd18a..000000000000 --- a/packages/twenty-postgres/linux/build_postgres.md +++ /dev/null @@ -1,24 +0,0 @@ - -This doc explains how to build postgresql for Twenty - -Build .control, .so and .pg_graphql--version.sql -``` -docker buildx create --name mybuilder -docker buildx use mybuilder -``` - -Do the same for in ['amd64', 'arm64'] ('amd64' builds faster) -``` -cd packages/twenty-postgres -docker buildx build --platform linux/ --load -t twenty-bitnami-postgres- linux -docker run --name twenty-bitnami- -v ~/Desktop/twenty/packages/twenty-postgres:/twenty -``` - -In another terminal -``` -docker exec -it sh -sh twenty/linux/build-postgres-linux.sh -cp opt/bitnami/postgresql/lib/pg_graphql.so twenty/linux//15/pg_graphql/ -cp opt/bitnami/postgresql/share/extension/pg_graphql.control twenty/linux//15/pg_graphql/ -cp opt/bitnami/postgresql/share/extension/pg_graphql--.sql twenty/linux//15/pg_graphql/ -``` diff --git a/packages/twenty-postgres/linux/provision-postgres-linux.sh b/packages/twenty-postgres/linux/provision-postgres-linux.sh deleted file mode 100755 index 84f047a72a82..000000000000 --- a/packages/twenty-postgres/linux/provision-postgres-linux.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/bin/sh - -# Colors -RED=31 -GREEN=32 -BLUE=34 - -# Function to display colored output -echo_header () { - COLOR=$1 - MESSAGE=$2 - echo "\e[${COLOR}m\n=======================================================\e[0m" - echo "\e[${COLOR}m${MESSAGE}\e[0m" - echo "\e[${COLOR}m=======================================================\e[0m" -} - -# Function to handle errors -handle_error () { - echo_header $RED "Error: $1" - exit 1 -} - -read -p "This script uses sudo to install PostgreSQL, curl, and configure the system. Do you want to run this script? [y/N] " AGREEMENT - -if ! echo "$AGREEMENT" | grep -iq "^y"; then - exit 1 -fi - -cat << "EOF" -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -@@@@@@@#*+=================@@@@@%*+=========++*%@@@@@@@ -@@@@#- .+@@%=. .+@@@@@ -@@@- .*@@%- .#@@@ -@@= .=+++++++++++*#@@@= -++++++++++- %@@ -@@. %@@@@@@@@@@@@@@@+ =%@@@@@@@@@@@@= +@@ -@@. .@@@@@@@@@@@@@@+. -%@@@@@@@@@@@@@@+ +@@ -@@. .@@@@@@@@@@@@*. -#@@#:=@@@@@@@@@@@= +@@ -@@ @@@@@@@@@@#: :#@@#: -@@@@@@@@@@@= +@@ -@@#====#@@@@@@@@#- .*@@@= -@@@@@@@@@@@= +@@ -@@@@@@@@@@@@@@%- .*@@@@# -@@@@@@@@@@@= +@@ -@@@@@@@@@@@@%= +@@@@@@# -@@@@@@@@@@@= +@@ -@@@@@@@@@@@+ =@@@@@@@@# -@@@@@@@@@@@= +@@ -@@@@@@@@@+. -%@@@@@@@@@# -@@@@@@@@@@@= +@@ -@@@@@@@*. -%@@@@@@@@@@@# -@@@@@@@@@@@= +@@ -@@@@@#: :#@@@@@@@@@@@@@# -@@@@@@@@@@@+ +@@ -@@@#: :#@@@@@@@@@@@@@@@# :@@@@@@@@@@@= +@@ -@@= :+*+++++++++++*%@@@. :+++++++++- %@@ -@@ :@@@%. .#@@@ -@@- :@@@@@+: .+@@@@@ -@@@#+===================+%@@@@@@@%*++=======++*%@@@@@@@ -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -EOF - -echo_header $BLUE " DATABASE SETUP" - -PG_MAIN_VERSION=15 -PG_GRAPHQL_VERSION=1.5.6 - -if command -v dpkg &> /dev/null; then - TARGETARCH=$(dpkg --print-architecture) -else - TARGETARCH=$(uname -m) -fi - -# Detect package manager and set up PostgreSQL and curl -if command -v dpkg &> /dev/null; then - PACKAGE_MANAGER="dpkg" -elif command -v pacman &> /dev/null; then - PACKAGE_MANAGER="pacman" -else - handle_error "Unsupported package manager. This script only supports dpkg and pacman." -fi - -# Installation for Debian/Ubuntu -if [ "$PACKAGE_MANAGER" = "dpkg" ]; then - echo_header $GREEN "Step [1/4]: Installing PostgreSQL on Debian/Ubuntu..." - sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' - wget -qO- https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo tee /etc/apt/trusted.gpg.d/pgdg.asc &>/dev/null - sudo apt update -y || handle_error "Failed to update package list." - sudo apt install -y postgresql-$PG_MAIN_VERSION postgresql-contrib-$PG_MAIN_VERSION curl || handle_error "Failed to install PostgreSQL or curl." - - echo_header $GREEN "Step [2/4]: Installing GraphQL for PostgreSQL on Debian/Ubuntu..." - curl -L https://github.com/supabase/pg_graphql/releases/download/v$PG_GRAPHQL_VERSION/pg_graphql-v$PG_GRAPHQL_VERSION-pg$PG_MAIN_VERSION-$TARGETARCH-linux-gnu.deb -o pg_graphql.deb || handle_error "Failed to download pg_graphql package." - sudo dpkg --install pg_graphql.deb || handle_error "Failed to install pg_graphql package." - rm pg_graphql.deb - - echo_header $GREEN "Step [3/4]: Starting PostgreSQL service..." - if sudo service postgresql start; then - echo "PostgreSQL service started successfully." - else - handle_error "Failed to start PostgreSQL service." - fi - -# Installation for Arch -elif [ "$PACKAGE_MANAGER" = "pacman" ]; then - echo_header $GREEN "Step [1/4]: Installing PostgreSQL on Arch..." - sudo pacman -Syu --noconfirm || handle_error "Failed to update package list." - sudo pacman -S postgresql postgresql-libs curl --noconfirm || handle_error "Failed to install PostgreSQL or curl." - - echo_header $GREEN "Step [2/4]: Installing GraphQL for PostgreSQL on Arch..." - if ! yay -S --noconfirm pg_graphql && ! paru -S --noconfirm pg_graphql; then - handle_error "Failed to install pg_graphql package from AUR." - fi - - echo_header $GREEN "Step [3/4]: Initializing and starting PostgreSQL service..." - if sudo -u postgres sh -c 'test "$(ls -A /var/lib/postgres/data 2>/dev/null)"'; then - echo "PostgreSQL data directory already contains data. Skipping initdb." - else - sudo -iu postgres initdb --locale en_US.UTF-8 -D /var/lib/postgres/data || handle_error "Failed to initialize PostgreSQL database." - fi - - if [ "$(ps -p 1 -o comm=)" = "systemd" ]; then - sudo systemctl enable postgresql - sudo systemctl start postgresql || handle_error "Failed to start PostgreSQL service." - else - sudo mkdir -p /run/postgresql - sudo chown postgres:postgres /run/postgresql - sudo -iu postgres pg_ctl -D /var/lib/postgres/data -l /var/lib/postgres/logfile start || handle_error "Failed to start PostgreSQL service." - fi -fi - -# Run the init.sql to setup database -echo_header $GREEN "Step [4/4]: Setting up database..." -cp ./init.sql /tmp/init.sql -sudo -u postgres psql -f /tmp/init.sql || handle_error "Failed to execute init.sql script." diff --git a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.4.2/pg_graphql--1.4.2.sql b/packages/twenty-postgres/macos/arm/15/pg_graphql/1.4.2/pg_graphql--1.4.2.sql deleted file mode 100644 index 7bafc8f7df5c..000000000000 --- a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.4.2/pg_graphql--1.4.2.sql +++ /dev/null @@ -1,116 +0,0 @@ -/* -This file is auto generated by pgrx. - -The ordering of items is not stable, it is driven by a dependency graph. -*/ - --- src/lib.rs:26 --- pg_graphql::_internal_resolve -CREATE FUNCTION graphql."_internal_resolve"( - "query" TEXT, /* &str */ - "variables" jsonb DEFAULT '{}', /* core::option::Option */ - "operationName" TEXT DEFAULT null, /* core::option::Option */ - "extensions" jsonb DEFAULT null /* core::option::Option */ -) RETURNS jsonb /* pgrx::datum::json::JsonB */ - -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'resolve_wrapper'; - --- src/lib.rs:19 --- Is updated every time the schema changes -create sequence if not exists graphql.seq_schema_version as int cycle; - -create or replace function graphql.increment_schema_version() - returns event_trigger - security definer - language plpgsql -as $$ -begin - perform nextval('graphql.seq_schema_version'); -end; -$$; - -create or replace function graphql.get_schema_version() - returns int - security definer - language sql -as $$ - select last_value from graphql.seq_schema_version; -$$; - --- On DDL event, increment the schema version number -create event trigger graphql_watch_ddl - on ddl_command_end - execute procedure graphql.increment_schema_version(); - -create event trigger graphql_watch_drop - on sql_drop - execute procedure graphql.increment_schema_version(); - - --- src/lib.rs:20 -create function graphql.comment_directive(comment_ text) - returns jsonb - language sql - immutable -as $$ - /* - comment on column public.account.name is '@graphql.name: myField' - */ - select - coalesce( - ( - regexp_match( - comment_, - '@graphql\((.+?)\)' - ) - )[1]::jsonb, - jsonb_build_object() - ) -$$; - - --- src/lib.rs:22 --- requires: --- resolve - -create or replace function graphql.resolve( - "query" text, - "variables" jsonb default '{}', - "operationName" text default null, - "extensions" jsonb default null -) - returns jsonb - language plpgsql -as $$ -declare - res jsonb; - message_text text; -begin - begin - select graphql._internal_resolve("query" := "query", - "variables" := "variables", - "operationName" := "operationName", - "extensions" := "extensions") into res; - return res; - exception - when others then - get stacked diagnostics message_text = message_text; - return - jsonb_build_object('data', null, - 'errors', jsonb_build_array(jsonb_build_object('message', message_text))); - end; -end; -$$; - - --- src/lib.rs:21 -create or replace function graphql.exception(message text) - returns text - language plpgsql -as $$ -begin - raise exception using errcode='22000', message=message; -end; -$$; - diff --git a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.4.2/pg_graphql.control b/packages/twenty-postgres/macos/arm/15/pg_graphql/1.4.2/pg_graphql.control deleted file mode 100644 index 94f95b90b6a1..000000000000 --- a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.4.2/pg_graphql.control +++ /dev/null @@ -1,6 +0,0 @@ -comment = 'pg_graphql: GraphQL support' -default_version = '1.4.2' -module_pathname = '$libdir/pg_graphql' -relocatable = false -superuser = true -schema = 'graphql' diff --git a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.4.2/pg_graphql.so b/packages/twenty-postgres/macos/arm/15/pg_graphql/1.4.2/pg_graphql.so deleted file mode 100755 index a3abd03724f1..000000000000 Binary files a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.4.2/pg_graphql.so and /dev/null differ diff --git a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.1/pg_graphql--1.5.1.sql b/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.1/pg_graphql--1.5.1.sql deleted file mode 100644 index 81b2d5c4cb21..000000000000 --- a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.1/pg_graphql--1.5.1.sql +++ /dev/null @@ -1,116 +0,0 @@ -/* -This file is auto generated by pgrx. - -The ordering of items is not stable, it is driven by a dependency graph. -*/ - --- src/lib.rs:27 --- pg_graphql::_internal_resolve -CREATE FUNCTION graphql."_internal_resolve"( - "query" TEXT, /* &str */ - "variables" jsonb DEFAULT '{}', /* core::option::Option */ - "operationName" TEXT DEFAULT null, /* core::option::Option */ - "extensions" jsonb DEFAULT null /* core::option::Option */ -) RETURNS jsonb /* pgrx::datum::json::JsonB */ - -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'resolve_wrapper'; - --- src/lib.rs:21 -create function graphql.comment_directive(comment_ text) - returns jsonb - language sql - immutable -as $$ - /* - comment on column public.account.name is '@graphql.name: myField' - */ - select - coalesce( - ( - regexp_match( - comment_, - '@graphql\((.+?)\)' - ) - )[1]::jsonb, - jsonb_build_object() - ) -$$; - - --- src/lib.rs:22 -create or replace function graphql.exception(message text) - returns text - language plpgsql -as $$ -begin - raise exception using errcode='22000', message=message; -end; -$$; - - --- src/lib.rs:23 --- requires: --- resolve - -create or replace function graphql.resolve( - "query" text, - "variables" jsonb default '{}', - "operationName" text default null, - "extensions" jsonb default null -) - returns jsonb - language plpgsql -as $$ -declare - res jsonb; - message_text text; -begin - begin - select graphql._internal_resolve("query" := "query", - "variables" := "variables", - "operationName" := "operationName", - "extensions" := "extensions") into res; - return res; - exception - when others then - get stacked diagnostics message_text = message_text; - return - jsonb_build_object('data', null, - 'errors', jsonb_build_array(jsonb_build_object('message', message_text))); - end; -end; -$$; - - --- src/lib.rs:20 --- Is updated every time the schema changes -create sequence if not exists graphql.seq_schema_version as int cycle; - -create or replace function graphql.increment_schema_version() - returns event_trigger - security definer - language plpgsql -as $$ -begin - perform nextval('graphql.seq_schema_version'); -end; -$$; - -create or replace function graphql.get_schema_version() - returns int - security definer - language sql -as $$ - select last_value from graphql.seq_schema_version; -$$; - --- On DDL event, increment the schema version number -create event trigger graphql_watch_ddl - on ddl_command_end - execute procedure graphql.increment_schema_version(); - -create event trigger graphql_watch_drop - on sql_drop - execute procedure graphql.increment_schema_version(); - diff --git a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.1/pg_graphql.control b/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.1/pg_graphql.control deleted file mode 100644 index 172c6f114234..000000000000 --- a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.1/pg_graphql.control +++ /dev/null @@ -1,6 +0,0 @@ -comment = 'pg_graphql: GraphQL support' -default_version = '1.5.1' -module_pathname = '$libdir/pg_graphql' -relocatable = false -superuser = true -schema = 'graphql' diff --git a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.1/pg_graphql.so b/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.1/pg_graphql.so deleted file mode 100755 index 96ffdaf0d354..000000000000 Binary files a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.1/pg_graphql.so and /dev/null differ diff --git a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.6/pg_graphql--1.5.6.sql b/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.6/pg_graphql--1.5.6.sql deleted file mode 100644 index f7687883c020..000000000000 --- a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.6/pg_graphql--1.5.6.sql +++ /dev/null @@ -1,116 +0,0 @@ -/* -This file is auto generated by pgrx. - -The ordering of items is not stable, it is driven by a dependency graph. -*/ - --- src/lib.rs:27 --- pg_graphql::_internal_resolve -CREATE FUNCTION graphql."_internal_resolve"( - "query" TEXT, /* &str */ - "variables" jsonb DEFAULT '{}', /* core::option::Option */ - "operationName" TEXT DEFAULT null, /* core::option::Option */ - "extensions" jsonb DEFAULT null /* core::option::Option */ -) RETURNS jsonb /* pgrx::datum::json::JsonB */ - -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'resolve_wrapper'; - --- src/lib.rs:22 -create or replace function graphql.exception(message text) - returns text - language plpgsql -as $$ -begin - raise exception using errcode='22000', message=message; -end; -$$; - - --- src/lib.rs:20 --- Is updated every time the schema changes -create sequence if not exists graphql.seq_schema_version as int cycle; - -create or replace function graphql.increment_schema_version() - returns event_trigger - security definer - language plpgsql -as $$ -begin - perform nextval('graphql.seq_schema_version'); -end; -$$; - -create or replace function graphql.get_schema_version() - returns int - security definer - language sql -as $$ - select last_value from graphql.seq_schema_version; -$$; - --- On DDL event, increment the schema version number -create event trigger graphql_watch_ddl - on ddl_command_end - execute procedure graphql.increment_schema_version(); - -create event trigger graphql_watch_drop - on sql_drop - execute procedure graphql.increment_schema_version(); - - --- src/lib.rs:21 -create function graphql.comment_directive(comment_ text) - returns jsonb - language sql - immutable -as $$ - /* - comment on column public.account.name is '@graphql.name: myField' - */ - select - coalesce( - ( - regexp_match( - comment_, - '@graphql\((.+?)\)' - ) - )[1]::jsonb, - jsonb_build_object() - ) -$$; - - --- src/lib.rs:23 --- requires: --- resolve - -create or replace function graphql.resolve( - "query" text, - "variables" jsonb default '{}', - "operationName" text default null, - "extensions" jsonb default null -) - returns jsonb - language plpgsql -as $$ -declare - res jsonb; - message_text text; -begin - begin - select graphql._internal_resolve("query" := "query", - "variables" := "variables", - "operationName" := "operationName", - "extensions" := "extensions") into res; - return res; - exception - when others then - get stacked diagnostics message_text = message_text; - return - jsonb_build_object('data', null, - 'errors', jsonb_build_array(jsonb_build_object('message', message_text))); - end; -end; -$$; - diff --git a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.6/pg_graphql.control b/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.6/pg_graphql.control deleted file mode 100644 index 56c20ea629f1..000000000000 --- a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.6/pg_graphql.control +++ /dev/null @@ -1,6 +0,0 @@ -comment = 'pg_graphql: GraphQL support' -default_version = '1.5.6' -module_pathname = '$libdir/pg_graphql' -relocatable = false -superuser = true -schema = 'graphql' diff --git a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.6/pg_graphql.so b/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.6/pg_graphql.so deleted file mode 100755 index c87cdbd4c66f..000000000000 Binary files a/packages/twenty-postgres/macos/arm/15/pg_graphql/1.5.6/pg_graphql.so and /dev/null differ diff --git a/packages/twenty-postgres/macos/arm/build-postgres-macos-arm.sh b/packages/twenty-postgres/macos/arm/build-postgres-macos-arm.sh deleted file mode 100755 index 3decabfd7c5c..000000000000 --- a/packages/twenty-postgres/macos/arm/build-postgres-macos-arm.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/bin/bash - -# Colors -RED=31 -GREEN=32 -BLUE=34 - -# Function to display colored output -function echo_header { - COLOR=$1 - MESSAGE=$2 - echo -e "\e[${COLOR}m\n=======================================================\e[0m" - echo -e "\e[${COLOR}m${MESSAGE}\e[0m" - echo -e "\e[${COLOR}m=======================================================\e[0m" -} - -# Function to handle errors -function handle_error { - echo_header $RED "Error: $1" - exit 1 -} - -cat << "EOF" -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -@@@@@@@#*+=================@@@@@%*+=========++*%@@@@@@@ -@@@@#- .+@@%=. .+@@@@@ -@@@- .*@@%- .#@@@ -@@= .=+++++++++++*#@@@= -++++++++++- %@@ -@@. %@@@@@@@@@@@@@@@+ =%@@@@@@@@@@@@= +@@ -@@. .@@@@@@@@@@@@@@+. -%@@@@@@@@@@@@@@+ +@@ -@@. .@@@@@@@@@@@@*. -#@@#:=@@@@@@@@@@@= +@@ -@@ @@@@@@@@@@#: :#@@#: -@@@@@@@@@@@= +@@ -@@#====#@@@@@@@@#- .*@@@= -@@@@@@@@@@@= +@@ -@@@@@@@@@@@@@@%- .*@@@@# -@@@@@@@@@@@= +@@ -@@@@@@@@@@@@%= +@@@@@@# -@@@@@@@@@@@= +@@ -@@@@@@@@@@@+ =@@@@@@@@# -@@@@@@@@@@@= +@@ -@@@@@@@@@+. -%@@@@@@@@@# -@@@@@@@@@@@= +@@ -@@@@@@@*. -%@@@@@@@@@@@# -@@@@@@@@@@@= +@@ -@@@@@#: :#@@@@@@@@@@@@@# -@@@@@@@@@@@+ +@@ -@@@#: :#@@@@@@@@@@@@@@@# :@@@@@@@@@@@= +@@ -@@= :+*+++++++++++*%@@@. :+++++++++- %@@ -@@ :@@@%. .#@@@ -@@- :@@@@@+: .+@@@@@ -@@@#+===================+%@@@@@@@%*++=======++*%@@@@@@@ -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -EOF - -echo_header $BLUE " DATABASE SETUP" - -PG_MAIN_VERSION=15 -PG_GRAPHQL_VERSION=1.5.6 -CARGO_PGRX_VERSION=0.11.2 - -current_directory=$(pwd) -script_directory="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" - -# Install PostgresSQL -echo_header $GREEN "Step [1/4]: Installing PostgreSQL..." - -brew reinstall postgresql@$PG_MAIN_VERSION - -# Install pg_graphql extensions -echo_header $GREEN "Step [2/4]: Installing GraphQL for PostgreSQL..." - -# Uninstall existing Rust installation if found -existing_rust_path=$(which rustc) -if [ -n "$existing_rust_path" ]; then - echo "Uninstalling existing Rust installation..." - rm -rf "$existing_rust_path" -fi - -# To force a reinstall of cargo-pgrx, pass --force to the command below -curl https://sh.rustup.rs -sSf | sh -source "$HOME/.cargo/env" -cargo install --locked cargo-pgrx@$CARGO_PGRX_VERSION --force -cargo pgrx init --pg$PG_MAIN_VERSION download - -# Create a temporary directory -temp_dir=$(mktemp -d) -cd "$temp_dir" - -curl -LJO https://github.com/supabase/pg_graphql/archive/refs/tags/v$PG_GRAPHQL_VERSION.zip || handle_error "Failed to download pg_graphql package." - -unzip pg_graphql-$PG_GRAPHQL_VERSION.zip - -[[ ":$PATH:" != *":/opt/homebrew/opt/postgresql@$PG_MAIN_VERSION/bin:"* ]] && PATH="/opt/homebrew/opt/postgresql@$PG_MAIN_VERSION/bin:${PATH}" - -cd "pg_graphql-$PG_GRAPHQL_VERSION" - -# Apply patches to pg_graphql files -echo "Applying patches to pg_graphql files..." -for patch_file in "$script_directory/../../patches/pg_graphql/"*.patch; do - echo "Applying patch: $patch_file" - patch -p1 < "$patch_file" -done - -cargo pgrx install --release --pg-config /opt/homebrew/opt/postgresql@$PG_MAIN_VERSION/bin/pg_config - -# Clean up the temporary directory -echo "Cleaning up..." -cd "$current_directory" -rm -rf "$temp_dir" - -# Start postgresql service -echo_header $GREEN "Step [3/4]: Starting PostgreSQL service..." - - -if brew services start postgresql@$PG_MAIN_VERSION; then - echo "PostgreSQL service started successfully." -else - handle_error "Failed to start PostgreSQL service." -fi - -# Run the init.sql to setup database -echo_header $GREEN "Step [4/4]: Setting up database..." -cp ./postgres/init.sql /tmp/init.sql -psql -f /tmp/init.sql -d postgres|| handle_error "Failed to execute init.sql script." diff --git a/packages/twenty-postgres/macos/arm/provision-postgres-macos-arm.sh b/packages/twenty-postgres/macos/arm/provision-postgres-macos-arm.sh deleted file mode 100755 index 201a4abb2ec0..000000000000 --- a/packages/twenty-postgres/macos/arm/provision-postgres-macos-arm.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -PG_MAIN_VERSION=15 -PG_GRAPHQL_VERSION=1.5.6 - -current_directory=$(pwd) - -echo "Step [1/4]: Installing PostgreSQL..." -brew reinstall postgresql@$PG_MAIN_VERSION - -echo "Step [2/4]: Installing GraphQL for PostgreSQL..." -cp ./macos/arm/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql--${PG_GRAPHQL_VERSION}.sql \ - /opt/homebrew/opt/postgresql@${PG_MAIN_VERSION}/share/postgresql@${PG_MAIN_VERSION}/extension -cp ./macos/arm/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.control \ - /opt/homebrew/opt/postgresql@${PG_MAIN_VERSION}/share/postgresql@${PG_MAIN_VERSION}/extension -cp ./macos/arm/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.so \ - /opt/homebrew/opt/postgresql@${PG_MAIN_VERSION}/lib/postgresql - -export PATH="/opt/homebrew/opt/postgresql@${PG_MAIN_VERSION}/bin:$PATH" - -echo "Step [3/4]: Starting PostgreSQL service..." -brew services restart postgresql@15 - -echo "Step [4/4]: Setting up database..." -cp ./init.sql /tmp/init.sql -sleep 5 -psql -f /tmp/init.sql -d postgres diff --git a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.3.0/pg_graphql--1.3.0.sql b/packages/twenty-postgres/macos/intel/15/pg_graphql/1.3.0/pg_graphql--1.3.0.sql deleted file mode 100644 index 7bafc8f7df5c..000000000000 --- a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.3.0/pg_graphql--1.3.0.sql +++ /dev/null @@ -1,116 +0,0 @@ -/* -This file is auto generated by pgrx. - -The ordering of items is not stable, it is driven by a dependency graph. -*/ - --- src/lib.rs:26 --- pg_graphql::_internal_resolve -CREATE FUNCTION graphql."_internal_resolve"( - "query" TEXT, /* &str */ - "variables" jsonb DEFAULT '{}', /* core::option::Option */ - "operationName" TEXT DEFAULT null, /* core::option::Option */ - "extensions" jsonb DEFAULT null /* core::option::Option */ -) RETURNS jsonb /* pgrx::datum::json::JsonB */ - -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'resolve_wrapper'; - --- src/lib.rs:19 --- Is updated every time the schema changes -create sequence if not exists graphql.seq_schema_version as int cycle; - -create or replace function graphql.increment_schema_version() - returns event_trigger - security definer - language plpgsql -as $$ -begin - perform nextval('graphql.seq_schema_version'); -end; -$$; - -create or replace function graphql.get_schema_version() - returns int - security definer - language sql -as $$ - select last_value from graphql.seq_schema_version; -$$; - --- On DDL event, increment the schema version number -create event trigger graphql_watch_ddl - on ddl_command_end - execute procedure graphql.increment_schema_version(); - -create event trigger graphql_watch_drop - on sql_drop - execute procedure graphql.increment_schema_version(); - - --- src/lib.rs:20 -create function graphql.comment_directive(comment_ text) - returns jsonb - language sql - immutable -as $$ - /* - comment on column public.account.name is '@graphql.name: myField' - */ - select - coalesce( - ( - regexp_match( - comment_, - '@graphql\((.+?)\)' - ) - )[1]::jsonb, - jsonb_build_object() - ) -$$; - - --- src/lib.rs:22 --- requires: --- resolve - -create or replace function graphql.resolve( - "query" text, - "variables" jsonb default '{}', - "operationName" text default null, - "extensions" jsonb default null -) - returns jsonb - language plpgsql -as $$ -declare - res jsonb; - message_text text; -begin - begin - select graphql._internal_resolve("query" := "query", - "variables" := "variables", - "operationName" := "operationName", - "extensions" := "extensions") into res; - return res; - exception - when others then - get stacked diagnostics message_text = message_text; - return - jsonb_build_object('data', null, - 'errors', jsonb_build_array(jsonb_build_object('message', message_text))); - end; -end; -$$; - - --- src/lib.rs:21 -create or replace function graphql.exception(message text) - returns text - language plpgsql -as $$ -begin - raise exception using errcode='22000', message=message; -end; -$$; - diff --git a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.3.0/pg_graphql.control b/packages/twenty-postgres/macos/intel/15/pg_graphql/1.3.0/pg_graphql.control deleted file mode 100644 index f5edd6fc5b58..000000000000 --- a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.3.0/pg_graphql.control +++ /dev/null @@ -1,6 +0,0 @@ -comment = 'pg_graphql: GraphQL support' -default_version = '1.3.0' -module_pathname = '$libdir/pg_graphql' -relocatable = false -superuser = true -schema = 'graphql' diff --git a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.3.0/pg_graphql.so b/packages/twenty-postgres/macos/intel/15/pg_graphql/1.3.0/pg_graphql.so deleted file mode 100644 index 319475bff74f..000000000000 Binary files a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.3.0/pg_graphql.so and /dev/null differ diff --git a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.1/pg_graphql--1.5.1.sql b/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.1/pg_graphql--1.5.1.sql deleted file mode 100644 index ef7ef9aca271..000000000000 --- a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.1/pg_graphql--1.5.1.sql +++ /dev/null @@ -1,116 +0,0 @@ -/* -This file is auto generated by pgrx. - -The ordering of items is not stable, it is driven by a dependency graph. -*/ - --- src/lib.rs:27 --- pg_graphql::_internal_resolve -CREATE FUNCTION graphql."_internal_resolve"( - "query" TEXT, /* &str */ - "variables" jsonb DEFAULT '{}', /* core::option::Option */ - "operationName" TEXT DEFAULT null, /* core::option::Option */ - "extensions" jsonb DEFAULT null /* core::option::Option */ -) RETURNS jsonb /* pgrx::datum::json::JsonB */ - -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'resolve_wrapper'; - --- src/lib.rs:23 --- requires: --- resolve - -create or replace function graphql.resolve( - "query" text, - "variables" jsonb default '{}', - "operationName" text default null, - "extensions" jsonb default null -) - returns jsonb - language plpgsql -as $$ -declare - res jsonb; - message_text text; -begin - begin - select graphql._internal_resolve("query" := "query", - "variables" := "variables", - "operationName" := "operationName", - "extensions" := "extensions") into res; - return res; - exception - when others then - get stacked diagnostics message_text = message_text; - return - jsonb_build_object('data', null, - 'errors', jsonb_build_array(jsonb_build_object('message', message_text))); - end; -end; -$$; - - --- src/lib.rs:22 -create or replace function graphql.exception(message text) - returns text - language plpgsql -as $$ -begin - raise exception using errcode='22000', message=message; -end; -$$; - - --- src/lib.rs:21 -create function graphql.comment_directive(comment_ text) - returns jsonb - language sql - immutable -as $$ - /* - comment on column public.account.name is '@graphql.name: myField' - */ - select - coalesce( - ( - regexp_match( - comment_, - '@graphql\((.+?)\)' - ) - )[1]::jsonb, - jsonb_build_object() - ) -$$; - - --- src/lib.rs:20 --- Is updated every time the schema changes -create sequence if not exists graphql.seq_schema_version as int cycle; - -create or replace function graphql.increment_schema_version() - returns event_trigger - security definer - language plpgsql -as $$ -begin - perform nextval('graphql.seq_schema_version'); -end; -$$; - -create or replace function graphql.get_schema_version() - returns int - security definer - language sql -as $$ - select last_value from graphql.seq_schema_version; -$$; - --- On DDL event, increment the schema version number -create event trigger graphql_watch_ddl - on ddl_command_end - execute procedure graphql.increment_schema_version(); - -create event trigger graphql_watch_drop - on sql_drop - execute procedure graphql.increment_schema_version(); - diff --git a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.1/pg_graphql.control b/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.1/pg_graphql.control deleted file mode 100644 index 172c6f114234..000000000000 --- a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.1/pg_graphql.control +++ /dev/null @@ -1,6 +0,0 @@ -comment = 'pg_graphql: GraphQL support' -default_version = '1.5.1' -module_pathname = '$libdir/pg_graphql' -relocatable = false -superuser = true -schema = 'graphql' diff --git a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.1/pg_graphql.so b/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.1/pg_graphql.so deleted file mode 100644 index 61eda3333e0b..000000000000 Binary files a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.1/pg_graphql.so and /dev/null differ diff --git a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.6/pg_graphql--1.5.6.sql b/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.6/pg_graphql--1.5.6.sql deleted file mode 100644 index a24d69a1aef6..000000000000 --- a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.6/pg_graphql--1.5.6.sql +++ /dev/null @@ -1,116 +0,0 @@ -/* -This file is auto generated by pgrx. - -The ordering of items is not stable, it is driven by a dependency graph. -*/ - --- src/lib.rs:27 --- pg_graphql::_internal_resolve -CREATE FUNCTION graphql."_internal_resolve"( - "query" TEXT, /* &str */ - "variables" jsonb DEFAULT '{}', /* core::option::Option */ - "operationName" TEXT DEFAULT null, /* core::option::Option */ - "extensions" jsonb DEFAULT null /* core::option::Option */ -) RETURNS jsonb /* pgrx::datum::json::JsonB */ - -LANGUAGE c /* Rust */ -AS 'MODULE_PATHNAME', 'resolve_wrapper'; - --- src/lib.rs:20 --- Is updated every time the schema changes -create sequence if not exists graphql.seq_schema_version as int cycle; - -create or replace function graphql.increment_schema_version() - returns event_trigger - security definer - language plpgsql -as $$ -begin - perform nextval('graphql.seq_schema_version'); -end; -$$; - -create or replace function graphql.get_schema_version() - returns int - security definer - language sql -as $$ - select last_value from graphql.seq_schema_version; -$$; - --- On DDL event, increment the schema version number -create event trigger graphql_watch_ddl - on ddl_command_end - execute procedure graphql.increment_schema_version(); - -create event trigger graphql_watch_drop - on sql_drop - execute procedure graphql.increment_schema_version(); - - --- src/lib.rs:22 -create or replace function graphql.exception(message text) - returns text - language plpgsql -as $$ -begin - raise exception using errcode='22000', message=message; -end; -$$; - - --- src/lib.rs:21 -create function graphql.comment_directive(comment_ text) - returns jsonb - language sql - immutable -as $$ - /* - comment on column public.account.name is '@graphql.name: myField' - */ - select - coalesce( - ( - regexp_match( - comment_, - '@graphql\((.+?)\)' - ) - )[1]::jsonb, - jsonb_build_object() - ) -$$; - - --- src/lib.rs:23 --- requires: --- resolve - -create or replace function graphql.resolve( - "query" text, - "variables" jsonb default '{}', - "operationName" text default null, - "extensions" jsonb default null -) - returns jsonb - language plpgsql -as $$ -declare - res jsonb; - message_text text; -begin - begin - select graphql._internal_resolve("query" := "query", - "variables" := "variables", - "operationName" := "operationName", - "extensions" := "extensions") into res; - return res; - exception - when others then - get stacked diagnostics message_text = message_text; - return - jsonb_build_object('data', null, - 'errors', jsonb_build_array(jsonb_build_object('message', message_text))); - end; -end; -$$; - diff --git a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.6/pg_graphql.control b/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.6/pg_graphql.control deleted file mode 100644 index 56c20ea629f1..000000000000 --- a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.6/pg_graphql.control +++ /dev/null @@ -1,6 +0,0 @@ -comment = 'pg_graphql: GraphQL support' -default_version = '1.5.6' -module_pathname = '$libdir/pg_graphql' -relocatable = false -superuser = true -schema = 'graphql' diff --git a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.6/pg_graphql.so b/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.6/pg_graphql.so deleted file mode 100644 index c6f712ec040d..000000000000 Binary files a/packages/twenty-postgres/macos/intel/15/pg_graphql/1.5.6/pg_graphql.so and /dev/null differ diff --git a/packages/twenty-postgres/macos/intel/build-postgres-macos-intel.sh b/packages/twenty-postgres/macos/intel/build-postgres-macos-intel.sh deleted file mode 100755 index 944f64d72326..000000000000 --- a/packages/twenty-postgres/macos/intel/build-postgres-macos-intel.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/bin/bash - -# Colors -RED=31 -GREEN=32 -BLUE=34 - -# Function to display colored output -function echo_header { - COLOR=$1 - MESSAGE=$2 - echo -e "\e[${COLOR}m\n=======================================================\e[0m" - echo -e "\e[${COLOR}m${MESSAGE}\e[0m" - echo -e "\e[${COLOR}m=======================================================\e[0m" -} - -# Function to handle errors -function handle_error { - echo_header $RED "Error: $1" - exit 1 -} - -cat << "EOF" -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -@@@@@@@#*+=================@@@@@%*+=========++*%@@@@@@@ -@@@@#- .+@@%=. .+@@@@@ -@@@- .*@@%- .#@@@ -@@= .=+++++++++++*#@@@= -++++++++++- %@@ -@@. %@@@@@@@@@@@@@@@+ =%@@@@@@@@@@@@= +@@ -@@. .@@@@@@@@@@@@@@+. -%@@@@@@@@@@@@@@+ +@@ -@@. .@@@@@@@@@@@@*. -#@@#:=@@@@@@@@@@@= +@@ -@@ @@@@@@@@@@#: :#@@#: -@@@@@@@@@@@= +@@ -@@#====#@@@@@@@@#- .*@@@= -@@@@@@@@@@@= +@@ -@@@@@@@@@@@@@@%- .*@@@@# -@@@@@@@@@@@= +@@ -@@@@@@@@@@@@%= +@@@@@@# -@@@@@@@@@@@= +@@ -@@@@@@@@@@@+ =@@@@@@@@# -@@@@@@@@@@@= +@@ -@@@@@@@@@+. -%@@@@@@@@@# -@@@@@@@@@@@= +@@ -@@@@@@@*. -%@@@@@@@@@@@# -@@@@@@@@@@@= +@@ -@@@@@#: :#@@@@@@@@@@@@@# -@@@@@@@@@@@+ +@@ -@@@#: :#@@@@@@@@@@@@@@@# :@@@@@@@@@@@= +@@ -@@= :+*+++++++++++*%@@@. :+++++++++- %@@ -@@ :@@@%. .#@@@ -@@- :@@@@@+: .+@@@@@ -@@@#+===================+%@@@@@@@%*++=======++*%@@@@@@@ -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -EOF - -echo_header $BLUE " DATABASE SETUP" - -PG_MAIN_VERSION=15 -PG_GRAPHQL_VERSION=1.5.6 -CARGO_PGRX_VERSION=0.11.2 - -current_directory=$(pwd) -script_directory="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" - -# Install PostgresSQL -echo_header $GREEN "Step [1/4]: Installing PostgreSQL..." - -brew reinstall postgresql@$PG_MAIN_VERSION - -# Install pg_graphql extensions -echo_header $GREEN "Step [2/4]: Installing GraphQL for PostgreSQL..." - -# Uninstall existing Rust installation if found -existing_rust_path=$(which rustc) -if [ -n "$existing_rust_path" ]; then - echo "Uninstalling existing Rust installation..." - rm -rf "$existing_rust_path" -fi - -# To force a reinstall of cargo-pgrx, pass --force to the command below -curl https://sh.rustup.rs -sSf | sh -source "$HOME/.cargo/env" -cargo install --locked cargo-pgrx@$CARGO_PGRX_VERSION --force -cargo pgrx init --pg$PG_MAIN_VERSION download - -# Create a temporary directory -temp_dir=$(mktemp -d) -cd "$temp_dir" - -curl -LJO https://github.com/supabase/pg_graphql/archive/refs/tags/v$PG_GRAPHQL_VERSION.zip || handle_error "Failed to download pg_graphql package." - -unzip pg_graphql-$PG_GRAPHQL_VERSION.zip - -[[ ":$PATH:" != *":/usr/local/opt/postgresql@$PG_MAIN_VERSION/bin:"* ]] && PATH="/usr/local/opt/postgresql@$PG_MAIN_VERSION/bin:${PATH}" - -cd "pg_graphql-$PG_GRAPHQL_VERSION" - -# Apply patches to pg_graphql files -echo "Applying patches to pg_graphql files..." -for patch_file in "$script_directory/../../patches/pg_graphql/"*.patch; do - echo "Applying patch: $patch_file" - patch -p1 < "$patch_file" -done - -cargo pgrx install --release --pg-config /usr/local/opt/postgresql@$PG_MAIN_VERSION/bin/pg_config - -# Clean up the temporary directory -echo "Cleaning up..." -cd "$current_directory" -rm -rf "$temp_dir" - -# Start postgresql service -echo_header $GREEN "Step [3/4]: Starting PostgreSQL service..." - - -if brew services start postgresql@$PG_MAIN_VERSION; then - echo "PostgreSQL service started successfully." -else - handle_error "Failed to start PostgreSQL service." -fi - -# Run the init.sql to setup database -echo_header $GREEN "Step [4/4]: Setting up database..." -cp ./postgres/init.sql /tmp/init.sql -psql -f /tmp/init.sql -d postgres|| handle_error "Failed to execute init.sql script." diff --git a/packages/twenty-postgres/macos/intel/provision-postgres-macos-intel.sh b/packages/twenty-postgres/macos/intel/provision-postgres-macos-intel.sh deleted file mode 100755 index 72091af18798..000000000000 --- a/packages/twenty-postgres/macos/intel/provision-postgres-macos-intel.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -PG_MAIN_VERSION=15 -PG_GRAPHQL_VERSION=1.5.6 - -current_directory=$(pwd) - -echo "Step [1/4]: Installing PostgreSQL..." -brew reinstall postgresql@$PG_MAIN_VERSION - -echo "Step [2/4]: Installing GraphQL for PostgreSQL..." -cp ./macos/intel/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql--${PG_GRAPHQL_VERSION}.sql \ - /usr/local/opt/postgresql@${PG_MAIN_VERSION}/share/postgresql@${PG_MAIN_VERSION}/extension -cp ./macos/intel/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.control \ - /usr/local/opt/postgresql@${PG_MAIN_VERSION}/share/postgresql@${PG_MAIN_VERSION}/extension -cp ./macos/intel/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.so \ - /usr/local/opt/postgresql@${PG_MAIN_VERSION}/lib/postgresql - -export PATH="/usr/local/opt/postgresql@${PG_MAIN_VERSION}/bin:$PATH" - -echo "Step [3/4]: Starting PostgreSQL service..." -brew services restart postgresql@15 - -echo "Step [4/4]: Setting up database..." -cp ./init.sql /tmp/init.sql -sleep 5 -psql -f /tmp/init.sql -d postgres diff --git a/packages/twenty-postgres/patches/pg_graphql/0001-performance.patch b/packages/twenty-postgres/patches/pg_graphql/0001-performance.patch deleted file mode 100644 index e0d35c950e38..000000000000 --- a/packages/twenty-postgres/patches/pg_graphql/0001-performance.patch +++ /dev/null @@ -1,28 +0,0 @@ -diff --git a/sql/load_sql_context.sql b/sql/load_sql_context.sql -index 565e4e3..40cd99e 100644 ---- a/sql/load_sql_context.sql -+++ b/sql/load_sql_context.sql -@@ -95,6 +95,8 @@ select - pg_type pt - left join pg_class tabs - on pt.typrelid = tabs.oid -+ join search_path_oids spo -+ on pt.typnamespace = spo.schema_oid or pt.typnamespace = 'pg_catalog'::regnamespace::oid - ), - jsonb_build_object() - ), -@@ -111,6 +113,8 @@ select - pg_type pt - join pg_class tabs - on pt.typrelid = tabs.oid -+ join search_path_oids spo -+ on pt.typnamespace = spo.schema_oid or pt.typnamespace = 'pg_catalog'::regnamespace::oid - where - pt.typcategory = 'C' - and tabs.relkind = 'c' -@@ -420,4 +424,4 @@ select - jsonb_build_array() - ) - -- ) -+ ); diff --git a/packages/twenty-server/.env.example b/packages/twenty-server/.env.example index 6f8167d6b869..f43d3ed7bca3 100644 --- a/packages/twenty-server/.env.example +++ b/packages/twenty-server/.env.example @@ -50,10 +50,7 @@ SIGN_IN_PREFILLED=true # SENTRY_FRONT_DSN=https://xxx@xxx.ingest.sentry.io/xxx # LOG_LEVELS=error,warn # MESSAGE_QUEUE_TYPE=pg-boss -# REDIS_HOST=127.0.0.1 -# REDIS_PORT=6379 -# REDIS_USERNAME= -# REDIS_PASSWORD= +# REDIS_URL=redis://localhost:6379 # DEMO_WORKSPACE_IDS=REPLACE_ME_WITH_A_RANDOM_UUID # SERVER_URL=http://localhost:3000 # WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION=30 diff --git a/packages/twenty-server/.env.test b/packages/twenty-server/.env.test index ed0c63d78337..e768984fbdc8 100644 --- a/packages/twenty-server/.env.test +++ b/packages/twenty-server/.env.test @@ -11,12 +11,9 @@ EXCEPTION_HANDLER_DRIVER=console SENTRY_DSN=https://ba869cb8fd72d5faeb6643560939cee0@o4505516959793152.ingest.sentry.io/4506660900306944 DEMO_WORKSPACE_IDS=63db4589-590f-42b3-bdf1-85268b3da02f,8de58f3f-7e86-4a0b-998d-b2cbe314ee3a,4d957b72-0b37-4bad-9468-8dc828ee082d,daa0b739-269e-49b6-9be5-5f0215941489,59c15f6a-909a-4495-9cf4-3ce1b0abbb6a,7202cc9d-92da-4b52-a323-d29d38cd3b4e,5f071b0d-646b-411a-94f1-5d9ba9d5c6ac,7bc10973-897b-4767-ab2f-35cdac3b2aec,4b3ba0be-2d29-4b1e-be66-8ac7eb65d000,edfb500d-cc4e-4f22-8e2b-f139a9758a68,eee459c9-9057-4459-ae0d-d51d14c01635,3dd2f505-0075-4217-ba33-fc4244aeaaa9,3d1a9165-3f3f-494e-a99d-f858eae95144,84db6ded-cfce-4aee-9160-6553b05c8143,96fb1540-269b-4d13-af21-2a8268eff8ca,b2463e69-d121-4ea5-80c9-bba82403e93e,5af30c15-867d-49ed-b939-d4856bed8514,b5677aa1-68fa-4818-aaaa-434a07ae2ed4,1ec7fa9a-d6bf-4fa2-a753-9a235d75ee3f,753a6fa2-df27-4c87-8c90-4da78fcb30dd,2138f2f2-bbe9-41df-b483-687a9075f94e,a885cfef-4636-4c3a-9788-1ff6e6b92df5,5458f7fb-9431-47a2-b7a0-32f31d115e23,6c09929f-11c3-4f92-9508-aa0e6b934d1e,57ae0a2c-7a4e-4c7d-8f43-68548e7f1206,cc7f0b85-6868-4c2d-85c5-3ce9977ea346,21871a7f-f067-45ea-989e-44339bb5ad07,c3efedab-84f5-4656-8297-55964b3d26cb,647dcdd1-4540-4003-9f58-fd84d4d759b7,fc5e6857-8d67-47b8-98f2-edeb0671e326,1ad8d72c-1826-40ed-8b44-d15a1d2aab70,eac6c90a-d25d-4c8c-a053-cfbc7cde0afb,023a70de-a85e-43fc-bbc6-757fbf6562f0,f3f0a7fb-1409-443b-8e39-4e58e628796e,62828804-97d4-40ec-82fa-2992a6ce4a81,af5441fe-b16f-4996-87f4-1a433ec53dd6,e8857860-f7b1-4478-9741-1eb9e7c11f2c,6bca9c44-c8c0-49f8-b0b5-1bb2ca7842b8,d50da092-09df-448f-84ea-3ebddfe1d9f6,9efd5d6d-db64-47d4-9ad3-5e4d8b65ff7f,6f089094-2dd2-4b0e-b5b7-8bb52b93ea8e,299b0822-68e9-4bfa-af35-da799012e80e,a3dd579c-93be-45a0-ad35-f518d8ed45dd,023b1b3e-4891-4061-aae0-f34368644f40,50174445-33c5-4482-bb8c-3ef6c511c8cd,9933c048-07a7-4735-9af2-940c2f9b6683,beadc568-3962-46bd-ad4d-06e23b37615b,0cdafc9f-d4c1-4576-837e-d7f6ec28643d,50bb24ce-1709-4928-a87b-d9d9e147a2ab,7690ed72-910d-4357-8e0e-17aa702b0b94,1ad0d69f-60fa-414f-bf79-4f94c2abba43,946d84a4-db4d-48cb-a5d3-03081b5c7e8e,1a080055-d2bf-4b14-8957-88a7d08769b8,ed343e38-e405-4fae-9486-27b09c98bdad,c8bdef75-a98c-4646-a372-3251340d2dea,87a8c6fa-f93e-4950-aff2-5f956ca1a6ba,604781ba-23c2-4220-a717-b5615431fcd9,31af6841-ad9f-4f28-a637-b5c5e6589447,cf067451-7b88-4ff2-a96d-3fc9c5d6fea0,26a8ad5e-29d9-4e7d-aa1f-e6221e8ea32a,fd14db29-e4df-44a7-9b3f-d00384458122,73b477a8-fcf4-4860-a685-65a0a79b8653,82e0f305-4c6c-4160-be1d-b0de834124e6,e38567ab-a6e2-4a94-99c5-a7db31c0aae8,faf3d6dc-66ff-4c1b-9658-f65a9cd9fcf1,6df6bb90-200e-4290-b73d-9bb374554229,2ff10cf4-a871-404a-9e7b-5ca7a232567e,06c614e2-0f36-4b72-8c82-59631680add2,5e508c81-3453-4185-ae8c-4c9b841f8c15,21b5c371-6010-4b1b-be67-7538eb877efb,54e61442-e291-4eea-8d49-7f11b5f85bd2,b6b7260a-4eea-40b0-9f7f-1dfd4c3cc7a8,e163fe76-30fb-44fb-b51a-50cc78745a21,4da672f2-29b4-4a98-b27c-b39a4aecc858,2fdb0601-c882-4aaf-ad49-ae17e530d47a,49525e1b-1b47-4545-a98c-0ba58778179f,f958ab32-b152-4004-9228-18148f7380f1,0ff5025a-62cd-4a10-a722-79f7cf360f01,642df445-e314-409a-a97d-64fc2aa2a15e,38b0dab5-d4fb-44f9-8cf9-bb35cf82e91d,62054133-f35a-4f64-a2ee-a31e48952835,536dbe8c-af33-4eab-a0a8-8d039a00db40,a04998ba-52c9-4538-b6d9-6d04408dbaf2,89016c7a-3d36-4619-a5c6-4f31795eebf7,7708b9a9-776c-46fc-94a4-dc28e7880958,5c92bc69-b328-4c66-a791-a05dbaf7a6f8,ad580a50-80b4-44be-9bc4-f2b57cd23207,36c0241c-891e-4b74-bd10-5e99df96bbc8,a96842ff-18be-4536-a23d-20d973d91621,0ea549b0-9558-4bdf-9944-5abc707c7660,0186c353-5ed2-4c94-b71a-fc0b48c90288,1508a165-2217-4911-b31c-1ea42a08f097,1731e392-dfdf-4fc4-863b-27ae62b0e374,0b245cea-96a6-4a3a-af6a-ef43496c239c,a844e208-7078-43a2-8bd0-86f31498cd3f,53d112b5-87f2-490b-a788-df1f4624f9ad,0d5794d4-3a52-482b-9a6a-f8185018bad1,df877aa6-231c-47fb-9be0-906e61677356,c56c6d1a-3418-49d2-82ce-bd9370668043,6e0b6f34-3cd0-4aa0-ae1f-25f5545dca68 MUTATION_MAXIMUM_RECORD_AFFECTED=100 -MESSAGE_QUEUE_TYPE=pg-boss +MESSAGE_QUEUE_TYPE=bull-mq CACHE_STORAGE_TYPE=redis -REDIS_HOST=127.0.0.1 -REDIS_PORT=6379 -REDIS_USERNAME=default -REDIS_PASSWORD= +REDIS_URL=redis://localhost:6379 AUTH_GOOGLE_ENABLED=false MESSAGING_PROVIDER_GMAIL_ENABLED=false diff --git a/packages/twenty-server/felix b/packages/twenty-server/felix deleted file mode 160000 index a33b01797795..000000000000 --- a/packages/twenty-server/felix +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a33b01797795419edef84f122b5214472648d1ce diff --git a/packages/twenty-server/jest-integration.config.ts b/packages/twenty-server/jest-integration.config.ts index 10b5cc9e2099..deb2ba3ee5a1 100644 --- a/packages/twenty-server/jest-integration.config.ts +++ b/packages/twenty-server/jest-integration.config.ts @@ -11,11 +11,14 @@ const jestConfig: JestConfigWithTsJest = { testEnvironment: 'node', testRegex: '.integration-spec.ts$', modulePathIgnorePatterns: ['/dist'], - globalSetup: '/test/utils/setup-test.ts', - globalTeardown: '/test/utils/teardown-test.ts', + globalSetup: '/test/integration/utils/setup-test.ts', + globalTeardown: '/test/integration/utils/teardown-test.ts', testTimeout: 15000, moduleNameMapper: { - ...pathsToModuleNameMapper(tsConfig.compilerOptions.paths), + ...pathsToModuleNameMapper(tsConfig.compilerOptions.paths, { + prefix: '/../..', + }), + '^test/(.*)$': '/test/$1', 'twenty-emails': '/../twenty-emails/dist/index.js', }, fakeTimers: { diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 7cedf04348bf..c5778aa6493d 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -44,6 +44,7 @@ "monaco-editor-auto-typings": "^0.4.5", "passport": "^0.7.0", "psl": "^1.9.0", + "ts-morph": "^24.0.0", "tsconfig-paths": "^4.2.0", "typeorm": "patch:typeorm@0.3.20#./patches/typeorm+0.3.20.patch", "unzipper": "^0.12.3", diff --git a/packages/twenty-server/project.json b/packages/twenty-server/project.json index ed8ad716b6e0..de8dec76a92d 100644 --- a/packages/twenty-server/project.json +++ b/packages/twenty-server/project.json @@ -162,7 +162,7 @@ "options": { "cwd": "packages/twenty-server", "commands": [ - "nx ts-node-no-deps -- ./scripts/generate-integration-tests/index.ts" + "nx ts-node-no-deps -- ./test/integration/graphql/codegen/index.ts" ], "parallel": false } diff --git a/packages/twenty-server/scripts/setup-db.ts b/packages/twenty-server/scripts/setup-db.ts index cefc1245ecbf..a562a31149a6 100644 --- a/packages/twenty-server/scripts/setup-db.ts +++ b/packages/twenty-server/scripts/setup-db.ts @@ -19,10 +19,6 @@ rawDataSource 'CREATE SCHEMA IF NOT EXISTS "core"', 'create schema "core"', ); - await performQuery( - 'CREATE EXTENSION IF NOT EXISTS "pg_graphql"', - 'create extension pg_graphql', - ); await performQuery( 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"', @@ -66,34 +62,6 @@ rawDataSource true, ); } - - await performQuery( - `COMMENT ON SCHEMA "core" IS '@graphql({"inflect_names": true})';`, - 'inflect names for graphql', - ); - - await performQuery( - ` - DROP FUNCTION IF EXISTS graphql; - CREATE FUNCTION graphql( - "operationName" text default null, - query text default null, - variables jsonb default null, - extensions jsonb default null - ) - returns jsonb - language sql - as $$ - select graphql.resolve( - query := query, - variables := coalesce(variables, '{}'), - "operationName" := "operationName", - extensions := extensions - ); - $$; - `, - 'create function graphql', - ); }) .catch((err) => { console.error('Error during Data Source initialization:', err); diff --git a/packages/twenty-server/src/database/commands/database-command.module.ts b/packages/twenty-server/src/database/commands/database-command.module.ts index f8207c318b24..808785d9f497 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -7,6 +7,7 @@ import { DataSeedDemoWorkspaceCommand } from 'src/database/commands/data-seed-de import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module'; import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command'; import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question'; +import { SimplifySearchVectorExpressionCommandModule } from 'src/database/commands/upgrade-version/0-31/0-32/0-32-simplify-search-vector-expression.module'; import { UpgradeTo0_32CommandModule } from 'src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; @@ -46,6 +47,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp DataSeedDemoWorkspaceModule, WorkspaceCacheStorageModule, WorkspaceMetadataVersionModule, + SimplifySearchVectorExpressionCommandModule, UpgradeTo0_32CommandModule, FeatureFlagModule, ], diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-32/0-32-simplify-search-vector-expression.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-32/0-32-simplify-search-vector-expression.module.ts new file mode 100644 index 000000000000..9e6ea5e2be04 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-32/0-32-simplify-search-vector-expression.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { SimplifySearchVectorExpressionCommand } from 'src/database/commands/upgrade-version/0-31/0-32/0-32-simplify-search-vector-expression'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { SearchModule } from 'src/engine/metadata-modules/search/search.module'; +import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; +import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Workspace], 'core'), + TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'), + WorkspaceSyncMetadataCommandsModule, + SearchModule, + WorkspaceMigrationRunnerModule, + ], + providers: [SimplifySearchVectorExpressionCommand], +}) +export class SimplifySearchVectorExpressionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-32/0-32-simplify-search-vector-expression.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-32/0-32-simplify-search-vector-expression.ts new file mode 100644 index 000000000000..27d75e213c56 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-32/0-32-simplify-search-vector-expression.ts @@ -0,0 +1,115 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { Command } from 'nest-commander'; +import { Repository } from 'typeorm'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { + FieldMetadataEntity, + FieldMetadataType, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { SearchService } from 'src/engine/metadata-modules/search/search.service'; +import { SEARCH_FIELDS_FOR_CUSTOM_OBJECT } from 'src/engine/twenty-orm/custom.workspace-entity'; +import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; +import { + COMPANY_STANDARD_FIELD_IDS, + CUSTOM_OBJECT_STANDARD_FIELD_IDS, + OPPORTUNITY_STANDARD_FIELD_IDS, + PERSON_STANDARD_FIELD_IDS, +} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; +import { SEARCH_FIELDS_FOR_COMPANY } from 'src/modules/company/standard-objects/company.workspace-entity'; +import { SEARCH_FIELDS_FOR_OPPORTUNITY } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity'; +import { SEARCH_FIELDS_FOR_PERSON } from 'src/modules/person/standard-objects/person.workspace-entity'; + +@Command({ + name: 'fix-0.32:simplify-search-vector-expression', + description: 'Replace searchVector with simpler expression', +}) +export class SimplifySearchVectorExpressionCommand extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + private readonly searchService: SearchService, + private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + _options: ActiveWorkspacesCommandOptions, + workspaceIds: string[], + ): Promise { + this.logger.log('Running command to fix migration'); + + for (const workspaceId of workspaceIds) { + this.logger.log(`Running command for workspace ${workspaceId}`); + + try { + const searchVectorFields = await this.fieldMetadataRepository.findBy({ + workspaceId: workspaceId, + type: FieldMetadataType.TS_VECTOR, + }); + + for (const searchVectorField of searchVectorFields) { + let fieldsUsedForSearch: FieldTypeAndNameMetadata[] = []; + + switch (searchVectorField.standardId) { + case CUSTOM_OBJECT_STANDARD_FIELD_IDS.searchVector: { + fieldsUsedForSearch = SEARCH_FIELDS_FOR_CUSTOM_OBJECT; + break; + } + case PERSON_STANDARD_FIELD_IDS.searchVector: { + fieldsUsedForSearch = SEARCH_FIELDS_FOR_PERSON; + break; + } + case COMPANY_STANDARD_FIELD_IDS.searchVector: { + fieldsUsedForSearch = SEARCH_FIELDS_FOR_COMPANY; + break; + } + case OPPORTUNITY_STANDARD_FIELD_IDS.searchVector: { + fieldsUsedForSearch = SEARCH_FIELDS_FOR_OPPORTUNITY; + break; + } + default: { + throw new Error( + `search vector has unexpected standardId: ${searchVectorField.standardId}`, + ); + } + } + + await this.searchService.updateSearchVector( + searchVectorField.objectMetadataId, + fieldsUsedForSearch, + workspaceId, + ); + + await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( + workspaceId, + ); + } + } catch (error) { + this.logger.log( + chalk.red( + `Running command on workspace ${workspaceId} failed with error: ${error}`, + ), + ); + continue; + } finally { + this.logger.log( + chalk.green(`Finished running command for workspace ${workspaceId}.`), + ); + } + + this.logger.log(chalk.green(`Command completed!`)); + } + } +} 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 c618677d3733..97b34a8844c7 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 @@ -55,16 +55,6 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: true, }, - { - key: FeatureFlagKey.IsSearchEnabled, - workspaceId: workspaceId, - value: true, - }, - { - key: FeatureFlagKey.IsWorkspaceMigratedForSearch, - workspaceId: workspaceId, - value: true, - }, { key: FeatureFlagKey.IsAnalyticsV2Enabled, workspaceId: workspaceId, diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1728999374151-addConstraintOnIndexMetadata.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1728999374151-addConstraintOnIndexMetadata.ts new file mode 100644 index 000000000000..9659750f889b --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1728999374151-addConstraintOnIndexMetadata.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddConstraintOnIndex1728999374151 implements MigrationInterface { + name = 'AddConstraintOnIndexMetadata1728999374151'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "metadata"."indexMetadata" ADD CONSTRAINT "IndexOnNameAndWorkspaceIdAndObjectMetadataUnique" UNIQUE ("name", "workspaceId", "objectMetadataId")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "metadata"."indexMetadata" DROP CONSTRAINT "IndexOnNameAndWorkspaceIdAndObjectMetadataUnique"`, + ); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts index 7eb55d60ae95..2330ec472815 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts @@ -361,19 +361,21 @@ export class GraphqlQueryRunnerService { authContext.workspace.id, ); + const resultWithGettersArray = Array.isArray(resultWithGetters) + ? resultWithGetters + : [resultWithGetters]; + await this.workspaceQueryHookService.executePostQueryHooks( authContext, objectMetadataItem.nameSingular, operationName, - Array.isArray(resultWithGetters) - ? resultWithGetters - : [resultWithGetters], + resultWithGettersArray, ); const jobOperation = this.operationNameToJobOperation(operationName); if (jobOperation) { - await this.triggerWebhooks(resultWithGetters, jobOperation, options); + await this.triggerWebhooks(resultWithGettersArray, jobOperation, options); } return resultWithGetters; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts index a0fdbf0d377d..378dfff97fc4 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts @@ -4,20 +4,19 @@ import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/int import { Record as IRecord, OrderByDirection, + RecordFilter, } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { SearchResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant'; -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; +import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { isDefined } from 'src/utils/is-defined'; @Injectable() export class GraphqlQuerySearchResolverService @@ -28,11 +27,19 @@ export class GraphqlQuerySearchResolverService private readonly featureFlagService: FeatureFlagService, ) {} - async resolve( + async resolve< + ObjectRecord extends IRecord = IRecord, + Filter extends RecordFilter = RecordFilter, + >( args: SearchResolverArgs, options: WorkspaceQueryRunnerOptions, ): Promise> { - const { authContext, objectMetadataItem, objectMetadataMap } = options; + const { + authContext, + objectMetadataItem, + objectMetadataMapItem, + objectMetadataMap, + } = options; const repository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( @@ -43,7 +50,7 @@ export class GraphqlQuerySearchResolverService const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); - if (!args.searchInput) { + if (!isDefined(args.searchInput)) { return typeORMObjectRecordsParser.createConnection({ objectRecords: [], objectName: objectMetadataItem.nameSingular, @@ -58,11 +65,27 @@ export class GraphqlQuerySearchResolverService const limit = args?.limit ?? QUERY_MAX_RECORDS; - const resultsWithTsVector = (await repository - .createQueryBuilder() - .where(`"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery(:searchTerms)`, { - searchTerms, - }) + const queryBuilder = repository.createQueryBuilder( + objectMetadataItem.nameSingular, + ); + const graphqlQueryParser = new GraphqlQueryParser( + objectMetadataMapItem.fields, + objectMetadataMap, + ); + + const queryBuilderWithFilter = graphqlQueryParser.applyFilterToBuilder( + queryBuilder, + objectMetadataMapItem.nameSingular, + args.filter ?? ({} as Filter), + ); + + const resultsWithTsVector = (await queryBuilderWithFilter + .andWhere( + searchTerms === '' + ? `"${SEARCH_VECTOR_FIELD.name}" IS NOT NULL` + : `"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery(:searchTerms)`, + searchTerms === '' ? {} : { searchTerms }, + ) .orderBy( `ts_rank("${SEARCH_VECTOR_FIELD.name}", to_tsquery(:searchTerms))`, 'DESC', @@ -88,6 +111,9 @@ export class GraphqlQuerySearchResolverService } private formatSearchTerms(searchTerm: string) { + if (searchTerm === '') { + return ''; + } const words = searchTerm.trim().split(/\s+/); const formattedWords = words.map((word) => { const escapedWord = word.replace(/[\\:'&|!()]/g, '\\$&'); @@ -100,20 +126,6 @@ export class GraphqlQuerySearchResolverService async validate( _args: SearchResolverArgs, - options: WorkspaceQueryRunnerOptions, - ): Promise { - const featureFlagsForWorkspace = - await this.featureFlagService.getWorkspaceFeatureFlags( - options.authContext.workspace.id, - ); - - const isSearchEnabled = featureFlagsForWorkspace.IS_SEARCH_ENABLED; - - if (!isSearchEnabled) { - throw new GraphqlQueryRunnerException( - 'This endpoint is not available yet, please use findMany instead.', - GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } - } + _options: WorkspaceQueryRunnerOptions, + ): Promise {} } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts index 219b185c451e..69bc97777b10 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts @@ -48,8 +48,11 @@ export interface FindDuplicatesResolverArgs< data?: Data[]; } -export interface SearchResolverArgs { +export interface SearchResolverArgs< + Filter extends RecordFilter = RecordFilter, +> { searchInput?: string; + filter?: Filter; limit?: number; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts index 7e1755218730..b50047e62e2a 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts @@ -147,6 +147,10 @@ export const getResolverArgs = ( type: GraphQLInt, isNullable: true, }, + filter: { + kind: InputTypeDefinitionKind.Filter, + isNullable: true, + }, }; default: throw new Error(`Unknown resolver type: ${type}`); diff --git a/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.pre-query-hook.ts b/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.pre-query-hook.ts index 2a90843f1d69..0ef8bab573b7 100644 --- a/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.pre-query-hook.ts +++ b/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.pre-query-hook.ts @@ -101,7 +101,7 @@ export class CreatedByPreQueryHook implements WorkspaceQueryHookInstance { for (const datum of payload.data) { // Front-end can fill the source field - if (createdBy && (!datum.createdBy || !datum.createdBy.name)) { + if (createdBy && (!datum.createdBy || !datum.createdBy?.name)) { datum.createdBy = { ...createdBy, source: datum.createdBy?.source ?? createdBy.source, diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts index 2387ff9d310a..62b215f2691b 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts @@ -16,4 +16,5 @@ export enum AuthExceptionCode { UNAUTHENTICATED = 'UNAUTHENTICATED', INVALID_DATA = 'INVALID_DATA', INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', + OAUTH_ACCESS_DENIED = 'OAUTH_ACCESS_DENIED', } diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts index 6ae9b11d7429..c674569d43bf 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts @@ -9,6 +9,7 @@ import { import { Response } from 'express'; +import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter'; import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter'; import { GoogleOauthGuard } from 'src/engine/core-modules/auth/guards/google-oauth.guard'; import { GoogleProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/google-provider-enabled.guard'; @@ -33,6 +34,7 @@ export class GoogleAuthController { @Get('redirect') @UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard) + @UseFilters(AuthOAuthExceptionFilter) async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) { const { firstName, diff --git a/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts new file mode 100644 index 000000000000..008e7d11032f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts @@ -0,0 +1,34 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + InternalServerErrorException, +} from '@nestjs/common'; + +import { Response } from 'express'; + +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; + +@Catch(AuthException) +export class AuthOAuthExceptionFilter implements ExceptionFilter { + constructor(private readonly environmentService: EnvironmentService) {} + + catch(exception: AuthException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + switch (exception.code) { + case AuthExceptionCode.OAUTH_ACCESS_DENIED: + response + .status(403) + .redirect(this.environmentService.get('FRONT_BASE_URL')); + break; + default: + throw new InternalServerErrorException(exception.message); + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts index dd9fbf17f2c0..f4675888b2e8 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts @@ -1,6 +1,11 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; + @Injectable() export class GoogleOauthGuard extends AuthGuard('google') { constructor() { @@ -14,6 +19,13 @@ export class GoogleOauthGuard extends AuthGuard('google') { const workspaceInviteHash = request.query.inviteHash; const workspacePersonalInviteToken = request.query.inviteToken; + if (request.query.error === 'access_denied') { + throw new AuthException( + 'Google OAuth access denied', + AuthExceptionCode.OAUTH_ACCESS_DENIED, + ); + } + if (workspaceInviteHash && typeof workspaceInviteHash === 'string') { request.params.workspaceInviteHash = workspaceInviteHash; } diff --git a/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module-factory.ts b/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module-factory.ts index ce7cb740a511..5a197a98e9df 100644 --- a/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module-factory.ts +++ b/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module-factory.ts @@ -20,27 +20,18 @@ export const cacheStorageModuleFactory = ( return cacheModuleOptions; } case CacheStorageType.Redis: { - const host = environmentService.get('REDIS_HOST'); - const port = environmentService.get('REDIS_PORT'); + const connectionString = environmentService.get('REDIS_URL'); - if (!(host && port)) { + if (!connectionString) { throw new Error( - `${cacheStorageType} cache storage requires host: ${host} and port: ${port} to be defined, check your .env file`, + `${cacheStorageType} cache storage requires REDIS_URL to be defined, check your .env file`, ); } - const username = environmentService.get('REDIS_USERNAME'); - const password = environmentService.get('REDIS_PASSWORD'); - return { ...cacheModuleOptions, store: redisStore, - socket: { - host, - port, - username, - password, - }, + url: connectionString, }; } default: diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts index 12cf5c3164dd..6001a90f4ade 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts @@ -88,6 +88,9 @@ export class ClientConfig { @Field(() => Boolean) debugMode: boolean; + @Field(() => Boolean) + analyticsEnabled: boolean; + @Field(() => Support) support: Support; diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts index 3615066a4390..f6ba1aaf4abd 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts @@ -48,6 +48,7 @@ export class ClientConfigResolver { 'MUTATION_MAXIMUM_AFFECTED_RECORDS', ), }, + analyticsEnabled: this.environmentService.get('ANALYTICS_ENABLED'), }; return Promise.resolve(clientConfig); diff --git a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts index cb5b2fbe2822..faab8c9d6497 100644 --- a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts @@ -375,14 +375,18 @@ export class EnvironmentVariables { @IsNumber() MUTATION_MAXIMUM_AFFECTED_RECORDS = 100; - REDIS_HOST = '127.0.0.1'; - - @CastToPositiveNumber() - REDIS_PORT = 6379; - - REDIS_USERNAME: string; - - REDIS_PASSWORD: string; + @IsOptional() + @ValidateIf( + (env) => + env.CACHE_STORAGE_TYPE === CacheStorageType.Redis || + env.MESSAGE_QUEUE_TYPE === MessageQueueDriverType.BullMQ, + ) + @IsUrl({ + protocols: ['redis'], + require_tld: false, + allow_underscores: true, + }) + REDIS_URL: string; API_TOKEN_EXPIRES_IN = '100y'; diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index 8483f3089f6b..58282f9deca4 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -10,8 +10,6 @@ export enum FeatureFlagKey { IsMessageThreadSubscriberEnabled = 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED', IsQueryRunnerTwentyORMEnabled = 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED', IsWorkspaceFavoriteEnabled = 'IS_WORKSPACE_FAVORITE_ENABLED', - IsSearchEnabled = 'IS_SEARCH_ENABLED', - IsWorkspaceMigratedForSearch = 'IS_WORKSPACE_MIGRATED_FOR_SEARCH', IsGmailSendEmailScopeEnabled = 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED', IsAnalyticsV2Enabled = 'IS_ANALYTICS_V2_ENABLED', IsUniqueIndexesEnabled = 'IS_UNIQUE_INDEXES_ENABLED', diff --git a/packages/twenty-server/src/engine/core-modules/message-queue/drivers/bullmq.driver.ts b/packages/twenty-server/src/engine/core-modules/message-queue/drivers/bullmq.driver.ts index cf4c414c8902..8525330b5e9a 100644 --- a/packages/twenty-server/src/engine/core-modules/message-queue/drivers/bullmq.driver.ts +++ b/packages/twenty-server/src/engine/core-modules/message-queue/drivers/bullmq.driver.ts @@ -1,15 +1,15 @@ import { OnModuleDestroy } from '@nestjs/common'; -import omitBy from 'lodash.omitby'; import { JobsOptions, Queue, QueueOptions, Worker } from 'bullmq'; +import omitBy from 'lodash.omitby'; import { QueueCronJobOptions, QueueJobOptions, } from 'src/engine/core-modules/message-queue/drivers/interfaces/job-options.interface'; +import { MessageQueueDriver } from 'src/engine/core-modules/message-queue/drivers/interfaces/message-queue-driver.interface'; import { MessageQueueJob } from 'src/engine/core-modules/message-queue/interfaces/message-queue-job.interface'; import { MessageQueueWorkerOptions } from 'src/engine/core-modules/message-queue/interfaces/message-queue-worker-options.interface'; -import { MessageQueueDriver } from 'src/engine/core-modules/message-queue/drivers/interfaces/message-queue-driver.interface'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; diff --git a/packages/twenty-server/src/engine/core-modules/message-queue/message-queue.module-factory.ts b/packages/twenty-server/src/engine/core-modules/message-queue/message-queue.module-factory.ts index ad18f6cd8224..2948b2ad9011 100644 --- a/packages/twenty-server/src/engine/core-modules/message-queue/message-queue.module-factory.ts +++ b/packages/twenty-server/src/engine/core-modules/message-queue/message-queue.module-factory.ts @@ -1,7 +1,12 @@ +import { ConnectionOptions } from 'tls'; + import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { + BullMQDriverFactoryOptions, MessageQueueDriverType, MessageQueueModuleOptions, + PgBossDriverFactoryOptions, + SyncDriverFactoryOptions, } from 'src/engine/core-modules/message-queue/interfaces'; /** @@ -19,7 +24,7 @@ export const messageQueueModuleFactory = async ( return { type: MessageQueueDriverType.Sync, options: {}, - }; + } satisfies SyncDriverFactoryOptions; } case MessageQueueDriverType.PgBoss: { const connectionString = environmentService.get('PG_DATABASE_URL'); @@ -29,25 +34,23 @@ export const messageQueueModuleFactory = async ( options: { connectionString, }, - }; + } satisfies PgBossDriverFactoryOptions; } case MessageQueueDriverType.BullMQ: { - const host = environmentService.get('REDIS_HOST'); - const port = environmentService.get('REDIS_PORT'); - const username = environmentService.get('REDIS_USERNAME'); - const password = environmentService.get('REDIS_PASSWORD'); + const connectionString = environmentService.get('REDIS_URL'); + + if (!connectionString) { + throw new Error( + `${MessageQueueDriverType.BullMQ} message queue requires REDIS_URL to be defined, check your .env file`, + ); + } return { type: MessageQueueDriverType.BullMQ, options: { - connection: { - host, - port, - username, - password, - }, + connection: connectionString as ConnectionOptions, }, - }; + } satisfies BullMQDriverFactoryOptions; } default: throw new Error( diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts index bab186af5231..6d5ab0751d5a 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts @@ -84,7 +84,7 @@ describe('computeParameters', () => { in: 'query', description: `Filters objects returned. Should have the following shape: **field_1[COMPARATOR]:value_1,field_2[COMPARATOR]:value_2... - To filter on nested objects use **field.nestedField[COMPARATOR]:value_1 + To filter on composite type fields use **field.subField[COMPARATOR]:value_1 ** Available comparators are **${Object.values(FilterComparators).join( '**, **', @@ -106,7 +106,7 @@ describe('computeParameters', () => { }, simpleNested: { value: 'emails.primaryEmail[eq]:foo99@example.com', - description: 'A simple nested filter param', + description: 'A simple composite type filter param', }, complex: { value: diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts index f16c6fe436b9..26679a18fc34 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts @@ -74,7 +74,7 @@ export const computeFilterParameters = (): OpenAPIV3_1.ParameterObject => { in: 'query', description: `Filters objects returned. Should have the following shape: **field_1[COMPARATOR]:value_1,field_2[COMPARATOR]:value_2... - To filter on nested objects use **field.nestedField[COMPARATOR]:value_1 + To filter on composite type fields use **field.subField[COMPARATOR]:value_1 ** Available comparators are **${Object.values(FilterComparators).join( '**, **', @@ -97,7 +97,7 @@ export const computeFilterParameters = (): OpenAPIV3_1.ParameterObject => { }, simpleNested: { value: 'emails.primaryEmail[eq]:foo99@example.com', - description: 'A simple nested filter param', + description: 'A simple composite type filter param', }, complex: { value: diff --git a/packages/twenty-server/src/engine/decorators/auth/auth-user.decorator.ts b/packages/twenty-server/src/engine/decorators/auth/auth-user.decorator.ts index 75f3a982e949..35d3ccc08da4 100644 --- a/packages/twenty-server/src/engine/decorators/auth/auth-user.decorator.ts +++ b/packages/twenty-server/src/engine/decorators/auth/auth-user.decorator.ts @@ -15,7 +15,9 @@ export const AuthUser = createParamDecorator( const request = getRequest(ctx); if (!options?.allowUndefined && !request.user) { - throw new ForbiddenException("You're not authorized to do this"); + throw new ForbiddenException( + "You're not authorized to do this. Note: This endpoint requires a user and won't work with just an API key.", + ); } return request.user; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts index f402f5b0b1b0..4291fe44a7a5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts @@ -29,7 +29,6 @@ import { import { generateNullable } from 'src/engine/metadata-modules/field-metadata/utils/generate-nullable'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { RelationMetadataEntity, @@ -76,7 +75,6 @@ export class FieldMetadataService extends TypeOrmQueryService, @InjectRepository(ObjectMetadataEntity, 'metadata') private readonly objectMetadataRepository: Repository, - private readonly objectMetadataService: ObjectMetadataService, private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, private readonly workspaceMigrationService: WorkspaceMigrationService, private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts index fc2f991ce225..b748e2ea5042 100644 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts @@ -7,6 +7,7 @@ import { OneToMany, PrimaryGeneratedColumn, Relation, + Unique, UpdateDateColumn, } from 'typeorm'; @@ -18,6 +19,11 @@ export enum IndexType { GIN = 'GIN', } +@Unique('IndexOnNameAndWorkspaceIdAndObjectMetadataUnique', [ + 'name', + 'workspaceId', + 'objectMetadataId', +]) @Entity('indexMetadata') export class IndexMetadataEntity { @PrimaryGeneratedColumn('uuid') diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts index 362519ef78a4..6b8ff8fcb7c0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts @@ -28,7 +28,7 @@ export class IndexMetadataService { private readonly workspaceMigrationService: WorkspaceMigrationService, ) {} - async createIndex( + async createIndexMetadata( workspaceId: string, objectMetadata: ObjectMetadataEntity, fieldMetadataToIndex: Partial[], @@ -45,24 +45,35 @@ export class IndexMetadataService { const indexName = `IDX_${generateDeterministicIndexName([tableName, ...columnNames])}`; - let savedIndexMetadata: IndexMetadataEntity; + let result: IndexMetadataEntity; + + const existingIndex = await this.indexMetadataRepository.findOne({ + where: { + name: indexName, + workspaceId, + objectMetadataId: objectMetadata.id, + }, + }); + + if (existingIndex) { + throw new Error( + `Index ${indexName} on object metadata ${objectMetadata.nameSingular} already exists`, + ); + } try { - savedIndexMetadata = await this.indexMetadataRepository.save({ + result = await this.indexMetadataRepository.save({ name: indexName, - tableName, indexFieldMetadatas: fieldMetadataToIndex.map( - (fieldMetadata, index) => { - return { - fieldMetadataId: fieldMetadata.id, - order: index, - }; - }, + (fieldMetadata, index) => ({ + fieldMetadataId: fieldMetadata.id, + order: index, + }), ), workspaceId, objectMetadataId: objectMetadata.id, - ...(isDefined(indexType) ? { indexType: indexType } : {}), - isCustom: isCustom, + ...(isDefined(indexType) ? { indexType } : {}), + isCustom, }); } catch (error) { throw new Error( @@ -70,12 +81,40 @@ export class IndexMetadataService { ); } - if (!savedIndexMetadata) { + if (!result) { throw new Error( `Failed to return saved index ${indexName} on object metadata ${objectMetadata.nameSingular}`, ); } + await this.createIndexCreationMigration( + workspaceId, + objectMetadata, + fieldMetadataToIndex, + isUnique, + isCustom, + indexType, + indexWhereClause, + ); + } + + async createIndexCreationMigration( + workspaceId: string, + objectMetadata: ObjectMetadataEntity, + fieldMetadataToIndex: Partial[], + isUnique: boolean, + isCustom: boolean, + indexType?: IndexType, + indexWhereClause?: string, + ) { + const tableName = computeObjectTargetTable(objectMetadata); + + const columnNames: string[] = fieldMetadataToIndex.map( + (fieldMetadata) => fieldMetadata.name as string, + ); + + const indexName = `IDX_${generateDeterministicIndexName([tableName, ...columnNames])}`; + const migration = { name: tableName, action: WorkspaceMigrationTableActionType.ALTER_INDEXES, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts index 14d9d58c2200..46992a6e1556 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts @@ -14,12 +14,12 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature- import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module'; import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor'; import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metadata/object-metadata.resolver'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { RemoteTableRelationsModule } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.module'; +import { SearchModule } from 'src/engine/metadata-modules/search/search.module'; import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; @@ -46,8 +46,8 @@ import { UpdateObjectPayload } from './dtos/update-object.input'; WorkspaceMigrationRunnerModule, WorkspaceMetadataVersionModule, RemoteTableRelationsModule, - IndexMetadataModule, FeatureFlagModule, + SearchModule, ], services: [ObjectMetadataService], resolvers: [ diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 83db047cd859..8ec403f845a6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -5,30 +5,20 @@ import console from 'console'; import { Query, QueryOptions } from '@ptc-org/nestjs-query-core'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; -import { isDefined } from 'class-validator'; import { FindManyOptions, FindOneOptions, In, Repository } from 'typeorm'; import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { FieldMetadataEntity, FieldMetadataType, } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { - computeColumnName, - FieldTypeAndNameMetadata, -} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; -import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service'; +import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input'; import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; -import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants'; import { ObjectMetadataException, ObjectMetadataExceptionCode, @@ -43,8 +33,8 @@ import { import { RelationToDelete } from 'src/engine/metadata-modules/relation-metadata/types/relation-to-delete'; import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service'; import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util'; +import { SearchService } from 'src/engine/metadata-modules/search/search.service'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; -import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { WorkspaceMigrationColumnActionType, @@ -72,7 +62,7 @@ import { createForeignKeyDeterministicUuid, createRelationDeterministicUuid, } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; -import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; +import { isSearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util'; import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; @@ -94,17 +84,14 @@ export class ObjectMetadataService extends TypeOrmQueryService - field.id === createdObjectMetadata.labelIdentifierFieldMetadataId, - ) - : createdObjectMetadata.fields.find( - (field) => field.name === DEFAULT_LABEL_IDENTIFIER_FIELD_NAME, - ); - - if (!isDefined(searchableFieldForCustomObject)) { - throw new Error('No searchable field found for custom object'); - } - - this.workspaceMigrationService.createCustomMigration( - generateMigrationName( - `update-${createdObjectMetadata.nameSingular}-add-searchVector`, - ), - createdObjectMetadata.workspaceId, - [ - { - name: computeTableName( - createdObjectMetadata.nameSingular, - createdObjectMetadata.isCustom, - ), - action: WorkspaceMigrationTableActionType.ALTER, - columns: this.tsVectorColumnActionFactory.handleCreateAction({ - ...searchVectorFieldMetadata, - defaultValue: undefined, - generatedType: 'STORED', - asExpression: getTsVectorColumnExpressionFromFields([ - searchableFieldForCustomObject as FieldTypeAndNameMetadata, - ]), - options: undefined, - } as FieldMetadataInterface), - }, - ], - ); - - await this.indexMetadataService.createIndex( - objectMetadataInput.workspaceId, - createdObjectMetadata, - [searchVectorFieldMetadata], - false, - false, - IndexType.GIN, - ); - } - private async createActivityTargetRelation( workspaceId: string, createdObjectMetadata: ObjectMetadataEntity, diff --git a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts index 90291ad6522a..d9a1850a5df6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts @@ -149,7 +149,7 @@ export class RelationMetadataService extends TypeOrmQueryService, + private readonly tsVectorColumnActionFactory: TsVectorColumnActionFactory, + private readonly indexMetadataService: IndexMetadataService, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + private readonly workspaceMigrationService: WorkspaceMigrationService, + private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, + ) {} + + public async createSearchVectorFieldForObject( + objectMetadataInput: CreateObjectInput, + createdObjectMetadata: ObjectMetadataEntity, + ) { + const searchVectorFieldMetadata = await this.fieldMetadataRepository.save({ + standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.searchVector, + objectMetadataId: createdObjectMetadata.id, + workspaceId: objectMetadataInput.workspaceId, + isCustom: false, + isActive: false, + isSystem: true, + type: FieldMetadataType.TS_VECTOR, + name: SEARCH_VECTOR_FIELD.name, + label: SEARCH_VECTOR_FIELD.label, + description: SEARCH_VECTOR_FIELD.description, + isNullable: true, + }); + + const searchableFieldForCustomObject = + createdObjectMetadata.labelIdentifierFieldMetadataId + ? createdObjectMetadata.fields.find( + (field) => + field.id === createdObjectMetadata.labelIdentifierFieldMetadataId, + ) + : createdObjectMetadata.fields.find( + (field) => field.name === DEFAULT_LABEL_IDENTIFIER_FIELD_NAME, + ); + + if (!isDefined(searchableFieldForCustomObject)) { + throw new Error( + `No searchable field found for custom object (object name: ${createdObjectMetadata.nameSingular})`, + ); + } + + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName(`create-${createdObjectMetadata.nameSingular}`), + createdObjectMetadata.workspaceId, + [ + { + name: computeTableName( + createdObjectMetadata.nameSingular, + createdObjectMetadata.isCustom, + ), + action: WorkspaceMigrationTableActionType.ALTER, + columns: this.tsVectorColumnActionFactory.handleCreateAction({ + ...searchVectorFieldMetadata, + defaultValue: undefined, + generatedType: 'STORED', + asExpression: getTsVectorColumnExpressionFromFields([ + { + type: searchableFieldForCustomObject.type as SearchableFieldType, + name: searchableFieldForCustomObject.name, + }, + ]), + options: undefined, + } as FieldMetadataInterface), + }, + ], + ); + + await this.indexMetadataService.createIndexMetadata( + objectMetadataInput.workspaceId, + createdObjectMetadata, + [searchVectorFieldMetadata], + false, + false, + IndexType.GIN, + ); + } + + public async updateSearchVector( + objectMetadataId: string, + fieldMetadataNameAndTypeForSearch: FieldTypeAndNameMetadata[], + workspaceId: string, + ) { + const objectMetadata = await this.objectMetadataRepository.findOneByOrFail({ + id: objectMetadataId, + }); + + const existingSearchVectorFieldMetadata = + await this.fieldMetadataRepository.findOneByOrFail({ + name: SEARCH_VECTOR_FIELD.name, + objectMetadataId, + }); + + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName(`update-${objectMetadata.nameSingular}`), + workspaceId, + [ + { + name: computeTableName( + objectMetadata.nameSingular, + objectMetadata.isCustom, + ), + action: WorkspaceMigrationTableActionType.ALTER, + columns: this.workspaceMigrationFactory.createColumnActions( + WorkspaceMigrationColumnActionType.ALTER, + existingSearchVectorFieldMetadata, + { + ...existingSearchVectorFieldMetadata, + asExpression: getTsVectorColumnExpressionFromFields( + fieldMetadataNameAndTypeForSearch, + ), + generatedType: 'STORED', // Not stored on fieldMetadata + options: undefined, + }, + ), + }, + ], + ); + + // index needs to be recreated as typeorm deletes then recreates searchVector column at alter + await this.indexMetadataService.createIndexCreationMigration( + workspaceId, + objectMetadata, + [existingSearchVectorFieldMetadata], + false, + false, + IndexType.GIN, + ); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts index 191dc9edf414..7e0fcbf9deec 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts @@ -4,19 +4,24 @@ import { InjectRepository } from '@nestjs/typeorm'; import { basename, dirname, join } from 'path'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; -import { Repository } from 'typeorm'; import deepEqual from 'deep-equal'; +import { Repository } from 'typeorm'; import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception'; import { ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface'; -import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content'; +import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name'; import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name'; +import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version'; +import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files'; +import { getLastLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies'; import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service'; import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; +import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input'; import { ServerlessFunctionEntity, @@ -27,11 +32,6 @@ import { ServerlessFunctionExceptionCode, } from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; import { isDefined } from 'src/utils/is-defined'; -import { getLastLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies'; -import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version'; -import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; -import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files'; -import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name'; @Injectable() export class ServerlessFunctionService extends TypeOrmQueryService { diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts index 7438e7a8559b..0051d7293a65 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts @@ -183,7 +183,10 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory, - _alteredFieldMetadata: FieldMetadataInterface, - _options?: WorkspaceColumnActionOptions, + handleAlterAction( + currentFieldMetadata: FieldMetadataInterface, + alteredFieldMetadata: FieldMetadataInterface, ): WorkspaceMigrationColumnAlter[] { - throw new WorkspaceMigrationException( - `TsVectorColumnActionFactory.handleAlterAction has not been implemented yet.`, - WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA, - ); + return [ + { + action: WorkspaceMigrationColumnActionType.ALTER, + currentColumnDefinition: { + columnName: currentFieldMetadata.name, + columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type), + isNullable: currentFieldMetadata.isNullable ?? true, + defaultValue: undefined, + }, + alteredColumnDefinition: { + columnName: alteredFieldMetadata.name, + columnType: fieldMetadataTypeToColumnType(alteredFieldMetadata.type), + isNullable: alteredFieldMetadata.isNullable ?? true, + defaultValue: undefined, + asExpression: alteredFieldMetadata.asExpression, + generatedType: alteredFieldMetadata.generatedType, + }, + }, + ]; } } diff --git a/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts b/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts index c9dc9264274d..993441f4d017 100644 --- a/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts +++ b/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts @@ -18,7 +18,10 @@ import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace- import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; -import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; +import { + FieldTypeAndNameMetadata, + getTsVectorColumnExpressionFromFields, +} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity'; import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; @@ -26,6 +29,12 @@ import { NoteTargetWorkspaceEntity } from 'src/modules/note/standard-objects/not import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; +export const SEARCH_FIELDS_FOR_CUSTOM_OBJECT: FieldTypeAndNameMetadata[] = [ + { + name: DEFAULT_LABEL_IDENTIFIER_FIELD_NAME, + type: FieldMetadataType.TEXT, + }, +]; @WorkspaceCustomEntity() export class CustomWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceField({ @@ -148,12 +157,9 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity { label: SEARCH_VECTOR_FIELD.label, description: SEARCH_VECTOR_FIELD.description, generatedType: 'STORED', - asExpression: getTsVectorColumnExpressionFromFields([ - { - name: DEFAULT_LABEL_IDENTIFIER_FIELD_NAME, - type: FieldMetadataType.TEXT, - }, - ]), + asExpression: getTsVectorColumnExpressionFromFields( + SEARCH_FIELDS_FOR_CUSTOM_OBJECT, + ), }) @WorkspaceIsNullable() @WorkspaceIsSystem() 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 878a25ed57b0..e95cd6949fc8 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 @@ -402,7 +402,10 @@ export class WorkspaceMigrationRunnerService { enumName: enumName, isArray: migrationColumn.isArray, isNullable: migrationColumn.isNullable, - isUnique: migrationColumn.isUnique, + /* For now unique constraints are created at a higher level + as we need to handle soft-delete and a bug on empty strings + */ + // isUnique: migrationColumn.isUnique, asExpression: migrationColumn.asExpression, generatedType: migrationColumn.generatedType, }), @@ -469,6 +472,8 @@ export class WorkspaceMigrationRunnerService { ), isArray: migrationColumn.alteredColumnDefinition.isArray, isNullable: migrationColumn.alteredColumnDefinition.isNullable, + asExpression: migrationColumn.alteredColumnDefinition.asExpression, + generatedType: migrationColumn.alteredColumnDefinition.generatedType, isUnique: migrationColumn.alteredColumnDefinition.isUnique, }), ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags.ts index 88ec505dd432..4b179f5cd90e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags.ts @@ -1,6 +1 @@ -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; - -export const DEFAULT_FEATURE_FLAGS = [ - FeatureFlagKey.IsSearchEnabled, - FeatureFlagKey.IsWorkspaceMigratedForSearch, -]; +export const DEFAULT_FEATURE_FLAGS = []; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts index 4e10f7ea2b81..00115e2fb36b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts @@ -10,7 +10,6 @@ import { } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; @@ -145,26 +144,13 @@ export class WorkspaceSyncFieldMetadataService { const originalObjectMetadata = originalObjectMetadataMap[standardObjectId]; - let computedStandardFieldMetadataCollection = computeStandardFields( + const computedStandardFieldMetadataCollection = computeStandardFields( standardFieldMetadataCollection, originalObjectMetadata, // We need to provide this for generated relations with custom objects customObjectMetadataCollection, ); - let originalObjectMetadataFields = originalObjectMetadata.fields; - - if (!workspaceFeatureFlagsMap.IS_SEARCH_ENABLED) { - computedStandardFieldMetadataCollection = - computedStandardFieldMetadataCollection.filter( - (field) => field.type !== FieldMetadataType.TS_VECTOR, - ); - - originalObjectMetadataFields = originalObjectMetadataFields.filter( - (field) => field.type !== FieldMetadataType.TS_VECTOR, - ); - } - const fieldComparatorResults = this.workspaceFieldComparator.compare( originalObjectMetadata.id, originalObjectMetadata.fields, @@ -192,24 +178,11 @@ export class WorkspaceSyncFieldMetadataService { // Loop over all custom objects from the DB and compare their fields with standard fields for (const customObjectMetadata of customObjectMetadataCollection) { // Also, maybe it's better to refactor a bit and move generation part into a separate module ? - let standardFieldMetadataCollection = computeStandardFields( + const standardFieldMetadataCollection = computeStandardFields( customObjectStandardFieldMetadataCollection, customObjectMetadata, ); - let customObjectMetadataFields = customObjectMetadata.fields; - - if (!workspaceFeatureFlagsMap.IS_SEARCH_ENABLED) { - standardFieldMetadataCollection = - standardFieldMetadataCollection.filter( - (field) => field.type !== FieldMetadataType.TS_VECTOR, - ); - - customObjectMetadataFields = customObjectMetadataFields.filter( - (field) => field.type !== FieldMetadataType.TS_VECTOR, - ); - } - /** * COMPARE FIELD METADATA */ diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service.ts index d9ee8908ce0e..0173d171e4a4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service.ts @@ -7,10 +7,7 @@ import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/wo import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; -import { - IndexMetadataEntity, - IndexType, -} from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; +import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { WorkspaceMigrationIndexFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory'; @@ -73,7 +70,7 @@ export class WorkspaceSyncIndexMetadataService { const indexMetadataRepository = manager.getRepository(IndexMetadataEntity); - let originalIndexMetadataCollection = await indexMetadataRepository.find({ + const originalIndexMetadataCollection = await indexMetadataRepository.find({ where: { workspaceId: context.workspaceId, objectMetadataId: Any( @@ -87,7 +84,7 @@ export class WorkspaceSyncIndexMetadataService { }); // Generate index metadata from models - let standardIndexMetadataCollection = this.standardIndexFactory.create( + const standardIndexMetadataCollection = this.standardIndexFactory.create( standardObjectMetadataDefinitions, context, originalStandardObjectMetadataMap, @@ -95,15 +92,6 @@ export class WorkspaceSyncIndexMetadataService { workspaceFeatureFlagsMap, ); - if (!workspaceFeatureFlagsMap.IS_SEARCH_ENABLED) { - originalIndexMetadataCollection = originalIndexMetadataCollection.filter( - (index) => index.indexType !== IndexType.GIN, - ); - - standardIndexMetadataCollection = standardIndexMetadataCollection.filter( - (index) => index.indexType !== IndexType.GIN, - ); - } const indexComparatorResults = this.workspaceIndexComparator.compare( originalIndexMetadataCollection, standardIndexMetadataCollection, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts index 8703879e5e64..a7a4f73bd3b7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts @@ -1,5 +1,8 @@ import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; +import { + FieldTypeAndNameMetadata, + getTsVectorColumnExpressionFromFields, +} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; const nameTextField = { name: 'name', type: FieldMetadataType.TEXT }; const nameFullNameField = { @@ -63,14 +66,18 @@ jest.mock( describe('getTsVectorColumnExpressionFromFields', () => { it('should generate correct expression for simple text field', () => { - const fields = [nameTextField]; + const fields = [nameTextField] as FieldTypeAndNameMetadata[]; const result = getTsVectorColumnExpressionFromFields(fields); expect(result).toContain("to_tsvector('simple', COALESCE(\"name\", ''))"); }); it('should handle multiple fields', () => { - const fields = [nameFullNameField, jobTitleTextField, emailsEmailsField]; + const fields = [ + nameFullNameField, + jobTitleTextField, + emailsEmailsField, + ] as FieldTypeAndNameMetadata[]; const result = getTsVectorColumnExpressionFromFields(fields); const expected = ` to_tsvector('simple', COALESCE("nameFirstName", '') || ' ' || COALESCE("nameLastName", '') || ' ' || COALESCE("jobTitle", '') || ' ' || diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts index 83cabf58b278..f4f197ff5259 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts @@ -9,15 +9,27 @@ import { WorkspaceMigrationException, WorkspaceMigrationExceptionCode, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception'; +import { + isSearchableFieldType, + SearchableFieldType, +} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util'; -type FieldTypeAndNameMetadata = { +export type FieldTypeAndNameMetadata = { name: string; - type: FieldMetadataType; + type: SearchableFieldType; }; export const getTsVectorColumnExpressionFromFields = ( fieldsUsedForSearch: FieldTypeAndNameMetadata[], ): string => { + const filteredFieldsUsedForSearch = fieldsUsedForSearch.filter((field) => + isSearchableFieldType(field.type), + ); + + if (filteredFieldsUsedForSearch.length < 1) { + throw new Error('No searchable fields found'); + } + const columnExpressions = fieldsUsedForSearch.flatMap( getColumnExpressionsFromField, ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts new file mode 100644 index 000000000000..bb482ae4a83d --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts @@ -0,0 +1,17 @@ +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; + +const SEARCHABLE_FIELD_TYPES = [ + FieldMetadataType.TEXT, + FieldMetadataType.FULL_NAME, + FieldMetadataType.EMAILS, + FieldMetadataType.ADDRESS, + FieldMetadataType.LINKS, +] as const; + +export type SearchableFieldType = (typeof SEARCHABLE_FIELD_TYPES)[number]; + +export const isSearchableFieldType = ( + type: FieldMetadataType, +): type is SearchableFieldType => { + return SEARCHABLE_FIELD_TYPES.includes(type as SearchableFieldType); +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts index c090d413aa18..16f01b999ad4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts @@ -5,7 +5,6 @@ import { DataSource, QueryFailedError, Repository } from 'typeorm'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; @@ -153,13 +152,6 @@ export class WorkspaceSyncMetadataService { await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( context.workspaceId, ); - - if (workspaceFeatureFlagsMap.IS_SEARCH_ENABLED) { - await this.featureFlagService.enableFeatureFlags( - [FeatureFlagKey.IsWorkspaceMigratedForSearch], - context.workspaceId, - ); - } } catch (error) { this.logger.error('Sync of standard objects failed with:', error); diff --git a/packages/twenty-server/src/modules/code-introspection/__tests__/code-introspection.service.spec.ts b/packages/twenty-server/src/modules/code-introspection/__tests__/code-introspection.service.spec.ts new file mode 100644 index 000000000000..8829699154b5 --- /dev/null +++ b/packages/twenty-server/src/modules/code-introspection/__tests__/code-introspection.service.spec.ts @@ -0,0 +1,106 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { CodeIntrospectionException } from 'src/modules/code-introspection/code-introspection.exception'; +import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service'; + +describe('CodeIntrospectionService', () => { + let service: CodeIntrospectionService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CodeIntrospectionService], + }).compile(); + + service = module.get(CodeIntrospectionService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('analyze', () => { + it('should analyze a function declaration correctly', () => { + const fileContent = ` + function testFunction(param1: string, param2: number): void { + console.log(param1, param2); + } + `; + + const result = service.analyze(fileContent); + + expect(result).toEqual([ + { name: 'param1', type: 'string' }, + { name: 'param2', type: 'number' }, + ]); + }); + + it('should analyze an arrow function correctly', () => { + const fileContent = ` + const testArrowFunction = (param1: string, param2: number): void => { + console.log(param1, param2); + }; + `; + + const result = service.analyze(fileContent); + + expect(result).toEqual([ + { name: 'param1', type: 'string' }, + { name: 'param2', type: 'number' }, + ]); + }); + + it('should return an empty array for files without functions', () => { + const fileContent = ` + const x = 5; + console.log(x); + `; + + const result = service.analyze(fileContent); + + expect(result).toEqual([]); + }); + + it('should throw an exception for multiple function declarations', () => { + const fileContent = ` + function func1(param1: string) {} + function func2(param2: number) {} + `; + + expect(() => service.analyze(fileContent)).toThrow( + CodeIntrospectionException, + ); + expect(() => service.analyze(fileContent)).toThrow( + 'Only one function is allowed', + ); + }); + + it('should throw an exception for multiple arrow functions', () => { + const fileContent = ` + const func1 = (param1: string) => {}; + const func2 = (param2: number) => {}; + `; + + expect(() => service.analyze(fileContent)).toThrow( + CodeIntrospectionException, + ); + expect(() => service.analyze(fileContent)).toThrow( + 'Only one arrow function is allowed', + ); + }); + + it('should correctly analyze complex types', () => { + const fileContent = ` + function complexFunction(param1: string[], param2: { key: number }): Promise { + return Promise.resolve(true); + } + `; + + const result = service.analyze(fileContent); + + expect(result).toEqual([ + { name: 'param1', type: 'string[]' }, + { name: 'param2', type: '{ key: number; }' }, + ]); + }); + }); +}); diff --git a/packages/twenty-server/src/modules/code-introspection/code-introspection.exception.ts b/packages/twenty-server/src/modules/code-introspection/code-introspection.exception.ts new file mode 100644 index 000000000000..22ebbd7bf300 --- /dev/null +++ b/packages/twenty-server/src/modules/code-introspection/code-introspection.exception.ts @@ -0,0 +1,12 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class CodeIntrospectionException extends CustomException { + code: CodeIntrospectionExceptionCode; + constructor(message: string, code: CodeIntrospectionExceptionCode) { + super(message, code); + } +} + +export enum CodeIntrospectionExceptionCode { + ONLY_ONE_FUNCTION_ALLOWED = 'ONLY_ONE_FUNCTION_ALLOWED', +} diff --git a/packages/twenty-server/src/modules/code-introspection/code-introspection.module.ts b/packages/twenty-server/src/modules/code-introspection/code-introspection.module.ts new file mode 100644 index 000000000000..d12a94ecf4ee --- /dev/null +++ b/packages/twenty-server/src/modules/code-introspection/code-introspection.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service'; + +@Module({ + providers: [CodeIntrospectionService], + exports: [CodeIntrospectionService], +}) +export class CodeIntrospectionModule {} diff --git a/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts b/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts new file mode 100644 index 000000000000..31e18b25fe09 --- /dev/null +++ b/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@nestjs/common'; + +import { + ArrowFunction, + FunctionDeclaration, + ParameterDeclaration, + Project, + SyntaxKind, +} from 'ts-morph'; + +import { + CodeIntrospectionException, + CodeIntrospectionExceptionCode, +} from 'src/modules/code-introspection/code-introspection.exception'; + +type FunctionParameter = { + name: string; + type: string; +}; + +@Injectable() +export class CodeIntrospectionService { + private project: Project; + + constructor() { + this.project = new Project(); + } + + public analyze( + fileContent: string, + fileName = 'temp.ts', + ): FunctionParameter[] { + const sourceFile = this.project.createSourceFile(fileName, fileContent, { + overwrite: true, + }); + + const functionDeclarations = sourceFile.getFunctions(); + + if (functionDeclarations.length > 0) { + return this.analyzeFunctions(functionDeclarations); + } + + const arrowFunctions = sourceFile.getDescendantsOfKind( + SyntaxKind.ArrowFunction, + ); + + if (arrowFunctions.length > 0) { + return this.analyzeArrowFunctions(arrowFunctions); + } + + return []; + } + + private analyzeFunctions( + functionDeclarations: FunctionDeclaration[], + ): FunctionParameter[] { + if (functionDeclarations.length > 1) { + throw new CodeIntrospectionException( + 'Only one function is allowed', + CodeIntrospectionExceptionCode.ONLY_ONE_FUNCTION_ALLOWED, + ); + } + + const functionDeclaration = functionDeclarations[0]; + + return functionDeclaration.getParameters().map(this.buildFunctionParameter); + } + + private analyzeArrowFunctions( + arrowFunctions: ArrowFunction[], + ): FunctionParameter[] { + if (arrowFunctions.length > 1) { + throw new CodeIntrospectionException( + 'Only one arrow function is allowed', + CodeIntrospectionExceptionCode.ONLY_ONE_FUNCTION_ALLOWED, + ); + } + + const arrowFunction = arrowFunctions[0]; + + return arrowFunction.getParameters().map(this.buildFunctionParameter); + } + + private buildFunctionParameter( + parameter: ParameterDeclaration, + ): FunctionParameter { + return { + name: parameter.getName(), + type: parameter.getType().getText(), + }; + } +} diff --git a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts index 1abbf5dd3c28..97572f3d1f19 100644 --- a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts +++ b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts @@ -25,7 +25,10 @@ import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace- import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; import { COMPANY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; -import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; +import { + FieldTypeAndNameMetadata, + getTsVectorColumnExpressionFromFields, +} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity'; import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; @@ -39,6 +42,11 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta const NAME_FIELD_NAME = 'name'; const DOMAIN_NAME_FIELD_NAME = 'domainName'; +export const SEARCH_FIELDS_FOR_COMPANY: FieldTypeAndNameMetadata[] = [ + { name: NAME_FIELD_NAME, type: FieldMetadataType.TEXT }, + { name: DOMAIN_NAME_FIELD_NAME, type: FieldMetadataType.LINKS }, +]; + @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.company, namePlural: 'companies', @@ -292,10 +300,9 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { description: SEARCH_VECTOR_FIELD.description, icon: 'IconUser', generatedType: 'STORED', - asExpression: getTsVectorColumnExpressionFromFields([ - { name: NAME_FIELD_NAME, type: FieldMetadataType.TEXT }, - { name: DOMAIN_NAME_FIELD_NAME, type: FieldMetadataType.LINKS }, - ]), + asExpression: getTsVectorColumnExpressionFromFields( + SEARCH_FIELDS_FOR_COMPANY, + ), }) @WorkspaceIsNullable() @WorkspaceIsSystem() diff --git a/packages/twenty-server/src/modules/mail-sender/workflow-actions/send-email.workflow-action.ts b/packages/twenty-server/src/modules/mail-sender/workflow-actions/send-email.workflow-action.ts index 026b4537d898..103045f88f57 100644 --- a/packages/twenty-server/src/modules/mail-sender/workflow-actions/send-email.workflow-action.ts +++ b/packages/twenty-server/src/modules/mail-sender/workflow-actions/send-email.workflow-action.ts @@ -1,40 +1,37 @@ import { Injectable, Logger } from '@nestjs/common'; -import { z } from 'zod'; -import Handlebars from 'handlebars'; -import { JSDOM } from 'jsdom'; import DOMPurify from 'dompurify'; +import { JSDOM } from 'jsdom'; +import { z } from 'zod'; + +import { WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface'; -import { WorkflowActionResult } from 'src/modules/workflow/workflow-executor/types/workflow-action-result.type'; -import { WorkflowSendEmailStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; -import { EmailService } from 'src/engine/core-modules/email/email.service'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { - WorkflowStepExecutorException, - WorkflowStepExecutorExceptionCode, -} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { MailSenderException, MailSenderExceptionCode, } from 'src/modules/mail-sender/exceptions/mail-sender.exception'; import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { + WorkflowStepExecutorException, + WorkflowStepExecutorExceptionCode, +} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception'; +import { WorkflowActionResult } from 'src/modules/workflow/workflow-executor/types/workflow-action-result.type'; +import { WorkflowSendEmailStepInput } from 'src/modules/workflow/workflow-executor/types/workflow-step-settings.type'; import { isDefined } from 'src/utils/is-defined'; @Injectable() -export class SendEmailWorkflowAction { +export class SendEmailWorkflowAction implements WorkflowAction { private readonly logger = new Logger(SendEmailWorkflowAction.name); constructor( - private readonly environmentService: EnvironmentService, - private readonly emailService: EmailService, private readonly gmailClientProvider: GmailClientProvider, private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} - private async getEmailClient(step: WorkflowSendEmailStep) { + private async getEmailClient(connectedAccountId: string) { const { workspaceId } = this.scopedWorkspaceContextFactory.create(); if (!workspaceId) { @@ -50,12 +47,12 @@ export class SendEmailWorkflowAction { 'connectedAccount', ); const connectedAccount = await connectedAccountRepository.findOneBy({ - id: step.settings.connectedAccountId, + id: connectedAccountId, }); if (!isDefined(connectedAccount)) { throw new MailSenderException( - `Connected Account '${step.settings.connectedAccountId}' not found`, + `Connected Account '${connectedAccountId}' not found`, MailSenderExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND, ); } @@ -71,39 +68,32 @@ export class SendEmailWorkflowAction { } } - async execute({ - step, - payload, - }: { - step: WorkflowSendEmailStep; - payload: { - email: string; - [key: string]: string; - }; - }): Promise { - const emailProvider = await this.getEmailClient(step); + async execute( + workflowStepInput: WorkflowSendEmailStepInput, + ): Promise { + const emailProvider = await this.getEmailClient( + workflowStepInput.connectedAccountId, + ); + const { email, body, subject } = workflowStepInput; try { const emailSchema = z.string().trim().email('Invalid email'); - const result = emailSchema.safeParse(payload.email); + const result = emailSchema.safeParse(email); if (!result.success) { - this.logger.warn(`Email '${payload.email}' invalid`); + this.logger.warn(`Email '${email}' invalid`); return { result: { success: false } }; } - const body = Handlebars.compile(step.settings.body)(payload); - const subject = Handlebars.compile(step.settings.subject)(payload); - const window = new JSDOM('').window; const purify = DOMPurify(window); const safeBody = purify.sanitize(body || ''); const safeSubject = purify.sanitize(subject || ''); const message = [ - `To: ${payload.email}`, + `To: ${email}`, `Subject: ${safeSubject || ''}`, 'MIME-Version: 1.0', 'Content-Type: text/plain; charset="UTF-8"', diff --git a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts index 52f1a449f312..22b3fae96a0e 100644 --- a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts +++ b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts @@ -24,7 +24,10 @@ import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace- import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; import { OPPORTUNITY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; -import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; +import { + FieldTypeAndNameMetadata, + getTsVectorColumnExpressionFromFields, +} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity'; import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity'; @@ -36,6 +39,10 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o const NAME_FIELD_NAME = 'name'; +export const SEARCH_FIELDS_FOR_OPPORTUNITY: FieldTypeAndNameMetadata[] = [ + { name: NAME_FIELD_NAME, type: FieldMetadataType.TEXT }, +]; + @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.opportunity, namePlural: 'opportunities', @@ -245,9 +252,9 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity { description: SEARCH_VECTOR_FIELD.description, icon: 'IconUser', generatedType: 'STORED', - asExpression: getTsVectorColumnExpressionFromFields([ - { name: NAME_FIELD_NAME, type: FieldMetadataType.TEXT }, - ]), + asExpression: getTsVectorColumnExpressionFromFields( + SEARCH_FIELDS_FOR_OPPORTUNITY, + ), }) @WorkspaceIsNullable() @WorkspaceIsSystem() diff --git a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts index 755308cee867..fc56dbebae23 100644 --- a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts +++ b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts @@ -22,11 +22,15 @@ import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator'; import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; +import { WorkspaceIsUnique } from 'src/engine/twenty-orm/decorators/workspace-is-unique.decorator'; import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator'; import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; import { PERSON_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; -import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; +import { + FieldTypeAndNameMetadata, + getTsVectorColumnExpressionFromFields, +} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity'; import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; @@ -42,6 +46,12 @@ const NAME_FIELD_NAME = 'name'; const EMAILS_FIELD_NAME = 'emails'; const JOB_TITLE_FIELD_NAME = 'jobTitle'; +export const SEARCH_FIELDS_FOR_PERSON: FieldTypeAndNameMetadata[] = [ + { name: NAME_FIELD_NAME, type: FieldMetadataType.FULL_NAME }, + { name: EMAILS_FIELD_NAME, type: FieldMetadataType.EMAILS }, + { name: JOB_TITLE_FIELD_NAME, type: FieldMetadataType.TEXT }, +]; + @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.person, namePlural: 'people', @@ -70,10 +80,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { description: 'Contact鈥檚 Emails', icon: 'IconMail', }) - /* - TODO: add back once we handle TEXT Unique index properly @WorkspaceIsUnique() - */ [EMAILS_FIELD_NAME]: EmailsMetadata; @WorkspaceField({ @@ -300,11 +307,9 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { description: SEARCH_VECTOR_FIELD.description, icon: 'IconUser', generatedType: 'STORED', - asExpression: getTsVectorColumnExpressionFromFields([ - { name: NAME_FIELD_NAME, type: FieldMetadataType.FULL_NAME }, - { name: EMAILS_FIELD_NAME, type: FieldMetadataType.EMAILS }, - { name: JOB_TITLE_FIELD_NAME, type: FieldMetadataType.TEXT }, - ]), + asExpression: getTsVectorColumnExpressionFromFields( + SEARCH_FIELDS_FOR_PERSON, + ), }) @WorkspaceIsNullable() @WorkspaceIsSystem() diff --git a/packages/twenty-server/src/modules/serverless/workflow-actions/code.workflow-action.ts b/packages/twenty-server/src/modules/serverless/workflow-actions/code.workflow-action.ts index 1e42dcf81eb0..f239c3c79490 100644 --- a/packages/twenty-server/src/modules/serverless/workflow-actions/code.workflow-action.ts +++ b/packages/twenty-server/src/modules/serverless/workflow-actions/code.workflow-action.ts @@ -1,28 +1,26 @@ import { Injectable } from '@nestjs/common'; +import { WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface'; + import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; -import { WorkflowActionResult } from 'src/modules/workflow/workflow-executor/types/workflow-action-result.type'; -import { WorkflowCodeStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type'; import { WorkflowStepExecutorException, WorkflowStepExecutorExceptionCode, } from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception'; +import { WorkflowActionResult } from 'src/modules/workflow/workflow-executor/types/workflow-action-result.type'; +import { WorkflowCodeStepInput } from 'src/modules/workflow/workflow-executor/types/workflow-step-settings.type'; @Injectable() -export class CodeWorkflowAction { +export class CodeWorkflowAction implements WorkflowAction { constructor( private readonly serverlessFunctionService: ServerlessFunctionService, private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, ) {} - async execute({ - step, - payload, - }: { - step: WorkflowCodeStep; - payload?: object; - }): Promise { + async execute( + workflowStepInput: WorkflowCodeStepInput, + ): Promise { const { workspaceId } = this.scopedWorkspaceContextFactory.create(); if (!workspaceId) { @@ -34,9 +32,9 @@ export class CodeWorkflowAction { const result = await this.serverlessFunctionService.executeOneServerlessFunction( - step.settings.serverlessFunctionId, + workflowStepInput.serverlessFunctionId, workspaceId, - payload || {}, + {}, // TODO: input will be dynamically calculated from function input ); if (result.error) { diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts index aa14605f74a0..545e88a47076 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts @@ -32,17 +32,21 @@ export enum WorkflowRunStatus { FAILED = 'FAILED', } -export type WorkflowRunOutput = { - steps: { - id: string; - name: string; - type: string; +type StepRunOutput = { + id: string; + name: string; + type: string; + outputs: { attemptCount: number; result: object | undefined; error: string | undefined; }[]; }; +export type WorkflowRunOutput = { + steps: Record; +}; + @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.workflowRun, namePlural: 'workflowRuns', diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-executor.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-executor.exception.ts index cdba717b8f67..aee8cc34dc45 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-executor.exception.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-executor.exception.ts @@ -8,4 +8,5 @@ export class WorkflowExecutorException extends CustomException { export enum WorkflowExecutorExceptionCode { WORKFLOW_FAILED = 'WORKFLOW_FAILED', + VARIABLE_EVALUATION_FAILED = 'VARIABLE_EVALUATION_FAILED', } diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/interfaces/workflow-action.interface.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/interfaces/workflow-action.interface.ts index 47b481949a76..499baaed8486 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/interfaces/workflow-action.interface.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/interfaces/workflow-action.interface.ts @@ -1,12 +1,5 @@ import { WorkflowActionResult } from 'src/modules/workflow/workflow-executor/types/workflow-action-result.type'; -import { WorkflowStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type'; export interface WorkflowAction { - execute({ - step, - payload, - }: { - step: WorkflowStep; - payload?: object; - }): Promise; + execute(workflowStepInput: unknown): Promise; } diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-step-settings.type.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-step-settings.type.ts index bb8f8351faab..1cc9b20c9c7c 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-step-settings.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-step-settings.type.ts @@ -9,12 +9,21 @@ type BaseWorkflowStepSettings = { }; }; -export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & { +export type WorkflowCodeStepInput = { serverlessFunctionId: string; }; -export type WorkflowSendEmailStepSettings = BaseWorkflowStepSettings & { +export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & { + input: WorkflowCodeStepInput; +}; + +export type WorkflowSendEmailStepInput = { connectedAccountId: string; + email: string; subject?: string; body?: string; }; + +export type WorkflowSendEmailStepSettings = BaseWorkflowStepSettings & { + input: WorkflowSendEmailStepInput; +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/utils/__tests__/variable-resolver.util.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/utils/__tests__/variable-resolver.util.spec.ts new file mode 100644 index 000000000000..0e73ec4a2c86 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/utils/__tests__/variable-resolver.util.spec.ts @@ -0,0 +1,81 @@ +import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util'; + +describe('resolveInput', () => { + const context = { + user: { + name: 'John Doe', + age: 30, + }, + settings: { + theme: 'dark', + notifications: true, + }, + }; + + it('should return null for null input', () => { + expect(resolveInput(null, context)).toBeNull(); + }); + + it('should return undefined for undefined input', () => { + expect(resolveInput(undefined, context)).toBeUndefined(); + }); + + it('should resolve a simple string variable', () => { + expect(resolveInput('{{user.name}}', context)).toBe('John Doe'); + }); + + it('should resolve multiple variables in a string', () => { + expect( + resolveInput('Name: {{user.name}}, Age: {{user.age}}', context), + ).toBe('Name: John Doe, Age: 30'); + }); + + it('should handle non-existent variables', () => { + expect(resolveInput('{{user.email}}', context)).toBe(''); + }); + + it('should resolve variables in an array', () => { + const input = ['{{user.name}}', '{{settings.theme}}', 'static']; + const expected = ['John Doe', 'dark', 'static']; + + expect(resolveInput(input, context)).toEqual(expected); + }); + + it('should resolve variables in an object', () => { + const input = { + name: '{{user.name}}', + theme: '{{settings.theme}}', + static: 'value', + }; + const expected = { + name: 'John Doe', + theme: 'dark', + static: 'value', + }; + + expect(resolveInput(input, context)).toEqual(expected); + }); + + it('should handle nested objects and arrays', () => { + const input = { + user: { + displayName: '{{user.name}}', + preferences: ['{{settings.theme}}', '{{settings.notifications}}'], + }, + staticData: [1, 2, 3], + }; + const expected = { + user: { + displayName: 'John Doe', + preferences: ['dark', 'true'], + }, + staticData: [1, 2, 3], + }; + + expect(resolveInput(input, context)).toEqual(expected); + }); + + it('should throw an error for invalid expressions', () => { + expect(() => resolveInput('{{invalidFunction()}}', context)).toThrow(); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/utils/variable-resolver.util.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/utils/variable-resolver.util.ts new file mode 100644 index 000000000000..c4fc012d453e --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/utils/variable-resolver.util.ts @@ -0,0 +1,98 @@ +import { isNil, isString } from '@nestjs/common/utils/shared.utils'; + +import Handlebars from 'handlebars'; + +import { + WorkflowExecutorException, + WorkflowExecutorExceptionCode, +} from 'src/modules/workflow/workflow-executor/exceptions/workflow-executor.exception'; + +const VARIABLE_PATTERN = RegExp('\\{\\{(.*?)\\}\\}', 'g'); + +export const resolveInput = ( + unresolvedInput: unknown, + context: Record, +): unknown => { + if (isNil(unresolvedInput)) { + return unresolvedInput; + } + + if (isString(unresolvedInput)) { + return resolveString(unresolvedInput, context); + } + + if (Array.isArray(unresolvedInput)) { + return resolveArray(unresolvedInput, context); + } + + if (typeof unresolvedInput === 'object' && unresolvedInput !== null) { + return resolveObject(unresolvedInput, context); + } + + return unresolvedInput; +}; + +const resolveArray = ( + input: unknown[], + context: Record, +): unknown[] => { + const resolvedArray = input; + + for (let i = 0; i < input.length; ++i) { + resolvedArray[i] = resolveInput(input[i], context); + } + + return resolvedArray; +}; + +const resolveObject = ( + input: object, + context: Record, +): object => { + const resolvedObject = input; + + const entries = Object.entries(resolvedObject); + + for (const [key, value] of entries) { + resolvedObject[key] = resolveInput(value, context); + } + + return resolvedObject; +}; + +const resolveString = ( + input: string, + context: Record, +): string => { + const matchedTokens = input.match(VARIABLE_PATTERN); + + if (!matchedTokens || matchedTokens.length === 0) { + return input; + } + + if (matchedTokens.length === 1 && matchedTokens[0] === input) { + return evalFromContext(input, context); + } + + return input.replace(VARIABLE_PATTERN, (matchedToken, _) => { + const processedToken = evalFromContext(matchedToken, context); + + return processedToken; + }); +}; + +const evalFromContext = ( + input: string, + context: Record, +): string => { + try { + const inferredInput = Handlebars.compile(input)(context); + + return inferredInput ?? ''; + } catch (exception) { + throw new WorkflowExecutorException( + `Failed to evaluate variable ${input}: ${exception}`, + WorkflowExecutorExceptionCode.VARIABLE_EVALUATION_FAILED, + ); + } +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.module.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.module.ts index 24ae66fd7f11..4cfc2d9888d7 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.module.ts @@ -1,13 +1,13 @@ import { Module } from '@nestjs/common'; -import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; -import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service'; -import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory'; -import { CodeWorkflowAction } from 'src/modules/serverless/workflow-actions/code.workflow-action'; -import { SendEmailWorkflowAction } from 'src/modules/mail-sender/workflow-actions/send-email.workflow-action'; import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; +import { SendEmailWorkflowAction } from 'src/modules/mail-sender/workflow-actions/send-email.workflow-action'; import { MessagingGmailDriverModule } from 'src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module'; +import { CodeWorkflowAction } from 'src/modules/serverless/workflow-actions/code.workflow-action'; +import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; +import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory'; +import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service'; @Module({ imports: [ diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts index c50684f876c6..5290c5d4d049 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts @@ -6,6 +6,7 @@ import { } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory'; import { WorkflowStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type'; +import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util'; const MAX_RETRIES_ON_FAILURE = 3; @@ -21,14 +22,14 @@ export class WorkflowExecutorWorkspaceService { async execute({ currentStepIndex, steps, - payload, + context, output, attemptCount = 1, }: { currentStepIndex: number; steps: WorkflowStep[]; output: WorkflowExecutorOutput; - payload?: object; + context: Record; attemptCount?: number; }): Promise { if (currentStepIndex >= steps.length) { @@ -39,59 +40,55 @@ export class WorkflowExecutorWorkspaceService { const workflowAction = this.workflowActionFactory.get(step.type); - const result = await workflowAction.execute({ - step, - payload, - }); + const actionPayload = resolveInput(step.settings.input, context); - const baseStepOutput = { + const result = await workflowAction.execute(actionPayload); + + const stepOutput = output.steps[step.id]; + + const error = + result.error?.errorMessage ?? + (result.result ? undefined : 'Execution result error, no data or error'); + + const updatedStepOutput = { id: step.id, name: step.name, type: step.type, - attemptCount, + outputs: [ + ...(stepOutput?.outputs ?? []), + { + attemptCount, + result: result.result, + error, + }, + ], }; const updatedOutput = { ...output, - steps: [ + steps: { ...output.steps, - { - ...baseStepOutput, - result: result.result, - error: result.error?.errorMessage, - }, - ], + [step.id]: updatedStepOutput, + }, }; if (result.result) { return await this.execute({ currentStepIndex: currentStepIndex + 1, steps, - payload: result.result, + context: { + ...context, + [step.id]: result.result, + }, output: updatedOutput, }); } - if (!result.error) { - return { - ...output, - steps: [ - ...output.steps, - { - ...baseStepOutput, - result: undefined, - error: 'Execution result error, no data or error', - }, - ], - status: WorkflowRunStatus.FAILED, - }; - } - if (step.settings.errorHandlingOptions.continueOnFailure.value) { return await this.execute({ currentStepIndex: currentStepIndex + 1, steps, - payload, + context, output: updatedOutput, }); } @@ -103,7 +100,7 @@ export class WorkflowExecutorWorkspaceService { return await this.execute({ currentStepIndex, steps, - payload, + context, output: updatedOutput, attemptCount: attemptCount + 1, }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts index 5a79462a355c..644595df6f3b 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts @@ -40,9 +40,11 @@ export class RunWorkflowJob { await this.workflowExecutorWorkspaceService.execute({ currentStepIndex: 0, steps: workflowVersion.steps || [], - payload, + context: { + trigger: payload, + }, output: { - steps: [], + steps: {}, status: WorkflowRunStatus.RUNNING, }, }); diff --git a/packages/twenty-server/src/utils/apply-cors-to-exceptions.ts b/packages/twenty-server/src/utils/apply-cors-to-exceptions.ts index 0dd3a5cc12ef..eba73f8e0571 100644 --- a/packages/twenty-server/src/utils/apply-cors-to-exceptions.ts +++ b/packages/twenty-server/src/utils/apply-cors-to-exceptions.ts @@ -1,7 +1,7 @@ import { - ExceptionFilter, - Catch, ArgumentsHost, + Catch, + ExceptionFilter, HttpException, } from '@nestjs/common'; diff --git a/packages/twenty-server/test/company.integration-spec.ts b/packages/twenty-server/test/company.integration-spec.ts deleted file mode 100644 index bd25d66eb4dc..000000000000 --- a/packages/twenty-server/test/company.integration-spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import request from 'supertest'; - -const graphqlClient = request(`http://localhost:${APP_PORT}`); - -describe('CompanyResolver (integration)', () => { - it('should find many companies', () => { - const queryData = { - query: ` - query Companies { - companies { - edges { - node { - id - name - } - } - } - } - `, - }; - - return graphqlClient - .post('/graphql') - .set('Authorization', `Bearer ${ACCESS_TOKEN}`) - .send(queryData) - .expect(200) - .expect((res) => { - expect(res.body.data).toBeDefined(); - expect(res.body.errors).toBeUndefined(); - }) - .expect((res) => { - const data = res.body.data.companies; - - expect(data).toBeDefined(); - expect(Array.isArray(data.edges)).toBe(true); - - const edges = data.edges; - - if (edges.length > 0) { - const company = edges[0].node; - - expect(company).toBeDefined(); - expect(company).toHaveProperty('id'); - expect(company).toHaveProperty('name'); - } - }); - }); -}); diff --git a/packages/twenty-server/scripts/generate-integration-tests/index.ts b/packages/twenty-server/test/integration/graphql/codegen/index.ts similarity index 98% rename from packages/twenty-server/scripts/generate-integration-tests/index.ts rename to packages/twenty-server/test/integration/graphql/codegen/index.ts index 5b635d3bccd7..9f1937cae2b5 100644 --- a/packages/twenty-server/scripts/generate-integration-tests/index.ts +++ b/packages/twenty-server/test/integration/graphql/codegen/index.ts @@ -13,7 +13,7 @@ import { const GRAPHQL_URL = 'http://localhost:3000/graphql'; const BEARER_TOKEN = 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwiaWF0IjoxNzI2NDkyNTAyLCJleHAiOjEzMjQ1MDE2NTAyfQ.zM6TbfeOqYVH5Sgryc2zf02hd9uqUOSL1-iJlMgwzsI'; -const TEST_OUTPUT_DIR = './test'; +const TEST_OUTPUT_DIR = './test/integration/graphql/suites/object-generated'; const fetchGraphQLSchema = async (): Promise => { const headers = { diff --git a/packages/twenty-server/scripts/generate-integration-tests/introspection-query.ts b/packages/twenty-server/test/integration/graphql/codegen/introspection-query.ts similarity index 100% rename from packages/twenty-server/scripts/generate-integration-tests/introspection-query.ts rename to packages/twenty-server/test/integration/graphql/codegen/introspection-query.ts diff --git a/packages/twenty-server/scripts/generate-integration-tests/introspection.interface.ts b/packages/twenty-server/test/integration/graphql/codegen/introspection.interface.ts similarity index 100% rename from packages/twenty-server/scripts/generate-integration-tests/introspection.interface.ts rename to packages/twenty-server/test/integration/graphql/codegen/introspection.interface.ts diff --git a/packages/twenty-server/test/integration/graphql/suites/all-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-resolvers.integration-spec.ts new file mode 100644 index 000000000000..3b1a46f89a44 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-resolvers.integration-spec.ts @@ -0,0 +1,430 @@ +import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util'; +import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util'; +import { deleteManyOperationFactory } from 'test/integration/graphql/utils/delete-many-operation-factory.util'; +import { deleteOneOperationFactory } from 'test/integration/graphql/utils/delete-one-operation-factory.util'; +import { destroyManyOperationFactory } from 'test/integration/graphql/utils/destroy-many-operation-factory.util'; +import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-one-operation-factory.util'; +import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util'; +import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util'; +import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { updateManyOperationFactory } from 'test/integration/graphql/utils/update-many-operation-factory.util'; +import { updateOneOperationFactory } from 'test/integration/graphql/utils/update-one-operation-factory.util'; +import { generateRecordName } from 'test/integration/utils/generate-record-name'; + +const COMPANY_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const COMPANY_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const COMPANY_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; +const COMPANY_GQL_FIELDS = ` + id + name + employees + idealCustomerProfile + position + createdAt + updatedAt + deletedAt + accountOwnerId + tagline + workPolicy + visaSponsorship +`; + +describe('companies resolvers (integration)', () => { + it('1. should create and return companies', async () => { + const companyName1 = generateRecordName(COMPANY_1_ID); + const companyName2 = generateRecordName(COMPANY_2_ID); + + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + data: [ + { + id: COMPANY_1_ID, + name: companyName1, + }, + { + id: COMPANY_2_ID, + name: companyName2, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createCompanies).toHaveLength(2); + + response.body.data.createCompanies.forEach((company) => { + expect(company).toHaveProperty('name'); + expect([companyName1, companyName2]).toContain(company.name); + + expect(company).toHaveProperty('employees'); + expect(company).toHaveProperty('idealCustomerProfile'); + expect(company).toHaveProperty('position'); + expect(company).toHaveProperty('id'); + expect(company).toHaveProperty('createdAt'); + expect(company).toHaveProperty('updatedAt'); + expect(company).toHaveProperty('deletedAt'); + expect(company).toHaveProperty('accountOwnerId'); + expect(company).toHaveProperty('tagline'); + expect(company).toHaveProperty('workPolicy'); + expect(company).toHaveProperty('visaSponsorship'); + }); + }); + + it('1b. should create and return one company', async () => { + const companyName = generateRecordName(COMPANY_3_ID); + + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + data: { + id: COMPANY_3_ID, + name: companyName, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdCompany = response.body.data.createCompany; + + expect(createdCompany).toHaveProperty('name'); + expect(createdCompany.name).toEqual(companyName); + + expect(createdCompany).toHaveProperty('employees'); + expect(createdCompany).toHaveProperty('idealCustomerProfile'); + expect(createdCompany).toHaveProperty('position'); + expect(createdCompany).toHaveProperty('id'); + expect(createdCompany).toHaveProperty('createdAt'); + expect(createdCompany).toHaveProperty('updatedAt'); + expect(createdCompany).toHaveProperty('deletedAt'); + expect(createdCompany).toHaveProperty('accountOwnerId'); + expect(createdCompany).toHaveProperty('tagline'); + expect(createdCompany).toHaveProperty('workPolicy'); + expect(createdCompany).toHaveProperty('visaSponsorship'); + }); + + it('2. should find many companies', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.companies; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const companies = edges[0].node; + + expect(companies).toHaveProperty('name'); + expect(companies).toHaveProperty('employees'); + expect(companies).toHaveProperty('idealCustomerProfile'); + expect(companies).toHaveProperty('position'); + expect(companies).toHaveProperty('id'); + expect(companies).toHaveProperty('createdAt'); + expect(companies).toHaveProperty('updatedAt'); + expect(companies).toHaveProperty('deletedAt'); + expect(companies).toHaveProperty('accountOwnerId'); + expect(companies).toHaveProperty('tagline'); + expect(companies).toHaveProperty('workPolicy'); + expect(companies).toHaveProperty('visaSponsorship'); + } + }); + + it('2b. should find one company', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + eq: COMPANY_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const company = response.body.data.company; + + expect(company).toHaveProperty('name'); + + expect(company).toHaveProperty('employees'); + expect(company).toHaveProperty('idealCustomerProfile'); + expect(company).toHaveProperty('position'); + expect(company).toHaveProperty('id'); + expect(company).toHaveProperty('createdAt'); + expect(company).toHaveProperty('updatedAt'); + expect(company).toHaveProperty('deletedAt'); + expect(company).toHaveProperty('accountOwnerId'); + expect(company).toHaveProperty('tagline'); + expect(company).toHaveProperty('workPolicy'); + expect(company).toHaveProperty('visaSponsorship'); + }); + + it('3. should update many companies', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + data: { + employees: 123, + }, + filter: { + id: { + in: [COMPANY_1_ID, COMPANY_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedCompanies = response.body.data.updateCompanies; + + expect(updatedCompanies).toHaveLength(2); + + updatedCompanies.forEach((company) => { + expect(company.employees).toEqual(123); + }); + }); + + it('3b. should update one company', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + data: { + employees: 122, + }, + recordId: COMPANY_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedCompany = response.body.data.updateCompany; + + expect(updatedCompany.employees).toEqual(122); + }); + + it('4. should find many companies with updated employees', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + employees: { + eq: 123, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.companies.edges).toHaveLength(2); + }); + + it('4b. should find one company with updated employees', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + employees: { + eq: 122, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.company.employees).toEqual(122); + }); + + it('5. should delete many companies', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + in: [COMPANY_1_ID, COMPANY_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deleteCompanies = response.body.data.deleteCompanies; + + expect(deleteCompanies).toHaveLength(2); + + deleteCompanies.forEach((company) => { + expect(company.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one company', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + recordId: COMPANY_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteCompany.deletedAt).toBeTruthy(); + }); + + it('6. should not find many companies anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + in: [COMPANY_1_ID, COMPANY_2_ID], + }, + }, + }); + + const findCompaniesResponse = await makeGraphqlAPIRequest(graphqlOperation); + + expect(findCompaniesResponse.body.data.companies.edges).toHaveLength(0); + }); + + it('6b. should not find one company anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + eq: COMPANY_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.company).toBeNull(); + }); + + it('7. should find many deleted companies with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + in: [COMPANY_1_ID, COMPANY_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.companies.edges).toHaveLength(2); + }); + + it('7b. should find one deleted company with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + eq: COMPANY_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.company.id).toEqual(COMPANY_3_ID); + }); + + it('8. should destroy many companies', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + in: [COMPANY_1_ID, COMPANY_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyCompanies).toHaveLength(2); + }); + + it('8b. should destroy one company', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + recordId: COMPANY_3_ID, + }); + + const destroyCompanyResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect(destroyCompanyResponse.body.data.destroyCompany).toBeTruthy(); + }); + + it('9. should not find many companies anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + in: [COMPANY_1_ID, COMPANY_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.companies.edges).toHaveLength(0); + }); + + it('9b. should not find one company anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + eq: COMPANY_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.company).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/auth.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/auth.integration-spec.ts similarity index 100% rename from packages/twenty-server/test/auth.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/auth.integration-spec.ts diff --git a/packages/twenty-server/test/activities.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/activities.integration-spec.ts similarity index 97% rename from packages/twenty-server/test/activities.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/activities.integration-spec.ts index 01f262c8f295..0e0134578e44 100644 --- a/packages/twenty-server/test/activities.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/activities.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('activitiesResolver (integration)', () => { +describe('activitiesResolver (e2e)', () => { it('should find many activities', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/activity-targets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/activity-targets.integration-spec.ts similarity index 92% rename from packages/twenty-server/test/activity-targets.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/activity-targets.integration-spec.ts index cbbeb216f0aa..99b4f0b1e10f 100644 --- a/packages/twenty-server/test/activity-targets.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/activity-targets.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('activityTargetsResolver (integration)', () => { +describe('activityTargetsResolver (e2e)', () => { it('should find many activityTargets', () => { const queryData = { query: ` @@ -18,6 +18,7 @@ describe('activityTargetsResolver (integration)', () => { personId companyId opportunityId + rocketId } } } @@ -53,6 +54,7 @@ describe('activityTargetsResolver (integration)', () => { expect(activityTargets).toHaveProperty('personId'); expect(activityTargets).toHaveProperty('companyId'); expect(activityTargets).toHaveProperty('opportunityId'); + expect(activityTargets).toHaveProperty('rocketId'); } }); }); diff --git a/packages/twenty-server/test/api-keys.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/api-keys.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/api-keys.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/api-keys.integration-spec.ts index a196db0861c4..5515abb9a620 100644 --- a/packages/twenty-server/test/api-keys.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/api-keys.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('apiKeysResolver (integration)', () => { +describe('apiKeysResolver (e2e)', () => { it('should find many apiKeys', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/attachments.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/attachments.integration-spec.ts similarity index 94% rename from packages/twenty-server/test/attachments.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/attachments.integration-spec.ts index 440a6484e6b1..fc9637963342 100644 --- a/packages/twenty-server/test/attachments.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/attachments.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('attachmentsResolver (integration)', () => { +describe('attachmentsResolver (e2e)', () => { it('should find many attachments', () => { const queryData = { query: ` @@ -24,6 +24,7 @@ describe('attachmentsResolver (integration)', () => { personId companyId opportunityId + rocketId } } } @@ -65,6 +66,7 @@ describe('attachmentsResolver (integration)', () => { expect(attachments).toHaveProperty('personId'); expect(attachments).toHaveProperty('companyId'); expect(attachments).toHaveProperty('opportunityId'); + expect(attachments).toHaveProperty('rocketId'); } }); }); diff --git a/packages/twenty-server/test/audit-logs.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/audit-logs.integration-spec.ts similarity index 97% rename from packages/twenty-server/test/audit-logs.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/audit-logs.integration-spec.ts index 99a573235cda..77a4507188f8 100644 --- a/packages/twenty-server/test/audit-logs.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/audit-logs.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('auditLogsResolver (integration)', () => { +describe('auditLogsResolver (e2e)', () => { it('should find many auditLogs', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/blocklists.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/blocklists.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/blocklists.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/blocklists.integration-spec.ts index d8080b3cd570..60da5e4673fd 100644 --- a/packages/twenty-server/test/blocklists.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/blocklists.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('blocklistsResolver (integration)', () => { +describe('blocklistsResolver (e2e)', () => { it('should find many blocklists', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/calendar-channel-event-associations.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-channel-event-associations.integration-spec.ts similarity index 89% rename from packages/twenty-server/test/calendar-channel-event-associations.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-channel-event-associations.integration-spec.ts index 5d03268b840d..023b0876915d 100644 --- a/packages/twenty-server/test/calendar-channel-event-associations.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-channel-event-associations.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('calendarChannelEventAssociationsResolver (integration)', () => { +describe('calendarChannelEventAssociationsResolver (e2e)', () => { it('should find many calendarChannelEventAssociations', () => { const queryData = { query: ` @@ -11,6 +11,7 @@ describe('calendarChannelEventAssociationsResolver (integration)', () => { edges { node { eventExternalId + recurringEventExternalId id createdAt updatedAt @@ -47,6 +48,9 @@ describe('calendarChannelEventAssociationsResolver (integration)', () => { expect(calendarChannelEventAssociations).toHaveProperty( 'eventExternalId', ); + expect(calendarChannelEventAssociations).toHaveProperty( + 'recurringEventExternalId', + ); expect(calendarChannelEventAssociations).toHaveProperty('id'); expect(calendarChannelEventAssociations).toHaveProperty('createdAt'); expect(calendarChannelEventAssociations).toHaveProperty('updatedAt'); diff --git a/packages/twenty-server/test/calendar-channels.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-channels.integration-spec.ts similarity index 94% rename from packages/twenty-server/test/calendar-channels.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-channels.integration-spec.ts index 6056af6ac616..baab9d5003d3 100644 --- a/packages/twenty-server/test/calendar-channels.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-channels.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('calendarChannelsResolver (integration)', () => { +describe('calendarChannelsResolver (e2e)', () => { it('should find many calendarChannels', () => { const queryData = { query: ` @@ -18,6 +18,7 @@ describe('calendarChannelsResolver (integration)', () => { contactAutoCreationPolicy isSyncEnabled syncCursor + syncedAt syncStageStartedAt throttleFailureCount id @@ -62,6 +63,7 @@ describe('calendarChannelsResolver (integration)', () => { expect(calendarChannels).toHaveProperty('contactAutoCreationPolicy'); expect(calendarChannels).toHaveProperty('isSyncEnabled'); expect(calendarChannels).toHaveProperty('syncCursor'); + expect(calendarChannels).toHaveProperty('syncedAt'); expect(calendarChannels).toHaveProperty('syncStageStartedAt'); expect(calendarChannels).toHaveProperty('throttleFailureCount'); expect(calendarChannels).toHaveProperty('id'); diff --git a/packages/twenty-server/test/calendar-event-participants.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-event-participants.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/calendar-event-participants.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-event-participants.integration-spec.ts index 50e65547e406..45a8c87a8471 100644 --- a/packages/twenty-server/test/calendar-event-participants.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-event-participants.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('calendarEventParticipantsResolver (integration)', () => { +describe('calendarEventParticipantsResolver (e2e)', () => { it('should find many calendarEventParticipants', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/comments.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/comments.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/comments.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/comments.integration-spec.ts index 0f89ba5491e6..2508ff628acd 100644 --- a/packages/twenty-server/test/comments.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/comments.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('commentsResolver (integration)', () => { +describe('commentsResolver (e2e)', () => { it('should find many comments', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/companies.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/companies.integration-spec.ts similarity index 93% rename from packages/twenty-server/test/companies.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/companies.integration-spec.ts index 63c7f9eec481..1273e624b0ac 100644 --- a/packages/twenty-server/test/companies.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/companies.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('companiesResolver (integration)', () => { +describe('companiesResolver (e2e)', () => { it('should find many companies', () => { const queryData = { query: ` @@ -14,6 +14,7 @@ describe('companiesResolver (integration)', () => { employees idealCustomerProfile position + searchVector id createdAt updatedAt @@ -53,6 +54,7 @@ describe('companiesResolver (integration)', () => { expect(companies).toHaveProperty('employees'); expect(companies).toHaveProperty('idealCustomerProfile'); expect(companies).toHaveProperty('position'); + expect(companies).toHaveProperty('searchVector'); expect(companies).toHaveProperty('id'); expect(companies).toHaveProperty('createdAt'); expect(companies).toHaveProperty('updatedAt'); diff --git a/packages/twenty-server/test/connected-accounts.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/connected-accounts.integration-spec.ts similarity index 93% rename from packages/twenty-server/test/connected-accounts.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/connected-accounts.integration-spec.ts index e17fd5b28504..0a6858940c6a 100644 --- a/packages/twenty-server/test/connected-accounts.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/connected-accounts.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('connectedAccountsResolver (integration)', () => { +describe('connectedAccountsResolver (e2e)', () => { it('should find many connectedAccounts', () => { const queryData = { query: ` @@ -17,6 +17,7 @@ describe('connectedAccountsResolver (integration)', () => { lastSyncHistoryId authFailedAt handleAliases + scopes id createdAt updatedAt @@ -56,6 +57,7 @@ describe('connectedAccountsResolver (integration)', () => { expect(connectedAccounts).toHaveProperty('lastSyncHistoryId'); expect(connectedAccounts).toHaveProperty('authFailedAt'); expect(connectedAccounts).toHaveProperty('handleAliases'); + expect(connectedAccounts).toHaveProperty('scopes'); expect(connectedAccounts).toHaveProperty('id'); expect(connectedAccounts).toHaveProperty('createdAt'); expect(connectedAccounts).toHaveProperty('updatedAt'); diff --git a/packages/twenty-server/test/favorites.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/favorites.integration-spec.ts similarity index 82% rename from packages/twenty-server/test/favorites.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/favorites.integration-spec.ts index f58e702e5517..ea410fabc912 100644 --- a/packages/twenty-server/test/favorites.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/favorites.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('favoritesResolver (integration)', () => { +describe('favoritesResolver (e2e)', () => { it('should find many favorites', () => { const queryData = { query: ` @@ -19,9 +19,13 @@ describe('favoritesResolver (integration)', () => { personId companyId opportunityId + workflowId + workflowVersionId + workflowRunId taskId noteId viewId + rocketId } } } @@ -58,9 +62,13 @@ describe('favoritesResolver (integration)', () => { expect(favorites).toHaveProperty('personId'); expect(favorites).toHaveProperty('companyId'); expect(favorites).toHaveProperty('opportunityId'); + expect(favorites).toHaveProperty('workflowId'); + expect(favorites).toHaveProperty('workflowVersionId'); + expect(favorites).toHaveProperty('workflowRunId'); expect(favorites).toHaveProperty('taskId'); expect(favorites).toHaveProperty('noteId'); expect(favorites).toHaveProperty('viewId'); + expect(favorites).toHaveProperty('rocketId'); } }); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/index-metadatas.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/index-metadatas.integration-spec.ts new file mode 100644 index 000000000000..02d3d54b0562 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/index-metadatas.integration-spec.ts @@ -0,0 +1,59 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('indexMetadatasResolver (e2e)', () => { + it('should find many indexMetadatas', () => { + const queryData = { + query: ` + query indexMetadatas { + indexMetadatas { + edges { + node { + id + name + isCustom + isUnique + indexWhereClause + indexType + createdAt + updatedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.indexMetadatas; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const indexMetadatas = edges[0].node; + + expect(indexMetadatas).toHaveProperty('id'); + expect(indexMetadatas).toHaveProperty('name'); + expect(indexMetadatas).toHaveProperty('isCustom'); + expect(indexMetadatas).toHaveProperty('isUnique'); + expect(indexMetadatas).toHaveProperty('indexWhereClause'); + expect(indexMetadatas).toHaveProperty('indexType'); + expect(indexMetadatas).toHaveProperty('createdAt'); + expect(indexMetadatas).toHaveProperty('updatedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/message-channel-message-associations.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/message-channel-message-associations.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/message-channel-message-associations.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/message-channel-message-associations.integration-spec.ts index a250550f4b88..db17b067bb18 100644 --- a/packages/twenty-server/test/message-channel-message-associations.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/message-channel-message-associations.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('messageChannelMessageAssociationsResolver (integration)', () => { +describe('messageChannelMessageAssociationsResolver (e2e)', () => { it('should find many messageChannelMessageAssociations', () => { const queryData = { query: ` @@ -10,11 +10,11 @@ describe('messageChannelMessageAssociationsResolver (integration)', () => { messageChannelMessageAssociations { edges { node { - createdAt messageExternalId messageThreadExternalId direction id + createdAt updatedAt deletedAt messageChannelId @@ -46,7 +46,6 @@ describe('messageChannelMessageAssociationsResolver (integration)', () => { if (edges.length > 0) { const messageChannelMessageAssociations = edges[0].node; - expect(messageChannelMessageAssociations).toHaveProperty('createdAt'); expect(messageChannelMessageAssociations).toHaveProperty( 'messageExternalId', ); @@ -55,6 +54,7 @@ describe('messageChannelMessageAssociationsResolver (integration)', () => { ); expect(messageChannelMessageAssociations).toHaveProperty('direction'); expect(messageChannelMessageAssociations).toHaveProperty('id'); + expect(messageChannelMessageAssociations).toHaveProperty('createdAt'); expect(messageChannelMessageAssociations).toHaveProperty('updatedAt'); expect(messageChannelMessageAssociations).toHaveProperty('deletedAt'); expect(messageChannelMessageAssociations).toHaveProperty( diff --git a/packages/twenty-server/test/message-channels.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/message-channels.integration-spec.ts similarity index 97% rename from packages/twenty-server/test/message-channels.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/message-channels.integration-spec.ts index 8100a885d89c..58f9b3ea8016 100644 --- a/packages/twenty-server/test/message-channels.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/message-channels.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('messageChannelsResolver (integration)', () => { +describe('messageChannelsResolver (e2e)', () => { it('should find many messageChannels', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/message-participants.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/message-participants.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/message-participants.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/message-participants.integration-spec.ts index 45c190c536ec..1271455c0c42 100644 --- a/packages/twenty-server/test/message-participants.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/message-participants.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('messageParticipantsResolver (integration)', () => { +describe('messageParticipantsResolver (e2e)', () => { it('should find many messageParticipants', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/message-threads.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/message-threads.integration-spec.ts similarity index 95% rename from packages/twenty-server/test/message-threads.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/message-threads.integration-spec.ts index 714bf06bb65a..85ec6e2a5046 100644 --- a/packages/twenty-server/test/message-threads.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/message-threads.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('messageThreadsResolver (integration)', () => { +describe('messageThreadsResolver (e2e)', () => { it('should find many messageThreads', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/note-targets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/note-targets.integration-spec.ts similarity index 92% rename from packages/twenty-server/test/note-targets.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/note-targets.integration-spec.ts index 30d309dc32f5..8cd6b76a34d2 100644 --- a/packages/twenty-server/test/note-targets.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/note-targets.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('noteTargetsResolver (integration)', () => { +describe('noteTargetsResolver (e2e)', () => { it('should find many noteTargets', () => { const queryData = { query: ` @@ -18,6 +18,7 @@ describe('noteTargetsResolver (integration)', () => { personId companyId opportunityId + rocketId } } } @@ -53,6 +54,7 @@ describe('noteTargetsResolver (integration)', () => { expect(noteTargets).toHaveProperty('personId'); expect(noteTargets).toHaveProperty('companyId'); expect(noteTargets).toHaveProperty('opportunityId'); + expect(noteTargets).toHaveProperty('rocketId'); } }); }); diff --git a/packages/twenty-server/test/notes.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/notes.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/notes.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/notes.integration-spec.ts index eb13fedbac42..5f25d3ffa1d1 100644 --- a/packages/twenty-server/test/notes.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/notes.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('notesResolver (integration)', () => { +describe('notesResolver (e2e)', () => { it('should find many notes', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/objects.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/objects.integration-spec.ts similarity index 97% rename from packages/twenty-server/test/objects.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/objects.integration-spec.ts index 80d1458ab668..afeb568c4c5b 100644 --- a/packages/twenty-server/test/objects.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/objects.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('objectsResolver (integration)', () => { +describe('objectsResolver (e2e)', () => { it('should find many objects', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/opportunities.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/opportunities.integration-spec.ts similarity index 92% rename from packages/twenty-server/test/opportunities.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/opportunities.integration-spec.ts index 9eebe96a0da7..82099405c9c7 100644 --- a/packages/twenty-server/test/opportunities.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/opportunities.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('opportunitiesResolver (integration)', () => { +describe('opportunitiesResolver (e2e)', () => { it('should find many opportunities', () => { const queryData = { query: ` @@ -14,6 +14,7 @@ describe('opportunitiesResolver (integration)', () => { closeDate stage position + searchVector id createdAt updatedAt @@ -51,6 +52,7 @@ describe('opportunitiesResolver (integration)', () => { expect(opportunities).toHaveProperty('closeDate'); expect(opportunities).toHaveProperty('stage'); expect(opportunities).toHaveProperty('position'); + expect(opportunities).toHaveProperty('searchVector'); expect(opportunities).toHaveProperty('id'); expect(opportunities).toHaveProperty('createdAt'); expect(opportunities).toHaveProperty('updatedAt'); diff --git a/packages/twenty-server/test/people.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/people.integration-spec.ts similarity index 82% rename from packages/twenty-server/test/people.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/people.integration-spec.ts index 28b981e22dbe..7a8454a12f8a 100644 --- a/packages/twenty-server/test/people.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/people.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('peopleResolver (integration)', () => { +describe('peopleResolver (e2e)', () => { it('should find many people', () => { const queryData = { query: ` @@ -11,22 +11,16 @@ describe('peopleResolver (integration)', () => { edges { node { jobTitle - phones { - primaryPhoneNumber - primaryPhoneCountryCode - } city avatarUrl position + searchVector id createdAt updatedAt deletedAt companyId intro - whatsapp { - primaryPhoneNumber - } workPreference performanceRating } @@ -42,7 +36,6 @@ describe('peopleResolver (integration)', () => { .send(queryData) .expect(200) .expect((res) => { - console.log(res.body); expect(res.body.data).toBeDefined(); expect(res.body.errors).toBeUndefined(); }) @@ -58,17 +51,16 @@ describe('peopleResolver (integration)', () => { const people = edges[0].node; expect(people).toHaveProperty('jobTitle'); - expect(people).toHaveProperty('phones'); expect(people).toHaveProperty('city'); expect(people).toHaveProperty('avatarUrl'); expect(people).toHaveProperty('position'); + expect(people).toHaveProperty('searchVector'); expect(people).toHaveProperty('id'); expect(people).toHaveProperty('createdAt'); expect(people).toHaveProperty('updatedAt'); expect(people).toHaveProperty('deletedAt'); expect(people).toHaveProperty('companyId'); expect(people).toHaveProperty('intro'); - expect(people).toHaveProperty('whatsapp'); expect(people).toHaveProperty('workPreference'); expect(people).toHaveProperty('performanceRating'); } diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/rockets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/rockets.integration-spec.ts new file mode 100644 index 000000000000..a9fa0f88f432 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/rockets.integration-spec.ts @@ -0,0 +1,57 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('rocketsResolver (e2e)', () => { + it('should find many rockets', () => { + const queryData = { + query: ` + query rockets { + rockets { + edges { + node { + id + name + createdAt + updatedAt + deletedAt + position + searchVector + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.rockets; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const rockets = edges[0].node; + + expect(rockets).toHaveProperty('id'); + expect(rockets).toHaveProperty('name'); + expect(rockets).toHaveProperty('createdAt'); + expect(rockets).toHaveProperty('updatedAt'); + expect(rockets).toHaveProperty('deletedAt'); + expect(rockets).toHaveProperty('position'); + expect(rockets).toHaveProperty('searchVector'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-activities.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-activities.integration-spec.ts new file mode 100644 index 000000000000..7d9be3362461 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-activities.integration-spec.ts @@ -0,0 +1,67 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchActivitiesResolver (e2e)', () => { + it('should find many searchActivities', () => { + const queryData = { + query: ` + query searchActivities { + searchActivities { + edges { + node { + title + body + type + reminderAt + dueAt + completedAt + id + createdAt + updatedAt + deletedAt + authorId + assigneeId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchActivities; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchActivities = edges[0].node; + + expect(searchActivities).toHaveProperty('title'); + expect(searchActivities).toHaveProperty('body'); + expect(searchActivities).toHaveProperty('type'); + expect(searchActivities).toHaveProperty('reminderAt'); + expect(searchActivities).toHaveProperty('dueAt'); + expect(searchActivities).toHaveProperty('completedAt'); + expect(searchActivities).toHaveProperty('id'); + expect(searchActivities).toHaveProperty('createdAt'); + expect(searchActivities).toHaveProperty('updatedAt'); + expect(searchActivities).toHaveProperty('deletedAt'); + expect(searchActivities).toHaveProperty('authorId'); + expect(searchActivities).toHaveProperty('assigneeId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-activity-targets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-activity-targets.integration-spec.ts new file mode 100644 index 000000000000..64b5fa8c2f1a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-activity-targets.integration-spec.ts @@ -0,0 +1,61 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchActivityTargetsResolver (e2e)', () => { + it('should find many searchActivityTargets', () => { + const queryData = { + query: ` + query searchActivityTargets { + searchActivityTargets { + edges { + node { + id + createdAt + updatedAt + deletedAt + activityId + personId + companyId + opportunityId + rocketId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchActivityTargets; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchActivityTargets = edges[0].node; + + expect(searchActivityTargets).toHaveProperty('id'); + expect(searchActivityTargets).toHaveProperty('createdAt'); + expect(searchActivityTargets).toHaveProperty('updatedAt'); + expect(searchActivityTargets).toHaveProperty('deletedAt'); + expect(searchActivityTargets).toHaveProperty('activityId'); + expect(searchActivityTargets).toHaveProperty('personId'); + expect(searchActivityTargets).toHaveProperty('companyId'); + expect(searchActivityTargets).toHaveProperty('opportunityId'); + expect(searchActivityTargets).toHaveProperty('rocketId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-api-keys.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-api-keys.integration-spec.ts new file mode 100644 index 000000000000..6d403e20d238 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-api-keys.integration-spec.ts @@ -0,0 +1,57 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchApiKeysResolver (e2e)', () => { + it('should find many searchApiKeys', () => { + const queryData = { + query: ` + query searchApiKeys { + searchApiKeys { + edges { + node { + name + expiresAt + revokedAt + id + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchApiKeys; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchApiKeys = edges[0].node; + + expect(searchApiKeys).toHaveProperty('name'); + expect(searchApiKeys).toHaveProperty('expiresAt'); + expect(searchApiKeys).toHaveProperty('revokedAt'); + expect(searchApiKeys).toHaveProperty('id'); + expect(searchApiKeys).toHaveProperty('createdAt'); + expect(searchApiKeys).toHaveProperty('updatedAt'); + expect(searchApiKeys).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-attachments.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-attachments.integration-spec.ts new file mode 100644 index 000000000000..1debda9ff470 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-attachments.integration-spec.ts @@ -0,0 +1,73 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchAttachmentsResolver (e2e)', () => { + it('should find many searchAttachments', () => { + const queryData = { + query: ` + query searchAttachments { + searchAttachments { + edges { + node { + name + fullPath + type + id + createdAt + updatedAt + deletedAt + authorId + activityId + taskId + noteId + personId + companyId + opportunityId + rocketId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchAttachments; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchAttachments = edges[0].node; + + expect(searchAttachments).toHaveProperty('name'); + expect(searchAttachments).toHaveProperty('fullPath'); + expect(searchAttachments).toHaveProperty('type'); + expect(searchAttachments).toHaveProperty('id'); + expect(searchAttachments).toHaveProperty('createdAt'); + expect(searchAttachments).toHaveProperty('updatedAt'); + expect(searchAttachments).toHaveProperty('deletedAt'); + expect(searchAttachments).toHaveProperty('authorId'); + expect(searchAttachments).toHaveProperty('activityId'); + expect(searchAttachments).toHaveProperty('taskId'); + expect(searchAttachments).toHaveProperty('noteId'); + expect(searchAttachments).toHaveProperty('personId'); + expect(searchAttachments).toHaveProperty('companyId'); + expect(searchAttachments).toHaveProperty('opportunityId'); + expect(searchAttachments).toHaveProperty('rocketId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-audit-logs.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-audit-logs.integration-spec.ts new file mode 100644 index 000000000000..0a7ecd6f8f06 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-audit-logs.integration-spec.ts @@ -0,0 +1,65 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchAuditLogsResolver (e2e)', () => { + it('should find many searchAuditLogs', () => { + const queryData = { + query: ` + query searchAuditLogs { + searchAuditLogs { + edges { + node { + name + properties + context + objectName + objectMetadataId + recordId + id + createdAt + updatedAt + deletedAt + workspaceMemberId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchAuditLogs; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchAuditLogs = edges[0].node; + + expect(searchAuditLogs).toHaveProperty('name'); + expect(searchAuditLogs).toHaveProperty('properties'); + expect(searchAuditLogs).toHaveProperty('context'); + expect(searchAuditLogs).toHaveProperty('objectName'); + expect(searchAuditLogs).toHaveProperty('objectMetadataId'); + expect(searchAuditLogs).toHaveProperty('recordId'); + expect(searchAuditLogs).toHaveProperty('id'); + expect(searchAuditLogs).toHaveProperty('createdAt'); + expect(searchAuditLogs).toHaveProperty('updatedAt'); + expect(searchAuditLogs).toHaveProperty('deletedAt'); + expect(searchAuditLogs).toHaveProperty('workspaceMemberId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-blocklists.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-blocklists.integration-spec.ts new file mode 100644 index 000000000000..f864a62857f6 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-blocklists.integration-spec.ts @@ -0,0 +1,55 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchBlocklistsResolver (e2e)', () => { + it('should find many searchBlocklists', () => { + const queryData = { + query: ` + query searchBlocklists { + searchBlocklists { + edges { + node { + handle + id + createdAt + updatedAt + deletedAt + workspaceMemberId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchBlocklists; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchBlocklists = edges[0].node; + + expect(searchBlocklists).toHaveProperty('handle'); + expect(searchBlocklists).toHaveProperty('id'); + expect(searchBlocklists).toHaveProperty('createdAt'); + expect(searchBlocklists).toHaveProperty('updatedAt'); + expect(searchBlocklists).toHaveProperty('deletedAt'); + expect(searchBlocklists).toHaveProperty('workspaceMemberId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-channel-event-associations.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-channel-event-associations.integration-spec.ts new file mode 100644 index 000000000000..749ed8c12df1 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-channel-event-associations.integration-spec.ts @@ -0,0 +1,73 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchCalendarChannelEventAssociationsResolver (e2e)', () => { + it('should find many searchCalendarChannelEventAssociations', () => { + const queryData = { + query: ` + query searchCalendarChannelEventAssociations { + searchCalendarChannelEventAssociations { + edges { + node { + eventExternalId + recurringEventExternalId + id + createdAt + updatedAt + deletedAt + calendarChannelId + calendarEventId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchCalendarChannelEventAssociations; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchCalendarChannelEventAssociations = edges[0].node; + + expect(searchCalendarChannelEventAssociations).toHaveProperty( + 'eventExternalId', + ); + expect(searchCalendarChannelEventAssociations).toHaveProperty( + 'recurringEventExternalId', + ); + expect(searchCalendarChannelEventAssociations).toHaveProperty('id'); + expect(searchCalendarChannelEventAssociations).toHaveProperty( + 'createdAt', + ); + expect(searchCalendarChannelEventAssociations).toHaveProperty( + 'updatedAt', + ); + expect(searchCalendarChannelEventAssociations).toHaveProperty( + 'deletedAt', + ); + expect(searchCalendarChannelEventAssociations).toHaveProperty( + 'calendarChannelId', + ); + expect(searchCalendarChannelEventAssociations).toHaveProperty( + 'calendarEventId', + ); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-channels.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-channels.integration-spec.ts new file mode 100644 index 000000000000..28196dce8066 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-channels.integration-spec.ts @@ -0,0 +1,79 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchCalendarChannelsResolver (e2e)', () => { + it('should find many searchCalendarChannels', () => { + const queryData = { + query: ` + query searchCalendarChannels { + searchCalendarChannels { + edges { + node { + handle + syncStatus + syncStage + visibility + isContactAutoCreationEnabled + contactAutoCreationPolicy + isSyncEnabled + syncCursor + syncedAt + syncStageStartedAt + throttleFailureCount + id + createdAt + updatedAt + deletedAt + connectedAccountId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchCalendarChannels; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchCalendarChannels = edges[0].node; + + expect(searchCalendarChannels).toHaveProperty('handle'); + expect(searchCalendarChannels).toHaveProperty('syncStatus'); + expect(searchCalendarChannels).toHaveProperty('syncStage'); + expect(searchCalendarChannels).toHaveProperty('visibility'); + expect(searchCalendarChannels).toHaveProperty( + 'isContactAutoCreationEnabled', + ); + expect(searchCalendarChannels).toHaveProperty( + 'contactAutoCreationPolicy', + ); + expect(searchCalendarChannels).toHaveProperty('isSyncEnabled'); + expect(searchCalendarChannels).toHaveProperty('syncCursor'); + expect(searchCalendarChannels).toHaveProperty('syncedAt'); + expect(searchCalendarChannels).toHaveProperty('syncStageStartedAt'); + expect(searchCalendarChannels).toHaveProperty('throttleFailureCount'); + expect(searchCalendarChannels).toHaveProperty('id'); + expect(searchCalendarChannels).toHaveProperty('createdAt'); + expect(searchCalendarChannels).toHaveProperty('updatedAt'); + expect(searchCalendarChannels).toHaveProperty('deletedAt'); + expect(searchCalendarChannels).toHaveProperty('connectedAccountId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-event-participants.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-event-participants.integration-spec.ts new file mode 100644 index 000000000000..b72c8aeaa4b5 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-event-participants.integration-spec.ts @@ -0,0 +1,71 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchCalendarEventParticipantsResolver (e2e)', () => { + it('should find many searchCalendarEventParticipants', () => { + const queryData = { + query: ` + query searchCalendarEventParticipants { + searchCalendarEventParticipants { + edges { + node { + handle + displayName + isOrganizer + responseStatus + id + createdAt + updatedAt + deletedAt + calendarEventId + personId + workspaceMemberId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchCalendarEventParticipants; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchCalendarEventParticipants = edges[0].node; + + expect(searchCalendarEventParticipants).toHaveProperty('handle'); + expect(searchCalendarEventParticipants).toHaveProperty('displayName'); + expect(searchCalendarEventParticipants).toHaveProperty('isOrganizer'); + expect(searchCalendarEventParticipants).toHaveProperty( + 'responseStatus', + ); + expect(searchCalendarEventParticipants).toHaveProperty('id'); + expect(searchCalendarEventParticipants).toHaveProperty('createdAt'); + expect(searchCalendarEventParticipants).toHaveProperty('updatedAt'); + expect(searchCalendarEventParticipants).toHaveProperty('deletedAt'); + expect(searchCalendarEventParticipants).toHaveProperty( + 'calendarEventId', + ); + expect(searchCalendarEventParticipants).toHaveProperty('personId'); + expect(searchCalendarEventParticipants).toHaveProperty( + 'workspaceMemberId', + ); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-events.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-events.integration-spec.ts new file mode 100644 index 000000000000..76ef636a0d4a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-events.integration-spec.ts @@ -0,0 +1,73 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchCalendarEventsResolver (e2e)', () => { + it('should find many searchCalendarEvents', () => { + const queryData = { + query: ` + query searchCalendarEvents { + searchCalendarEvents { + edges { + node { + title + isCanceled + isFullDay + startsAt + endsAt + externalCreatedAt + externalUpdatedAt + description + location + iCalUID + conferenceSolution + id + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchCalendarEvents; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchCalendarEvents = edges[0].node; + + expect(searchCalendarEvents).toHaveProperty('title'); + expect(searchCalendarEvents).toHaveProperty('isCanceled'); + expect(searchCalendarEvents).toHaveProperty('isFullDay'); + expect(searchCalendarEvents).toHaveProperty('startsAt'); + expect(searchCalendarEvents).toHaveProperty('endsAt'); + expect(searchCalendarEvents).toHaveProperty('externalCreatedAt'); + expect(searchCalendarEvents).toHaveProperty('externalUpdatedAt'); + expect(searchCalendarEvents).toHaveProperty('description'); + expect(searchCalendarEvents).toHaveProperty('location'); + expect(searchCalendarEvents).toHaveProperty('iCalUID'); + expect(searchCalendarEvents).toHaveProperty('conferenceSolution'); + expect(searchCalendarEvents).toHaveProperty('id'); + expect(searchCalendarEvents).toHaveProperty('createdAt'); + expect(searchCalendarEvents).toHaveProperty('updatedAt'); + expect(searchCalendarEvents).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-comments.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-comments.integration-spec.ts new file mode 100644 index 000000000000..549f1d3011d1 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-comments.integration-spec.ts @@ -0,0 +1,57 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchCommentsResolver (e2e)', () => { + it('should find many searchComments', () => { + const queryData = { + query: ` + query searchComments { + searchComments { + edges { + node { + body + id + createdAt + updatedAt + deletedAt + authorId + activityId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchComments; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchComments = edges[0].node; + + expect(searchComments).toHaveProperty('body'); + expect(searchComments).toHaveProperty('id'); + expect(searchComments).toHaveProperty('createdAt'); + expect(searchComments).toHaveProperty('updatedAt'); + expect(searchComments).toHaveProperty('deletedAt'); + expect(searchComments).toHaveProperty('authorId'); + expect(searchComments).toHaveProperty('activityId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-companies.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-companies.integration-spec.ts new file mode 100644 index 000000000000..da309385f76f --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-companies.integration-spec.ts @@ -0,0 +1,69 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchCompaniesResolver (e2e)', () => { + it('should find many searchCompanies', () => { + const queryData = { + query: ` + query searchCompanies { + searchCompanies { + edges { + node { + name + employees + idealCustomerProfile + position + searchVector + id + createdAt + updatedAt + deletedAt + accountOwnerId + tagline + workPolicy + visaSponsorship + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchCompanies; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchCompanies = edges[0].node; + + expect(searchCompanies).toHaveProperty('name'); + expect(searchCompanies).toHaveProperty('employees'); + expect(searchCompanies).toHaveProperty('idealCustomerProfile'); + expect(searchCompanies).toHaveProperty('position'); + expect(searchCompanies).toHaveProperty('searchVector'); + expect(searchCompanies).toHaveProperty('id'); + expect(searchCompanies).toHaveProperty('createdAt'); + expect(searchCompanies).toHaveProperty('updatedAt'); + expect(searchCompanies).toHaveProperty('deletedAt'); + expect(searchCompanies).toHaveProperty('accountOwnerId'); + expect(searchCompanies).toHaveProperty('tagline'); + expect(searchCompanies).toHaveProperty('workPolicy'); + expect(searchCompanies).toHaveProperty('visaSponsorship'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-connected-accounts.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-connected-accounts.integration-spec.ts new file mode 100644 index 000000000000..d00c81ecb1a9 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-connected-accounts.integration-spec.ts @@ -0,0 +1,69 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchConnectedAccountsResolver (e2e)', () => { + it('should find many searchConnectedAccounts', () => { + const queryData = { + query: ` + query searchConnectedAccounts { + searchConnectedAccounts { + edges { + node { + handle + provider + accessToken + refreshToken + lastSyncHistoryId + authFailedAt + handleAliases + scopes + id + createdAt + updatedAt + deletedAt + accountOwnerId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchConnectedAccounts; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchConnectedAccounts = edges[0].node; + + expect(searchConnectedAccounts).toHaveProperty('handle'); + expect(searchConnectedAccounts).toHaveProperty('provider'); + expect(searchConnectedAccounts).toHaveProperty('accessToken'); + expect(searchConnectedAccounts).toHaveProperty('refreshToken'); + expect(searchConnectedAccounts).toHaveProperty('lastSyncHistoryId'); + expect(searchConnectedAccounts).toHaveProperty('authFailedAt'); + expect(searchConnectedAccounts).toHaveProperty('handleAliases'); + expect(searchConnectedAccounts).toHaveProperty('scopes'); + expect(searchConnectedAccounts).toHaveProperty('id'); + expect(searchConnectedAccounts).toHaveProperty('createdAt'); + expect(searchConnectedAccounts).toHaveProperty('updatedAt'); + expect(searchConnectedAccounts).toHaveProperty('deletedAt'); + expect(searchConnectedAccounts).toHaveProperty('accountOwnerId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-favorites.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-favorites.integration-spec.ts new file mode 100644 index 000000000000..982aff72675a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-favorites.integration-spec.ts @@ -0,0 +1,75 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchFavoritesResolver (e2e)', () => { + it('should find many searchFavorites', () => { + const queryData = { + query: ` + query searchFavorites { + searchFavorites { + edges { + node { + position + id + createdAt + updatedAt + deletedAt + workspaceMemberId + personId + companyId + opportunityId + workflowId + workflowVersionId + workflowRunId + taskId + noteId + viewId + rocketId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchFavorites; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchFavorites = edges[0].node; + + expect(searchFavorites).toHaveProperty('position'); + expect(searchFavorites).toHaveProperty('id'); + expect(searchFavorites).toHaveProperty('createdAt'); + expect(searchFavorites).toHaveProperty('updatedAt'); + expect(searchFavorites).toHaveProperty('deletedAt'); + expect(searchFavorites).toHaveProperty('workspaceMemberId'); + expect(searchFavorites).toHaveProperty('personId'); + expect(searchFavorites).toHaveProperty('companyId'); + expect(searchFavorites).toHaveProperty('opportunityId'); + expect(searchFavorites).toHaveProperty('workflowId'); + expect(searchFavorites).toHaveProperty('workflowVersionId'); + expect(searchFavorites).toHaveProperty('workflowRunId'); + expect(searchFavorites).toHaveProperty('taskId'); + expect(searchFavorites).toHaveProperty('noteId'); + expect(searchFavorites).toHaveProperty('viewId'); + expect(searchFavorites).toHaveProperty('rocketId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-channel-message-associations.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-channel-message-associations.integration-spec.ts new file mode 100644 index 000000000000..514b67bb32a3 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-channel-message-associations.integration-spec.ts @@ -0,0 +1,77 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchMessageChannelMessageAssociationsResolver (e2e)', () => { + it('should find many searchMessageChannelMessageAssociations', () => { + const queryData = { + query: ` + query searchMessageChannelMessageAssociations { + searchMessageChannelMessageAssociations { + edges { + node { + messageExternalId + messageThreadExternalId + direction + id + createdAt + updatedAt + deletedAt + messageChannelId + messageId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchMessageChannelMessageAssociations; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchMessageChannelMessageAssociations = edges[0].node; + + expect(searchMessageChannelMessageAssociations).toHaveProperty( + 'messageExternalId', + ); + expect(searchMessageChannelMessageAssociations).toHaveProperty( + 'messageThreadExternalId', + ); + expect(searchMessageChannelMessageAssociations).toHaveProperty( + 'direction', + ); + expect(searchMessageChannelMessageAssociations).toHaveProperty('id'); + expect(searchMessageChannelMessageAssociations).toHaveProperty( + 'createdAt', + ); + expect(searchMessageChannelMessageAssociations).toHaveProperty( + 'updatedAt', + ); + expect(searchMessageChannelMessageAssociations).toHaveProperty( + 'deletedAt', + ); + expect(searchMessageChannelMessageAssociations).toHaveProperty( + 'messageChannelId', + ); + expect(searchMessageChannelMessageAssociations).toHaveProperty( + 'messageId', + ); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-channels.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-channels.integration-spec.ts new file mode 100644 index 000000000000..c39ccae7c5de --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-channels.integration-spec.ts @@ -0,0 +1,87 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchMessageChannelsResolver (e2e)', () => { + it('should find many searchMessageChannels', () => { + const queryData = { + query: ` + query searchMessageChannels { + searchMessageChannels { + edges { + node { + visibility + handle + type + isContactAutoCreationEnabled + contactAutoCreationPolicy + excludeNonProfessionalEmails + excludeGroupEmails + isSyncEnabled + syncCursor + syncedAt + syncStatus + syncStage + syncStageStartedAt + throttleFailureCount + id + createdAt + updatedAt + deletedAt + connectedAccountId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchMessageChannels; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchMessageChannels = edges[0].node; + + expect(searchMessageChannels).toHaveProperty('visibility'); + expect(searchMessageChannels).toHaveProperty('handle'); + expect(searchMessageChannels).toHaveProperty('type'); + expect(searchMessageChannels).toHaveProperty( + 'isContactAutoCreationEnabled', + ); + expect(searchMessageChannels).toHaveProperty( + 'contactAutoCreationPolicy', + ); + expect(searchMessageChannels).toHaveProperty( + 'excludeNonProfessionalEmails', + ); + expect(searchMessageChannels).toHaveProperty('excludeGroupEmails'); + expect(searchMessageChannels).toHaveProperty('isSyncEnabled'); + expect(searchMessageChannels).toHaveProperty('syncCursor'); + expect(searchMessageChannels).toHaveProperty('syncedAt'); + expect(searchMessageChannels).toHaveProperty('syncStatus'); + expect(searchMessageChannels).toHaveProperty('syncStage'); + expect(searchMessageChannels).toHaveProperty('syncStageStartedAt'); + expect(searchMessageChannels).toHaveProperty('throttleFailureCount'); + expect(searchMessageChannels).toHaveProperty('id'); + expect(searchMessageChannels).toHaveProperty('createdAt'); + expect(searchMessageChannels).toHaveProperty('updatedAt'); + expect(searchMessageChannels).toHaveProperty('deletedAt'); + expect(searchMessageChannels).toHaveProperty('connectedAccountId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-participants.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-participants.integration-spec.ts new file mode 100644 index 000000000000..71b9ee486251 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-participants.integration-spec.ts @@ -0,0 +1,63 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchMessageParticipantsResolver (e2e)', () => { + it('should find many searchMessageParticipants', () => { + const queryData = { + query: ` + query searchMessageParticipants { + searchMessageParticipants { + edges { + node { + role + handle + displayName + id + createdAt + updatedAt + deletedAt + messageId + personId + workspaceMemberId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchMessageParticipants; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchMessageParticipants = edges[0].node; + + expect(searchMessageParticipants).toHaveProperty('role'); + expect(searchMessageParticipants).toHaveProperty('handle'); + expect(searchMessageParticipants).toHaveProperty('displayName'); + expect(searchMessageParticipants).toHaveProperty('id'); + expect(searchMessageParticipants).toHaveProperty('createdAt'); + expect(searchMessageParticipants).toHaveProperty('updatedAt'); + expect(searchMessageParticipants).toHaveProperty('deletedAt'); + expect(searchMessageParticipants).toHaveProperty('messageId'); + expect(searchMessageParticipants).toHaveProperty('personId'); + expect(searchMessageParticipants).toHaveProperty('workspaceMemberId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-threads.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-threads.integration-spec.ts new file mode 100644 index 000000000000..5c38ebfeb441 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-threads.integration-spec.ts @@ -0,0 +1,51 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchMessageThreadsResolver (e2e)', () => { + it('should find many searchMessageThreads', () => { + const queryData = { + query: ` + query searchMessageThreads { + searchMessageThreads { + edges { + node { + id + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchMessageThreads; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchMessageThreads = edges[0].node; + + expect(searchMessageThreads).toHaveProperty('id'); + expect(searchMessageThreads).toHaveProperty('createdAt'); + expect(searchMessageThreads).toHaveProperty('updatedAt'); + expect(searchMessageThreads).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-messages.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-messages.integration-spec.ts new file mode 100644 index 000000000000..4865fb8d312b --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-messages.integration-spec.ts @@ -0,0 +1,61 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchMessagesResolver (e2e)', () => { + it('should find many searchMessages', () => { + const queryData = { + query: ` + query searchMessages { + searchMessages { + edges { + node { + headerMessageId + subject + text + receivedAt + id + createdAt + updatedAt + deletedAt + messageThreadId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchMessages; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchMessages = edges[0].node; + + expect(searchMessages).toHaveProperty('headerMessageId'); + expect(searchMessages).toHaveProperty('subject'); + expect(searchMessages).toHaveProperty('text'); + expect(searchMessages).toHaveProperty('receivedAt'); + expect(searchMessages).toHaveProperty('id'); + expect(searchMessages).toHaveProperty('createdAt'); + expect(searchMessages).toHaveProperty('updatedAt'); + expect(searchMessages).toHaveProperty('deletedAt'); + expect(searchMessages).toHaveProperty('messageThreadId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-note-targets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-note-targets.integration-spec.ts new file mode 100644 index 000000000000..45188e8aca7c --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-note-targets.integration-spec.ts @@ -0,0 +1,61 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchNoteTargetsResolver (e2e)', () => { + it('should find many searchNoteTargets', () => { + const queryData = { + query: ` + query searchNoteTargets { + searchNoteTargets { + edges { + node { + id + createdAt + updatedAt + deletedAt + noteId + personId + companyId + opportunityId + rocketId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchNoteTargets; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchNoteTargets = edges[0].node; + + expect(searchNoteTargets).toHaveProperty('id'); + expect(searchNoteTargets).toHaveProperty('createdAt'); + expect(searchNoteTargets).toHaveProperty('updatedAt'); + expect(searchNoteTargets).toHaveProperty('deletedAt'); + expect(searchNoteTargets).toHaveProperty('noteId'); + expect(searchNoteTargets).toHaveProperty('personId'); + expect(searchNoteTargets).toHaveProperty('companyId'); + expect(searchNoteTargets).toHaveProperty('opportunityId'); + expect(searchNoteTargets).toHaveProperty('rocketId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-notes.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-notes.integration-spec.ts new file mode 100644 index 000000000000..8965c5006f26 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-notes.integration-spec.ts @@ -0,0 +1,57 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchNotesResolver (e2e)', () => { + it('should find many searchNotes', () => { + const queryData = { + query: ` + query searchNotes { + searchNotes { + edges { + node { + position + title + body + id + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchNotes; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchNotes = edges[0].node; + + expect(searchNotes).toHaveProperty('position'); + expect(searchNotes).toHaveProperty('title'); + expect(searchNotes).toHaveProperty('body'); + expect(searchNotes).toHaveProperty('id'); + expect(searchNotes).toHaveProperty('createdAt'); + expect(searchNotes).toHaveProperty('updatedAt'); + expect(searchNotes).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-opportunities.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-opportunities.integration-spec.ts new file mode 100644 index 000000000000..0f63d73d7175 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-opportunities.integration-spec.ts @@ -0,0 +1,65 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchOpportunitiesResolver (e2e)', () => { + it('should find many searchOpportunities', () => { + const queryData = { + query: ` + query searchOpportunities { + searchOpportunities { + edges { + node { + name + closeDate + stage + position + searchVector + id + createdAt + updatedAt + deletedAt + pointOfContactId + companyId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchOpportunities; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchOpportunities = edges[0].node; + + expect(searchOpportunities).toHaveProperty('name'); + expect(searchOpportunities).toHaveProperty('closeDate'); + expect(searchOpportunities).toHaveProperty('stage'); + expect(searchOpportunities).toHaveProperty('position'); + expect(searchOpportunities).toHaveProperty('searchVector'); + expect(searchOpportunities).toHaveProperty('id'); + expect(searchOpportunities).toHaveProperty('createdAt'); + expect(searchOpportunities).toHaveProperty('updatedAt'); + expect(searchOpportunities).toHaveProperty('deletedAt'); + expect(searchOpportunities).toHaveProperty('pointOfContactId'); + expect(searchOpportunities).toHaveProperty('companyId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-people.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-people.integration-spec.ts new file mode 100644 index 000000000000..8c45c0c7e29b --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-people.integration-spec.ts @@ -0,0 +1,69 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchPeopleResolver (e2e)', () => { + it('should find many searchPeople', () => { + const queryData = { + query: ` + query searchPeople { + searchPeople { + edges { + node { + jobTitle + city + avatarUrl + position + searchVector + id + createdAt + updatedAt + deletedAt + companyId + intro + workPreference + performanceRating + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchPeople; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchPeople = edges[0].node; + + expect(searchPeople).toHaveProperty('jobTitle'); + expect(searchPeople).toHaveProperty('city'); + expect(searchPeople).toHaveProperty('avatarUrl'); + expect(searchPeople).toHaveProperty('position'); + expect(searchPeople).toHaveProperty('searchVector'); + expect(searchPeople).toHaveProperty('id'); + expect(searchPeople).toHaveProperty('createdAt'); + expect(searchPeople).toHaveProperty('updatedAt'); + expect(searchPeople).toHaveProperty('deletedAt'); + expect(searchPeople).toHaveProperty('companyId'); + expect(searchPeople).toHaveProperty('intro'); + expect(searchPeople).toHaveProperty('workPreference'); + expect(searchPeople).toHaveProperty('performanceRating'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-rockets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-rockets.integration-spec.ts new file mode 100644 index 000000000000..1bf738515746 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-rockets.integration-spec.ts @@ -0,0 +1,57 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchRocketsResolver (e2e)', () => { + it('should find many searchRockets', () => { + const queryData = { + query: ` + query searchRockets { + searchRockets { + edges { + node { + id + name + createdAt + updatedAt + deletedAt + position + searchVector + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchRockets; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchRockets = edges[0].node; + + expect(searchRockets).toHaveProperty('id'); + expect(searchRockets).toHaveProperty('name'); + expect(searchRockets).toHaveProperty('createdAt'); + expect(searchRockets).toHaveProperty('updatedAt'); + expect(searchRockets).toHaveProperty('deletedAt'); + expect(searchRockets).toHaveProperty('position'); + expect(searchRockets).toHaveProperty('searchVector'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-task-targets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-task-targets.integration-spec.ts new file mode 100644 index 000000000000..76f9d7b1ec92 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-task-targets.integration-spec.ts @@ -0,0 +1,61 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchTaskTargetsResolver (e2e)', () => { + it('should find many searchTaskTargets', () => { + const queryData = { + query: ` + query searchTaskTargets { + searchTaskTargets { + edges { + node { + id + createdAt + updatedAt + deletedAt + taskId + personId + companyId + opportunityId + rocketId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchTaskTargets; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchTaskTargets = edges[0].node; + + expect(searchTaskTargets).toHaveProperty('id'); + expect(searchTaskTargets).toHaveProperty('createdAt'); + expect(searchTaskTargets).toHaveProperty('updatedAt'); + expect(searchTaskTargets).toHaveProperty('deletedAt'); + expect(searchTaskTargets).toHaveProperty('taskId'); + expect(searchTaskTargets).toHaveProperty('personId'); + expect(searchTaskTargets).toHaveProperty('companyId'); + expect(searchTaskTargets).toHaveProperty('opportunityId'); + expect(searchTaskTargets).toHaveProperty('rocketId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-tasks.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-tasks.integration-spec.ts new file mode 100644 index 000000000000..d9af7a1c6a42 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-tasks.integration-spec.ts @@ -0,0 +1,63 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchTasksResolver (e2e)', () => { + it('should find many searchTasks', () => { + const queryData = { + query: ` + query searchTasks { + searchTasks { + edges { + node { + position + title + body + dueAt + status + id + createdAt + updatedAt + deletedAt + assigneeId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchTasks; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchTasks = edges[0].node; + + expect(searchTasks).toHaveProperty('position'); + expect(searchTasks).toHaveProperty('title'); + expect(searchTasks).toHaveProperty('body'); + expect(searchTasks).toHaveProperty('dueAt'); + expect(searchTasks).toHaveProperty('status'); + expect(searchTasks).toHaveProperty('id'); + expect(searchTasks).toHaveProperty('createdAt'); + expect(searchTasks).toHaveProperty('updatedAt'); + expect(searchTasks).toHaveProperty('deletedAt'); + expect(searchTasks).toHaveProperty('assigneeId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-timeline-activities.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-timeline-activities.integration-spec.ts new file mode 100644 index 000000000000..b87ec60f1dd4 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-timeline-activities.integration-spec.ts @@ -0,0 +1,87 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchTimelineActivitiesResolver (e2e)', () => { + it('should find many searchTimelineActivities', () => { + const queryData = { + query: ` + query searchTimelineActivities { + searchTimelineActivities { + edges { + node { + happensAt + name + properties + linkedRecordCachedName + linkedRecordId + linkedObjectMetadataId + id + createdAt + updatedAt + deletedAt + workspaceMemberId + personId + companyId + opportunityId + noteId + taskId + workflowId + workflowVersionId + workflowRunId + rocketId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchTimelineActivities; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchTimelineActivities = edges[0].node; + + expect(searchTimelineActivities).toHaveProperty('happensAt'); + expect(searchTimelineActivities).toHaveProperty('name'); + expect(searchTimelineActivities).toHaveProperty('properties'); + expect(searchTimelineActivities).toHaveProperty( + 'linkedRecordCachedName', + ); + expect(searchTimelineActivities).toHaveProperty('linkedRecordId'); + expect(searchTimelineActivities).toHaveProperty( + 'linkedObjectMetadataId', + ); + expect(searchTimelineActivities).toHaveProperty('id'); + expect(searchTimelineActivities).toHaveProperty('createdAt'); + expect(searchTimelineActivities).toHaveProperty('updatedAt'); + expect(searchTimelineActivities).toHaveProperty('deletedAt'); + expect(searchTimelineActivities).toHaveProperty('workspaceMemberId'); + expect(searchTimelineActivities).toHaveProperty('personId'); + expect(searchTimelineActivities).toHaveProperty('companyId'); + expect(searchTimelineActivities).toHaveProperty('opportunityId'); + expect(searchTimelineActivities).toHaveProperty('noteId'); + expect(searchTimelineActivities).toHaveProperty('taskId'); + expect(searchTimelineActivities).toHaveProperty('workflowId'); + expect(searchTimelineActivities).toHaveProperty('workflowVersionId'); + expect(searchTimelineActivities).toHaveProperty('workflowRunId'); + expect(searchTimelineActivities).toHaveProperty('rocketId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-fields.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-fields.integration-spec.ts new file mode 100644 index 000000000000..b84def1a813c --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-fields.integration-spec.ts @@ -0,0 +1,61 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchViewFieldsResolver (e2e)', () => { + it('should find many searchViewFields', () => { + const queryData = { + query: ` + query searchViewFields { + searchViewFields { + edges { + node { + fieldMetadataId + isVisible + size + position + id + createdAt + updatedAt + deletedAt + viewId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchViewFields; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchViewFields = edges[0].node; + + expect(searchViewFields).toHaveProperty('fieldMetadataId'); + expect(searchViewFields).toHaveProperty('isVisible'); + expect(searchViewFields).toHaveProperty('size'); + expect(searchViewFields).toHaveProperty('position'); + expect(searchViewFields).toHaveProperty('id'); + expect(searchViewFields).toHaveProperty('createdAt'); + expect(searchViewFields).toHaveProperty('updatedAt'); + expect(searchViewFields).toHaveProperty('deletedAt'); + expect(searchViewFields).toHaveProperty('viewId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-filters.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-filters.integration-spec.ts new file mode 100644 index 000000000000..fe1e96c4b63c --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-filters.integration-spec.ts @@ -0,0 +1,61 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchViewFiltersResolver (e2e)', () => { + it('should find many searchViewFilters', () => { + const queryData = { + query: ` + query searchViewFilters { + searchViewFilters { + edges { + node { + fieldMetadataId + operand + value + displayValue + id + createdAt + updatedAt + deletedAt + viewId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchViewFilters; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchViewFilters = edges[0].node; + + expect(searchViewFilters).toHaveProperty('fieldMetadataId'); + expect(searchViewFilters).toHaveProperty('operand'); + expect(searchViewFilters).toHaveProperty('value'); + expect(searchViewFilters).toHaveProperty('displayValue'); + expect(searchViewFilters).toHaveProperty('id'); + expect(searchViewFilters).toHaveProperty('createdAt'); + expect(searchViewFilters).toHaveProperty('updatedAt'); + expect(searchViewFilters).toHaveProperty('deletedAt'); + expect(searchViewFilters).toHaveProperty('viewId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-sorts.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-sorts.integration-spec.ts new file mode 100644 index 000000000000..7f5b2e6f6a91 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-sorts.integration-spec.ts @@ -0,0 +1,57 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchViewSortsResolver (e2e)', () => { + it('should find many searchViewSorts', () => { + const queryData = { + query: ` + query searchViewSorts { + searchViewSorts { + edges { + node { + fieldMetadataId + direction + id + createdAt + updatedAt + deletedAt + viewId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchViewSorts; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchViewSorts = edges[0].node; + + expect(searchViewSorts).toHaveProperty('fieldMetadataId'); + expect(searchViewSorts).toHaveProperty('direction'); + expect(searchViewSorts).toHaveProperty('id'); + expect(searchViewSorts).toHaveProperty('createdAt'); + expect(searchViewSorts).toHaveProperty('updatedAt'); + expect(searchViewSorts).toHaveProperty('deletedAt'); + expect(searchViewSorts).toHaveProperty('viewId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-views.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-views.integration-spec.ts new file mode 100644 index 000000000000..716c4a843301 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-views.integration-spec.ts @@ -0,0 +1,67 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchViewsResolver (e2e)', () => { + it('should find many searchViews', () => { + const queryData = { + query: ` + query searchViews { + searchViews { + edges { + node { + name + objectMetadataId + type + key + icon + kanbanFieldMetadataId + position + isCompact + id + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchViews; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchViews = edges[0].node; + + expect(searchViews).toHaveProperty('name'); + expect(searchViews).toHaveProperty('objectMetadataId'); + expect(searchViews).toHaveProperty('type'); + expect(searchViews).toHaveProperty('key'); + expect(searchViews).toHaveProperty('icon'); + expect(searchViews).toHaveProperty('kanbanFieldMetadataId'); + expect(searchViews).toHaveProperty('position'); + expect(searchViews).toHaveProperty('isCompact'); + expect(searchViews).toHaveProperty('id'); + expect(searchViews).toHaveProperty('createdAt'); + expect(searchViews).toHaveProperty('updatedAt'); + expect(searchViews).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-webhooks.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-webhooks.integration-spec.ts new file mode 100644 index 000000000000..d5a93db25ec0 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-webhooks.integration-spec.ts @@ -0,0 +1,57 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchWebhooksResolver (e2e)', () => { + it('should find many searchWebhooks', () => { + const queryData = { + query: ` + query searchWebhooks { + searchWebhooks { + edges { + node { + id + targetUrl + operation + description + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchWebhooks; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchWebhooks = edges[0].node; + + expect(searchWebhooks).toHaveProperty('id'); + expect(searchWebhooks).toHaveProperty('targetUrl'); + expect(searchWebhooks).toHaveProperty('operation'); + expect(searchWebhooks).toHaveProperty('description'); + expect(searchWebhooks).toHaveProperty('createdAt'); + expect(searchWebhooks).toHaveProperty('updatedAt'); + expect(searchWebhooks).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-event-listeners.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-event-listeners.integration-spec.ts new file mode 100644 index 000000000000..ddf55a1a493c --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-event-listeners.integration-spec.ts @@ -0,0 +1,55 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchWorkflowEventListenersResolver (e2e)', () => { + it('should find many searchWorkflowEventListeners', () => { + const queryData = { + query: ` + query searchWorkflowEventListeners { + searchWorkflowEventListeners { + edges { + node { + eventName + id + createdAt + updatedAt + deletedAt + workflowId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchWorkflowEventListeners; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchWorkflowEventListeners = edges[0].node; + + expect(searchWorkflowEventListeners).toHaveProperty('eventName'); + expect(searchWorkflowEventListeners).toHaveProperty('id'); + expect(searchWorkflowEventListeners).toHaveProperty('createdAt'); + expect(searchWorkflowEventListeners).toHaveProperty('updatedAt'); + expect(searchWorkflowEventListeners).toHaveProperty('deletedAt'); + expect(searchWorkflowEventListeners).toHaveProperty('workflowId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-runs.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-runs.integration-spec.ts new file mode 100644 index 000000000000..6307d8ae6bbd --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-runs.integration-spec.ts @@ -0,0 +1,69 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchWorkflowRunsResolver (e2e)', () => { + it('should find many searchWorkflowRuns', () => { + const queryData = { + query: ` + query searchWorkflowRuns { + searchWorkflowRuns { + edges { + node { + workflowRunId + name + startedAt + endedAt + status + output + position + id + createdAt + updatedAt + deletedAt + workflowVersionId + workflowId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchWorkflowRuns; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchWorkflowRuns = edges[0].node; + + expect(searchWorkflowRuns).toHaveProperty('workflowRunId'); + expect(searchWorkflowRuns).toHaveProperty('name'); + expect(searchWorkflowRuns).toHaveProperty('startedAt'); + expect(searchWorkflowRuns).toHaveProperty('endedAt'); + expect(searchWorkflowRuns).toHaveProperty('status'); + expect(searchWorkflowRuns).toHaveProperty('output'); + expect(searchWorkflowRuns).toHaveProperty('position'); + expect(searchWorkflowRuns).toHaveProperty('id'); + expect(searchWorkflowRuns).toHaveProperty('createdAt'); + expect(searchWorkflowRuns).toHaveProperty('updatedAt'); + expect(searchWorkflowRuns).toHaveProperty('deletedAt'); + expect(searchWorkflowRuns).toHaveProperty('workflowVersionId'); + expect(searchWorkflowRuns).toHaveProperty('workflowId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-versions.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-versions.integration-spec.ts new file mode 100644 index 000000000000..86bd6df8099c --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-versions.integration-spec.ts @@ -0,0 +1,63 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchWorkflowVersionsResolver (e2e)', () => { + it('should find many searchWorkflowVersions', () => { + const queryData = { + query: ` + query searchWorkflowVersions { + searchWorkflowVersions { + edges { + node { + name + trigger + steps + status + position + id + createdAt + updatedAt + deletedAt + workflowId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchWorkflowVersions; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchWorkflowVersions = edges[0].node; + + expect(searchWorkflowVersions).toHaveProperty('name'); + expect(searchWorkflowVersions).toHaveProperty('trigger'); + expect(searchWorkflowVersions).toHaveProperty('steps'); + expect(searchWorkflowVersions).toHaveProperty('status'); + expect(searchWorkflowVersions).toHaveProperty('position'); + expect(searchWorkflowVersions).toHaveProperty('id'); + expect(searchWorkflowVersions).toHaveProperty('createdAt'); + expect(searchWorkflowVersions).toHaveProperty('updatedAt'); + expect(searchWorkflowVersions).toHaveProperty('deletedAt'); + expect(searchWorkflowVersions).toHaveProperty('workflowId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflows.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflows.integration-spec.ts new file mode 100644 index 000000000000..b12d780dfeb5 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflows.integration-spec.ts @@ -0,0 +1,59 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchWorkflowsResolver (e2e)', () => { + it('should find many searchWorkflows', () => { + const queryData = { + query: ` + query searchWorkflows { + searchWorkflows { + edges { + node { + name + lastPublishedVersionId + statuses + position + id + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchWorkflows; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchWorkflows = edges[0].node; + + expect(searchWorkflows).toHaveProperty('name'); + expect(searchWorkflows).toHaveProperty('lastPublishedVersionId'); + expect(searchWorkflows).toHaveProperty('statuses'); + expect(searchWorkflows).toHaveProperty('position'); + expect(searchWorkflows).toHaveProperty('id'); + expect(searchWorkflows).toHaveProperty('createdAt'); + expect(searchWorkflows).toHaveProperty('updatedAt'); + expect(searchWorkflows).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workspace-members.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workspace-members.integration-spec.ts new file mode 100644 index 000000000000..efc76b40438a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workspace-members.integration-spec.ts @@ -0,0 +1,67 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchWorkspaceMembersResolver (e2e)', () => { + it('should find many searchWorkspaceMembers', () => { + const queryData = { + query: ` + query searchWorkspaceMembers { + searchWorkspaceMembers { + edges { + node { + id + colorScheme + avatarUrl + locale + timeZone + dateFormat + timeFormat + userEmail + userId + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchWorkspaceMembers; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchWorkspaceMembers = edges[0].node; + + expect(searchWorkspaceMembers).toHaveProperty('id'); + expect(searchWorkspaceMembers).toHaveProperty('colorScheme'); + expect(searchWorkspaceMembers).toHaveProperty('avatarUrl'); + expect(searchWorkspaceMembers).toHaveProperty('locale'); + expect(searchWorkspaceMembers).toHaveProperty('timeZone'); + expect(searchWorkspaceMembers).toHaveProperty('dateFormat'); + expect(searchWorkspaceMembers).toHaveProperty('timeFormat'); + expect(searchWorkspaceMembers).toHaveProperty('userEmail'); + expect(searchWorkspaceMembers).toHaveProperty('userId'); + expect(searchWorkspaceMembers).toHaveProperty('createdAt'); + expect(searchWorkspaceMembers).toHaveProperty('updatedAt'); + expect(searchWorkspaceMembers).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/serverless-functions.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/serverless-functions.integration-spec.ts new file mode 100644 index 000000000000..9e8d50619e0d --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/serverless-functions.integration-spec.ts @@ -0,0 +1,59 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('serverlessFunctionsResolver (e2e)', () => { + it('should find many serverlessFunctions', () => { + const queryData = { + query: ` + query serverlessFunctions { + serverlessFunctions { + edges { + node { + id + name + description + runtime + latestVersion + syncStatus + createdAt + updatedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.serverlessFunctions; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const serverlessFunctions = edges[0].node; + + expect(serverlessFunctions).toHaveProperty('id'); + expect(serverlessFunctions).toHaveProperty('name'); + expect(serverlessFunctions).toHaveProperty('description'); + expect(serverlessFunctions).toHaveProperty('runtime'); + expect(serverlessFunctions).toHaveProperty('latestVersion'); + expect(serverlessFunctions).toHaveProperty('syncStatus'); + expect(serverlessFunctions).toHaveProperty('createdAt'); + expect(serverlessFunctions).toHaveProperty('updatedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/task-targets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/task-targets.integration-spec.ts similarity index 92% rename from packages/twenty-server/test/task-targets.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/task-targets.integration-spec.ts index e54e855d31e5..b9d5cb493093 100644 --- a/packages/twenty-server/test/task-targets.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/task-targets.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('taskTargetsResolver (integration)', () => { +describe('taskTargetsResolver (e2e)', () => { it('should find many taskTargets', () => { const queryData = { query: ` @@ -18,6 +18,7 @@ describe('taskTargetsResolver (integration)', () => { personId companyId opportunityId + rocketId } } } @@ -53,6 +54,7 @@ describe('taskTargetsResolver (integration)', () => { expect(taskTargets).toHaveProperty('personId'); expect(taskTargets).toHaveProperty('companyId'); expect(taskTargets).toHaveProperty('opportunityId'); + expect(taskTargets).toHaveProperty('rocketId'); } }); }); diff --git a/packages/twenty-server/test/tasks.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/tasks.integration-spec.ts similarity index 97% rename from packages/twenty-server/test/tasks.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/tasks.integration-spec.ts index 900fd3de5ce1..016341966bd0 100644 --- a/packages/twenty-server/test/tasks.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/tasks.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('tasksResolver (integration)', () => { +describe('tasksResolver (e2e)', () => { it('should find many tasks', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/timeline-activities.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/timeline-activities.integration-spec.ts similarity index 84% rename from packages/twenty-server/test/timeline-activities.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/timeline-activities.integration-spec.ts index a5ef6a5f9662..3e5c72fec0ac 100644 --- a/packages/twenty-server/test/timeline-activities.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/timeline-activities.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('timelineActivitiesResolver (integration)', () => { +describe('timelineActivitiesResolver (e2e)', () => { it('should find many timelineActivities', () => { const queryData = { query: ` @@ -26,6 +26,10 @@ describe('timelineActivitiesResolver (integration)', () => { opportunityId noteId taskId + workflowId + workflowVersionId + workflowRunId + rocketId } } } @@ -69,6 +73,10 @@ describe('timelineActivitiesResolver (integration)', () => { expect(timelineActivities).toHaveProperty('opportunityId'); expect(timelineActivities).toHaveProperty('noteId'); expect(timelineActivities).toHaveProperty('taskId'); + expect(timelineActivities).toHaveProperty('workflowId'); + expect(timelineActivities).toHaveProperty('workflowVersionId'); + expect(timelineActivities).toHaveProperty('workflowRunId'); + expect(timelineActivities).toHaveProperty('rocketId'); } }); }); diff --git a/packages/twenty-server/test/view-fields.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/view-fields.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/view-fields.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/view-fields.integration-spec.ts index 058568763900..24b28bc5b6d9 100644 --- a/packages/twenty-server/test/view-fields.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/view-fields.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('viewFieldsResolver (integration)', () => { +describe('viewFieldsResolver (e2e)', () => { it('should find many viewFields', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/view-filters.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/view-filters.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/view-filters.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/view-filters.integration-spec.ts index 8caa942b2b61..e76c2f12fd08 100644 --- a/packages/twenty-server/test/view-filters.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/view-filters.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('viewFiltersResolver (integration)', () => { +describe('viewFiltersResolver (e2e)', () => { it('should find many viewFilters', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/view-sorts.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/view-sorts.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/view-sorts.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/view-sorts.integration-spec.ts index fc29b1d4c293..850d24cf8795 100644 --- a/packages/twenty-server/test/view-sorts.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/view-sorts.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('viewSortsResolver (integration)', () => { +describe('viewSortsResolver (e2e)', () => { it('should find many viewSorts', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/views.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/views.integration-spec.ts similarity index 97% rename from packages/twenty-server/test/views.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/views.integration-spec.ts index 122a8c398fcc..29cf849985f0 100644 --- a/packages/twenty-server/test/views.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/views.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('viewsResolver (integration)', () => { +describe('viewsResolver (e2e)', () => { it('should find many views', () => { const queryData = { query: ` @@ -10,13 +10,13 @@ describe('viewsResolver (integration)', () => { views { edges { node { - position name objectMetadataId type key icon kanbanFieldMetadataId + position isCompact id createdAt @@ -49,13 +49,13 @@ describe('viewsResolver (integration)', () => { if (edges.length > 0) { const views = edges[0].node; - expect(views).toHaveProperty('position'); expect(views).toHaveProperty('name'); expect(views).toHaveProperty('objectMetadataId'); expect(views).toHaveProperty('type'); expect(views).toHaveProperty('key'); expect(views).toHaveProperty('icon'); expect(views).toHaveProperty('kanbanFieldMetadataId'); + expect(views).toHaveProperty('position'); expect(views).toHaveProperty('isCompact'); expect(views).toHaveProperty('id'); expect(views).toHaveProperty('createdAt'); diff --git a/packages/twenty-server/test/webhooks.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/webhooks.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/webhooks.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/webhooks.integration-spec.ts index 7c4224b69aa6..aaf181bf3849 100644 --- a/packages/twenty-server/test/webhooks.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/webhooks.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('webhooksResolver (integration)', () => { +describe('webhooksResolver (e2e)', () => { it('should find many webhooks', () => { const queryData = { query: ` @@ -10,10 +10,10 @@ describe('webhooksResolver (integration)', () => { webhooks { edges { node { + id targetUrl operation description - id createdAt updatedAt deletedAt @@ -44,10 +44,10 @@ describe('webhooksResolver (integration)', () => { if (edges.length > 0) { const webhooks = edges[0].node; + expect(webhooks).toHaveProperty('id'); expect(webhooks).toHaveProperty('targetUrl'); expect(webhooks).toHaveProperty('operation'); expect(webhooks).toHaveProperty('description'); - expect(webhooks).toHaveProperty('id'); expect(webhooks).toHaveProperty('createdAt'); expect(webhooks).toHaveProperty('updatedAt'); expect(webhooks).toHaveProperty('deletedAt'); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/workflow-event-listeners.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/workflow-event-listeners.integration-spec.ts new file mode 100644 index 000000000000..6859b52abe29 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/workflow-event-listeners.integration-spec.ts @@ -0,0 +1,55 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('workflowEventListenersResolver (e2e)', () => { + it('should find many workflowEventListeners', () => { + const queryData = { + query: ` + query workflowEventListeners { + workflowEventListeners { + edges { + node { + eventName + id + createdAt + updatedAt + deletedAt + workflowId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.workflowEventListeners; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const workflowEventListeners = edges[0].node; + + expect(workflowEventListeners).toHaveProperty('eventName'); + expect(workflowEventListeners).toHaveProperty('id'); + expect(workflowEventListeners).toHaveProperty('createdAt'); + expect(workflowEventListeners).toHaveProperty('updatedAt'); + expect(workflowEventListeners).toHaveProperty('deletedAt'); + expect(workflowEventListeners).toHaveProperty('workflowId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/workflow-versions.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/workflow-versions.integration-spec.ts new file mode 100644 index 000000000000..cf3a7d113e8d --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/workflow-versions.integration-spec.ts @@ -0,0 +1,63 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('workflowVersionsResolver (e2e)', () => { + it('should find many workflowVersions', () => { + const queryData = { + query: ` + query workflowVersions { + workflowVersions { + edges { + node { + name + trigger + steps + status + position + id + createdAt + updatedAt + deletedAt + workflowId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.workflowVersions; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const workflowVersions = edges[0].node; + + expect(workflowVersions).toHaveProperty('name'); + expect(workflowVersions).toHaveProperty('trigger'); + expect(workflowVersions).toHaveProperty('steps'); + expect(workflowVersions).toHaveProperty('status'); + expect(workflowVersions).toHaveProperty('position'); + expect(workflowVersions).toHaveProperty('id'); + expect(workflowVersions).toHaveProperty('createdAt'); + expect(workflowVersions).toHaveProperty('updatedAt'); + expect(workflowVersions).toHaveProperty('deletedAt'); + expect(workflowVersions).toHaveProperty('workflowId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/workflows.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/workflows.integration-spec.ts new file mode 100644 index 000000000000..a1c0450f02bc --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/workflows.integration-spec.ts @@ -0,0 +1,59 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('workflowsResolver (e2e)', () => { + it('should find many workflows', () => { + const queryData = { + query: ` + query workflows { + workflows { + edges { + node { + name + lastPublishedVersionId + statuses + position + id + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.workflows; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const workflows = edges[0].node; + + expect(workflows).toHaveProperty('name'); + expect(workflows).toHaveProperty('lastPublishedVersionId'); + expect(workflows).toHaveProperty('statuses'); + expect(workflows).toHaveProperty('position'); + expect(workflows).toHaveProperty('id'); + expect(workflows).toHaveProperty('createdAt'); + expect(workflows).toHaveProperty('updatedAt'); + expect(workflows).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/workspace-members.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/workspace-members.integration-spec.ts similarity index 97% rename from packages/twenty-server/test/workspace-members.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/workspace-members.integration-spec.ts index 5ef7a415d886..63fd94d81baa 100644 --- a/packages/twenty-server/test/workspace-members.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/workspace-members.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('workspaceMembersResolver (integration)', () => { +describe('workspaceMembersResolver (e2e)', () => { it('should find many workspaceMembers', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/integration/graphql/utils/create-many-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/create-many-operation-factory.util.ts new file mode 100644 index 000000000000..70604c009399 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/create-many-operation-factory.util.ts @@ -0,0 +1,28 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type CreateManyOperationFactoryParams = { + objectMetadataSingularName: string; + objectMetadataPluralName: string; + gqlFields: string; + data?: object; +}; + +export const createManyOperationFactory = ({ + objectMetadataSingularName, + objectMetadataPluralName, + gqlFields, + data = {}, +}: CreateManyOperationFactoryParams) => ({ + query: gql` + mutation Create${capitalize(objectMetadataSingularName)}($data: [${capitalize(objectMetadataSingularName)}CreateInput!]!) { + create${capitalize(objectMetadataPluralName)}(data: $data) { + ${gqlFields} + } + } + `, + variables: { + data, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/create-one-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/create-one-operation-factory.util.ts new file mode 100644 index 000000000000..ed477b1a7792 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/create-one-operation-factory.util.ts @@ -0,0 +1,26 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type CreateOneOperationFactoryParams = { + objectMetadataSingularName: string; + gqlFields: string; + data?: object; +}; + +export const createOneOperationFactory = ({ + objectMetadataSingularName, + gqlFields, + data = {}, +}: CreateOneOperationFactoryParams) => ({ + query: gql` + mutation Create${capitalize(objectMetadataSingularName)}($data: ${capitalize(objectMetadataSingularName)}CreateInput) { + create${capitalize(objectMetadataSingularName)}(data: $data) { + ${gqlFields} + } + } + `, + variables: { + data, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/delete-many-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/delete-many-operation-factory.util.ts new file mode 100644 index 000000000000..2bfe3c158e7a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/delete-many-operation-factory.util.ts @@ -0,0 +1,30 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type DeleteManyOperationFactoryParams = { + objectMetadataSingularName: string; + objectMetadataPluralName: string; + gqlFields: string; + filter?: object; +}; + +export const deleteManyOperationFactory = ({ + objectMetadataSingularName, + objectMetadataPluralName, + gqlFields, + filter = {}, +}: DeleteManyOperationFactoryParams) => ({ + query: gql` + mutation Delete${capitalize(objectMetadataPluralName)}( + $filter: ${capitalize(objectMetadataSingularName)}FilterInput + ) { + delete${capitalize(objectMetadataPluralName)}(filter: $filter) { + ${gqlFields} + } + } + `, + variables: { + filter, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/delete-one-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/delete-one-operation-factory.util.ts new file mode 100644 index 000000000000..f3cfd765b25a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/delete-one-operation-factory.util.ts @@ -0,0 +1,26 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type DeleteOneOperationFactoryParams = { + objectMetadataSingularName: string; + gqlFields: string; + recordId: string; +}; + +export const deleteOneOperationFactory = ({ + objectMetadataSingularName, + gqlFields, + recordId, +}: DeleteOneOperationFactoryParams) => ({ + query: gql` + mutation Delete${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: ID!) { + delete${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id) { + ${gqlFields} + } + } + `, + variables: { + [`${objectMetadataSingularName}Id`]: recordId, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/destroy-many-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/destroy-many-operation-factory.util.ts new file mode 100644 index 000000000000..f664a4088268 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/destroy-many-operation-factory.util.ts @@ -0,0 +1,30 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type DestroyManyOperationFactoryParams = { + objectMetadataSingularName: string; + objectMetadataPluralName: string; + gqlFields: string; + filter?: object; +}; + +export const destroyManyOperationFactory = ({ + objectMetadataSingularName, + objectMetadataPluralName, + gqlFields, + filter = {}, +}: DestroyManyOperationFactoryParams) => ({ + query: gql` + mutation Destroy${capitalize(objectMetadataPluralName)}( + $filter: ${capitalize(objectMetadataSingularName)}FilterInput + ) { + destroy${capitalize(objectMetadataPluralName)}(filter: $filter) { + ${gqlFields} + } + } + `, + variables: { + filter, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/destroy-one-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/destroy-one-operation-factory.util.ts new file mode 100644 index 000000000000..4062e9319f5a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/destroy-one-operation-factory.util.ts @@ -0,0 +1,26 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type DestroyOneOperationFactoryParams = { + objectMetadataSingularName: string; + gqlFields: string; + recordId: string; +}; + +export const destroyOneOperationFactory = ({ + objectMetadataSingularName, + gqlFields, + recordId, +}: DestroyOneOperationFactoryParams) => ({ + query: gql` + mutation Destroy${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: ID!) { + destroy${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id) { + ${gqlFields} + } + } + `, + variables: { + [`${objectMetadataSingularName}Id`]: recordId, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/find-many-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/find-many-operation-factory.util.ts new file mode 100644 index 000000000000..752e9aca0c20 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/find-many-operation-factory.util.ts @@ -0,0 +1,32 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type FindManyOperationFactoryParams = { + objectMetadataSingularName: string; + objectMetadataPluralName: string; + gqlFields: string; + filter?: object; +}; + +export const findManyOperationFactory = ({ + objectMetadataSingularName, + objectMetadataPluralName, + gqlFields, + filter = {}, +}: FindManyOperationFactoryParams) => ({ + query: gql` + query ${capitalize(objectMetadataPluralName)}($filter: ${capitalize(objectMetadataSingularName)}FilterInput) { + ${objectMetadataPluralName}(filter: $filter) { + edges { + node { + ${gqlFields} + } + } + } + } + `, + variables: { + filter, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/find-one-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/find-one-operation-factory.util.ts new file mode 100644 index 000000000000..1a6972a841a9 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/find-one-operation-factory.util.ts @@ -0,0 +1,26 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type FindOneOperationFactoryParams = { + objectMetadataSingularName: string; + gqlFields: string; + filter?: object; +}; + +export const findOneOperationFactory = ({ + objectMetadataSingularName, + gqlFields, + filter = {}, +}: FindOneOperationFactoryParams) => ({ + query: gql` + query ${capitalize(objectMetadataSingularName)}($filter: ${capitalize(objectMetadataSingularName)}FilterInput) { + ${objectMetadataSingularName}(filter: $filter) { + ${gqlFields} + } + } + `, + variables: { + filter, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/make-graphql-api-request.util.ts b/packages/twenty-server/test/integration/graphql/utils/make-graphql-api-request.util.ts new file mode 100644 index 000000000000..21b3e889716c --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/make-graphql-api-request.util.ts @@ -0,0 +1,19 @@ +import { ASTNode, print } from 'graphql'; +import request from 'supertest'; + +type GraphqlOperation = { + query: ASTNode; + variables?: Record; +}; + +export const makeGraphqlAPIRequest = (graphqlOperation: GraphqlOperation) => { + const client = request(`http://localhost:${APP_PORT}`); + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send({ + query: print(graphqlOperation.query), + variables: graphqlOperation.variables || {}, + }); +}; diff --git a/packages/twenty-server/test/integration/graphql/utils/update-many-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/update-many-operation-factory.util.ts new file mode 100644 index 000000000000..688ae9199950 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/update-many-operation-factory.util.ts @@ -0,0 +1,34 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type UpdateManyOperationFactoryParams = { + objectMetadataSingularName: string; + objectMetadataPluralName: string; + gqlFields: string; + data?: object; + filter?: object; +}; + +export const updateManyOperationFactory = ({ + objectMetadataSingularName, + objectMetadataPluralName, + gqlFields, + data = {}, + filter = {}, +}: UpdateManyOperationFactoryParams) => ({ + query: gql` + mutation Update${capitalize(objectMetadataPluralName)}( + $data: ${capitalize(objectMetadataSingularName)}UpdateInput + $filter: ${capitalize(objectMetadataSingularName)}FilterInput + ) { + update${capitalize(objectMetadataPluralName)}(data: $data, filter: $filter) { + ${gqlFields} + } + } + `, + variables: { + data, + filter, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/update-one-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/update-one-operation-factory.util.ts new file mode 100644 index 000000000000..cf7827241292 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/update-one-operation-factory.util.ts @@ -0,0 +1,29 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type UpdateOneOperationFactoryParams = { + objectMetadataSingularName: string; + gqlFields: string; + data?: object; + recordId: string; +}; + +export const updateOneOperationFactory = ({ + objectMetadataSingularName, + gqlFields, + data = {}, + recordId, +}: UpdateOneOperationFactoryParams) => ({ + query: gql` + mutation Update${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: ID, $data: ${capitalize(objectMetadataSingularName)}UpdateInput) { + update${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id, data: $data) { + ${gqlFields} + } + } + `, + variables: { + data, + [`${objectMetadataSingularName}Id`]: recordId, + }, +}); diff --git a/packages/twenty-server/test/utils/create-app.ts b/packages/twenty-server/test/integration/utils/create-app.ts similarity index 100% rename from packages/twenty-server/test/utils/create-app.ts rename to packages/twenty-server/test/integration/utils/create-app.ts diff --git a/packages/twenty-server/test/integration/utils/generate-record-name.ts b/packages/twenty-server/test/integration/utils/generate-record-name.ts new file mode 100644 index 000000000000..123de9b6b17d --- /dev/null +++ b/packages/twenty-server/test/integration/utils/generate-record-name.ts @@ -0,0 +1,4 @@ +export const TEST_NAME_PREFIX = 'test_record_'; + +export const generateRecordName = (uuid: string) => + `${TEST_NAME_PREFIX}-${uuid}`; diff --git a/packages/twenty-server/test/utils/setup-test.ts b/packages/twenty-server/test/integration/utils/setup-test.ts similarity index 100% rename from packages/twenty-server/test/utils/setup-test.ts rename to packages/twenty-server/test/integration/utils/setup-test.ts diff --git a/packages/twenty-server/test/utils/teardown-test.ts b/packages/twenty-server/test/integration/utils/teardown-test.ts similarity index 100% rename from packages/twenty-server/test/utils/teardown-test.ts rename to packages/twenty-server/test/integration/utils/teardown-test.ts diff --git a/packages/twenty-ui/src/display/chip/components/Chip.tsx b/packages/twenty-ui/src/display/chip/components/Chip.tsx index 48795fd42551..3bf0cd9bb90d 100644 --- a/packages/twenty-ui/src/display/chip/components/Chip.tsx +++ b/packages/twenty-ui/src/display/chip/components/Chip.tsx @@ -66,7 +66,7 @@ const StyledContainer = withTheme(styled.div< display: inline-flex; justify-content: center; gap: ${({ theme }) => theme.spacing(1)}; - height: ${({ theme }) => theme.spacing(3)}; + height: ${({ theme }) => theme.spacing(4)}; max-width: ${({ maxWidth }) => maxWidth ? `calc(${maxWidth}px - 2 * var(--chip-horizontal-padding))` diff --git a/packages/twenty-website/src/app/_components/ui/layout/FooterDesktop.tsx b/packages/twenty-website/src/app/_components/ui/layout/FooterDesktop.tsx index 8045afd0043f..0a7c00551d28 100644 --- a/packages/twenty-website/src/app/_components/ui/layout/FooterDesktop.tsx +++ b/packages/twenty-website/src/app/_components/ui/layout/FooterDesktop.tsx @@ -92,12 +92,11 @@ export const FooterDesktop = () => { Developers - - Changelog - - User Guide + User-Guide + Releases + Jobs Other diff --git a/packages/twenty-website/src/content/developers/local-setup.mdx b/packages/twenty-website/src/content/developers/local-setup.mdx index 6bae7137c223..b66ea2ee45aa 100644 --- a/packages/twenty-website/src/content/developers/local-setup.mdx +++ b/packages/twenty-website/src/content/developers/local-setup.mdx @@ -93,49 +93,49 @@ cd twenty You should run all commands in the following steps from the root of the project. ## Step 3: Set up a PostgreSQL Database -We rely on [pg_graphql](https://github.com/supabase/pg_graphql) and recommend you use the scripts below to provision a database with the right extensions. You can access the database at [localhost:5432](localhost:5432), with user `twenty` and password `twenty` . Option 1: To provision your database locally: + Use the following link to install Postgresql on your Linux machine: [Postgresql Installation](https://www.postgresql.org/download/linux/) ```bash - make postgres-on-linux + psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;" -c "CREATE USER twenty PASSWORD 'twenty';" -c "ALTER ROLE twenty superuser;" ``` Option 2: If you have docker installed: ```bash - make postgres-on-docker + make postgres-on-docker ``` Option 1: To provision your database locally with `brew`: - ```bash - make postgres-on-macos-intel #for intel architecture - make postgres-on-macos-arm #for M1/M2/M3 architecture + ```bash + brew install postgresql@16 + export PATH="/opt/homebrew/opt/postgresql@16/bin:$PATH" + psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;" -c "CREATE USER twenty PASSWORD 'twenty';" -c "ALTER ROLE twenty superuser;" ``` Option 2: If you have docker installed: ```bash - make postgres-on-docker + make postgres-on-docker ``` All the following steps are to be run in the WSL terminal (within your virtual machine) - Option 1: To provision your database locally: + Option 1: To provision your Postgresql locally: + Use the following link to install Postgresql on your Linux virtual machine: [Postgresql Installation](https://www.postgresql.org/download/linux/) ```bash - make postgres-on-linux + psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;" -c "CREATE USER twenty PASSWORD 'twenty';" -c "ALTER ROLE twenty superuser;" ``` - Note: you might need to run `sudo apt-get install build-essential` before running the above command if you don't have the build tools installed. - Option 2: If you have docker installed: Running Docker on WSL adds an extra layer of complexity. Only use this option if you are comfortable with the extra steps involved, including turning on [Docker Desktop WSL2](https://docs.docker.com/desktop/wsl). ```bash - make postgres-on-docker + make postgres-on-docker ``` @@ -150,18 +150,18 @@ Twenty requires a redis cache to provide the best performances Option 2: If you have docker installed: ```bash - docker run -d --name my-redis-stack -p 6379:6379 redis/redis-stack-server:latest + make redis-on-docker ``` - Option 1:To provision your Redis locally with `brew`: + Option 1: To provision your Redis locally with `brew`: ```bash brew install redis ``` Option 2: If you have docker installed: ```bash - docker run -d --name my-redis-stack -p 6379:6379 redis/redis-stack-server:latest + make redis-on-docker ``` @@ -170,7 +170,7 @@ Twenty requires a redis cache to provide the best performances Option 2: If you have docker installed: ```bash - docker run -d --name my-redis-stack -p 6379:6379 redis/redis-stack-server:latest + make redis-on-docker ``` @@ -225,13 +225,14 @@ Setup your database with the following command: npx nx database:reset twenty-server ``` -Start the server and the frontend: +Start the server, the worker and the frontend services: ```bash npx nx start twenty-server +npx nx worker twenty-server npx nx start twenty-front ``` -Alternatively, you can start both applications at once: +Alternatively, you can start all services at once: ```bash npx nx start ``` diff --git a/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx b/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx index cda5fb3a3d1c..d679096f80a7 100644 --- a/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx +++ b/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx @@ -34,10 +34,7 @@ yarn command:prod cron:calendar:calendar-event-list-fetch diff --git a/packages/twenty-website/src/database/migrations/0003_polite_lorna_dane.sql b/packages/twenty-website/src/database/migrations/0003_polite_lorna_dane.sql new file mode 100644 index 000000000000..c4b1eceb6a1c --- /dev/null +++ b/packages/twenty-website/src/database/migrations/0003_polite_lorna_dane.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS "githubReleases" ( + "tagName" text PRIMARY KEY NOT NULL, + "publishedAt" date NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "githubStars" ( + "timestamp" timestamp DEFAULT now() NOT NULL, + "numberOfStars" integer +); diff --git a/packages/twenty-website/src/database/migrations/meta/0003_snapshot.json b/packages/twenty-website/src/database/migrations/meta/0003_snapshot.json new file mode 100644 index 000000000000..e8ba9df971dc --- /dev/null +++ b/packages/twenty-website/src/database/migrations/meta/0003_snapshot.json @@ -0,0 +1,388 @@ +{ + "id": "b3a89784-eb82-49d8-b081-31c49e6906dc", + "prevId": "a7895a79-44a3-4fad-b750-f89d8c04d85c", + "version": "5", + "dialect": "pg", + "tables": { + "githubReleases": { + "name": "githubReleases", + "schema": "", + "columns": { + "tagName": { + "name": "tagName", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "publishedAt": { + "name": "publishedAt", + "type": "date", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "githubStars": { + "name": "githubStars", + "schema": "", + "columns": { + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "numberOfStars": { + "name": "numberOfStars", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "issueLabels": { + "name": "issueLabels", + "schema": "", + "columns": { + "issueId": { + "name": "issueId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "labelId": { + "name": "labelId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "issueLabels_issueId_issues_id_fk": { + "name": "issueLabels_issueId_issues_id_fk", + "tableFrom": "issueLabels", + "tableTo": "issues", + "columnsFrom": [ + "issueId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issueLabels_labelId_labels_id_fk": { + "name": "issueLabels_labelId_labels_id_fk", + "tableFrom": "issueLabels", + "tableTo": "labels", + "columnsFrom": [ + "labelId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "issues": { + "name": "issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "externalId": { + "name": "externalId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "closedAt": { + "name": "closedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorId": { + "name": "authorId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "issues_authorId_users_id_fk": { + "name": "issues_authorId_users_id_fk", + "tableFrom": "issues", + "tableTo": "users", + "columnsFrom": [ + "authorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "externalId": { + "name": "externalId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "pullRequestLabels": { + "name": "pullRequestLabels", + "schema": "", + "columns": { + "pullRequestExternalId": { + "name": "pullRequestExternalId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "labelId": { + "name": "labelId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "pullRequestLabels_pullRequestExternalId_pullRequests_id_fk": { + "name": "pullRequestLabels_pullRequestExternalId_pullRequests_id_fk", + "tableFrom": "pullRequestLabels", + "tableTo": "pullRequests", + "columnsFrom": [ + "pullRequestExternalId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pullRequestLabels_labelId_labels_id_fk": { + "name": "pullRequestLabels_labelId_labels_id_fk", + "tableFrom": "pullRequestLabels", + "tableTo": "labels", + "columnsFrom": [ + "labelId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "pullRequests": { + "name": "pullRequests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "closedAt": { + "name": "closedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mergedAt": { + "name": "mergedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorId": { + "name": "authorId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "pullRequests_authorId_users_id_fk": { + "name": "pullRequests_authorId_users_id_fk", + "tableFrom": "pullRequests", + "tableTo": "users", + "columnsFrom": [ + "authorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "avatarUrl": { + "name": "avatarUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isEmployee": { + "name": "isEmployee", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index d3400e4a26b3..9d8eabefb6c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15073,6 +15073,17 @@ __metadata: languageName: node linkType: hard +"@ts-morph/common@npm:~0.25.0": + version: 0.25.0 + resolution: "@ts-morph/common@npm:0.25.0" + dependencies: + minimatch: "npm:^9.0.4" + path-browserify: "npm:^1.0.1" + tinyglobby: "npm:^0.2.9" + checksum: 10c0/c67e66db678e44886e9823e6482834acebfae0ea52ccbfa2af1ca9abfe5a9774dad6e852c8f480909bc196175f17e15454af71d7a41a1c137db09e74f046a830 + languageName: node + linkType: hard + "@tsconfig/node10@npm:^1.0.7": version: 1.0.11 resolution: "@tsconfig/node10@npm:1.0.11" @@ -22021,6 +22032,13 @@ __metadata: languageName: node linkType: hard +"code-block-writer@npm:^13.0.3": + version: 13.0.3 + resolution: "code-block-writer@npm:13.0.3" + checksum: 10c0/87db97b37583f71cfd7eced8bf3f0a0a0ca53af912751a734372b36c08cd27f3e8a4878ec05591c0cd9ae11bea8add1423e132d660edd86aab952656dd41fd66 + languageName: node + linkType: hard + "code-point-at@npm:^1.0.0": version: 1.1.0 resolution: "code-point-at@npm:1.1.0" @@ -26491,6 +26509,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.4.0": + version: 6.4.0 + resolution: "fdir@npm:6.4.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/9a03efa1335d78ea386b701799b08ad9e7e8da85d88567dc162cd28dd8e9486e8c269b3e95bfeb21dd6a5b14ebf69d230eb6e18f49d33fbda3cd97432f648c48 + languageName: node + linkType: hard + "fetch-retry@npm:^5.0.2": version: 5.0.6 resolution: "fetch-retry@npm:5.0.6" @@ -43048,6 +43078,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.9": + version: 0.2.9 + resolution: "tinyglobby@npm:0.2.9" + dependencies: + fdir: "npm:^6.4.0" + picomatch: "npm:^4.0.2" + checksum: 10c0/f65f847afe70f56de069d4f1f9c3b0c1a76aaf2b0297656754734a83b9bac8e105b5534dfbea8599560476b88f7b747d0855370a957a07246d18b976addb87ec + languageName: node + linkType: hard + "tinypool@npm:^0.8.2": version: 0.8.4 resolution: "tinypool@npm:0.8.4" @@ -43474,6 +43514,16 @@ __metadata: languageName: node linkType: hard +"ts-morph@npm:^24.0.0": + version: 24.0.0 + resolution: "ts-morph@npm:24.0.0" + dependencies: + "@ts-morph/common": "npm:~0.25.0" + code-block-writer: "npm:^13.0.3" + checksum: 10c0/2a0813ba428a154966d4038901f6c32457a60870936b23778f2629433257f87d1881fc4ecae7b791a223a88c2edf96aaac9fb0f88bf34d3c652af8c09c4f43bc + languageName: node + linkType: hard + "ts-node@npm:10.9.1": version: 10.9.1 resolution: "ts-node@npm:10.9.1" @@ -43817,6 +43867,7 @@ __metadata: passport: "npm:^0.7.0" psl: "npm:^1.9.0" rimraf: "npm:^5.0.5" + ts-morph: "npm:^24.0.0" tsconfig-paths: "npm:^4.2.0" typeorm: "patch:typeorm@0.3.20#./patches/typeorm+0.3.20.patch" typescript: "npm:5.3.3"