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 b61f187117b5..d4f5a4d0ab99 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 @@ -22,4 +22,5 @@ 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) --- diff --git a/oss-gg/twenty-side-quest/5-gif-magic.md b/oss-gg/twenty-side-quest/5-gif-magic.md index 20467fef4784..f683bf3eb21c 100644 --- a/oss-gg/twenty-side-quest/5-gif-magic.md +++ b/oss-gg/twenty-side-quest/5-gif-magic.md @@ -29,5 +29,6 @@ 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 --- 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-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 97705b937cba..6947f8aa34a4 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -28,13 +28,6 @@ export type Scalars = { Upload: { input: any; output: any; } }; -export type AisqlQueryResult = { - __typename?: 'AISQLQueryResult'; - queryFailedErrorMessage?: Maybe; - sqlQuery: Scalars['String']['output']; - sqlQueryResult?: Maybe; -}; - export type ActivateWorkspaceInput = { displayName?: InputMaybe; }; @@ -862,7 +855,6 @@ export type Query = { findOneRemoteServerById: RemoteServer; findWorkspaceFromInviteHash: Workspace; findWorkspaceInvitations: Array; - getAISQLQuery: AisqlQueryResult; getAvailablePackages: Scalars['JSON']['output']; getPostgresCredentials?: Maybe; getProductPrices: ProductPricesEntity; @@ -930,11 +922,6 @@ export type QueryFindWorkspaceFromInviteHashArgs = { }; -export type QueryGetAisqlQueryArgs = { - text: Scalars['String']['input']; -}; - - export type QueryGetProductPricesArgs = { product: Scalars['String']['input']; }; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 9e930133b3fb..5950c81d5cc1 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1,5 +1,5 @@ -import * as Apollo from '@apollo/client'; import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -21,13 +21,6 @@ export type Scalars = { Upload: any; }; -export type AisqlQueryResult = { - __typename?: 'AISQLQueryResult'; - queryFailedErrorMessage?: Maybe; - sqlQuery: Scalars['String']; - sqlQueryResult?: Maybe; -}; - export type ActivateWorkspaceInput = { displayName?: InputMaybe; }; @@ -160,13 +153,7 @@ export type ClientConfig = { support: Support; }; -export type CreateServerlessFunctionFromFileInput = { - description?: InputMaybe; - name: Scalars['String']; -}; - export type CreateServerlessFunctionInput = { - code: Scalars['String']; description?: InputMaybe; name: Scalars['String']; }; @@ -302,6 +289,36 @@ export type GetServerlessFunctionSourceCodeInput = { version?: Scalars['String']; }; +export type IndexConnection = { + __typename?: 'IndexConnection'; + /** Array of edges. */ + edges: Array; + /** Paging information */ + pageInfo: PageInfo; +}; + +export type IndexIndexFieldMetadatasConnection = { + __typename?: 'IndexIndexFieldMetadatasConnection'; + /** Array of edges. */ + edges: Array; + /** Paging information */ + pageInfo: PageInfo; +}; + +export type IndexObjectMetadataConnection = { + __typename?: 'IndexObjectMetadataConnection'; + /** Array of edges. */ + edges: Array; + /** Paging information */ + pageInfo: PageInfo; +}; + +/** Type of the index */ +export enum IndexType { + Btree = 'BTREE', + Gin = 'GIN' +} + export type InvalidatePassword = { __typename?: 'InvalidatePassword'; /** Boolean that confirms query was dispatched */ @@ -344,7 +361,6 @@ export type Mutation = { createOneAppToken: AppToken; createOneObject: Object; createOneServerlessFunction: ServerlessFunction; - createOneServerlessFunctionFromFile: ServerlessFunction; deactivateWorkflowVersion: Scalars['Boolean']; deleteCurrentWorkspace: Workspace; deleteOneObject: Object; @@ -426,12 +442,6 @@ export type MutationCreateOneServerlessFunctionArgs = { }; -export type MutationCreateOneServerlessFunctionFromFileArgs = { - file: Scalars['Upload']; - input: CreateServerlessFunctionFromFileInput; -}; - - export type MutationDeactivateWorkflowVersionArgs = { workflowVersionId: Scalars['String']; }; @@ -520,9 +530,8 @@ export type MutationSignUpArgs = { export type MutationTrackArgs = { - data: Scalars['JSON']; - sessionId: Scalars['String']; - type: Scalars['String']; + action: Scalars['String']; + payload: Scalars['JSON']; }; @@ -589,6 +598,14 @@ export type ObjectFieldsConnection = { pageInfo: PageInfo; }; +export type ObjectIndexMetadatasConnection = { + __typename?: 'ObjectIndexMetadatasConnection'; + /** Array of edges. */ + edges: Array; + /** Paging information */ + pageInfo: PageInfo; +}; + /** Onboarding status */ export enum OnboardingStatus { Completed = 'COMPLETED', @@ -654,15 +671,16 @@ export type Query = { currentWorkspace: Workspace; findWorkspaceFromInviteHash: Workspace; findWorkspaceInvitations: Array; - getAISQLQuery: AisqlQueryResult; getAvailablePackages: Scalars['JSON']; getPostgresCredentials?: Maybe; getProductPrices: ProductPricesEntity; - getServerlessFunctionSourceCode?: Maybe; + getServerlessFunctionSourceCode?: Maybe; getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal; getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal; getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal; getTimelineThreadsFromPersonId: TimelineThreadsWithTotal; + index: Index; + indexMetadatas: IndexConnection; object: Object; objects: ObjectConnection; serverlessFunction: ServerlessFunction; @@ -692,11 +710,6 @@ export type QueryFindWorkspaceFromInviteHashArgs = { }; -export type QueryGetAisqlQueryArgs = { - text: Scalars['String']; -}; - - export type QueryGetProductPricesArgs = { product: Scalars['String']; }; @@ -831,7 +844,6 @@ export type ServerlessFunction = { latestVersion?: Maybe; name: Scalars['String']; runtime: Scalars['String']; - sourceCodeHash: Scalars['String']; syncStatus: ServerlessFunctionSyncStatus; updatedAt: Scalars['DateTime']; }; @@ -1028,7 +1040,7 @@ export type UpdateOneObjectInput = { }; export type UpdateServerlessFunctionInput = { - code: Scalars['String']; + code: Scalars['JSON']; description?: InputMaybe; /** Id of the serverless function to execute */ id: Scalars['UUID']; @@ -1213,6 +1225,7 @@ export type Field = { isCustom?: Maybe; isNullable?: Maybe; isSystem?: Maybe; + isUnique?: Maybe; label: Scalars['String']; name: Scalars['String']; object?: Maybe; @@ -1241,6 +1254,71 @@ export type FieldFilter = { or?: InputMaybe>; }; +export type Index = { + __typename?: 'index'; + createdAt: Scalars['DateTime']; + id: Scalars['UUID']; + indexFieldMetadatas: IndexIndexFieldMetadatasConnection; + indexType: IndexType; + indexWhereClause?: Maybe; + isCustom?: Maybe; + isUnique: Scalars['Boolean']; + name: Scalars['String']; + objectMetadata: IndexObjectMetadataConnection; + updatedAt: Scalars['DateTime']; +}; + + +export type IndexIndexFieldMetadatasArgs = { + filter?: IndexFieldFilter; + paging?: CursorPaging; +}; + + +export type IndexObjectMetadataArgs = { + filter?: ObjectFilter; + paging?: CursorPaging; +}; + +export type IndexEdge = { + __typename?: 'indexEdge'; + /** Cursor for this node. */ + cursor: Scalars['ConnectionCursor']; + /** The node containing the index */ + node: Index; +}; + +export type IndexField = { + __typename?: 'indexField'; + createdAt: Scalars['DateTime']; + fieldMetadataId: Scalars['UUID']; + id: Scalars['UUID']; + order: Scalars['Float']; + updatedAt: Scalars['DateTime']; +}; + +export type IndexFieldEdge = { + __typename?: 'indexFieldEdge'; + /** Cursor for this node. */ + cursor: Scalars['ConnectionCursor']; + /** The node containing the indexField */ + node: IndexField; +}; + +export type IndexFieldFilter = { + and?: InputMaybe>; + fieldMetadataId?: InputMaybe; + id?: InputMaybe; + or?: InputMaybe>; +}; + +export type IndexFilter = { + and?: InputMaybe>; + id?: InputMaybe; + isCustom?: InputMaybe; + or?: InputMaybe>; +}; + export type Object = { __typename?: 'object'; createdAt: Scalars['DateTime']; @@ -1250,6 +1328,7 @@ export type Object = { icon?: Maybe; id: Scalars['UUID']; imageIdentifierFieldMetadataId?: Maybe; + indexMetadatas: ObjectIndexMetadatasConnection; isActive: Scalars['Boolean']; isCustom: Scalars['Boolean']; isRemote: Scalars['Boolean']; @@ -1268,6 +1347,12 @@ export type ObjectFieldsArgs = { paging?: CursorPaging; }; + +export type ObjectIndexMetadatasArgs = { + filter?: IndexFilter; + paging?: CursorPaging; +}; + export type ObjectEdge = { __typename?: 'objectEdge'; /** Cursor for this node. */ @@ -1276,6 +1361,16 @@ export type ObjectEdge = { node: Object; }; +export type ObjectFilter = { + and?: InputMaybe>; + id?: InputMaybe; + isActive?: InputMaybe; + isCustom?: InputMaybe; + isRemote?: InputMaybe; + isSystem?: InputMaybe; + or?: InputMaybe>; +}; + export type Relation = { __typename?: 'relation'; createdAt: Scalars['DateTime']; @@ -1511,13 +1606,6 @@ export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string] export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } }; -export type GetAisqlQueryQueryVariables = Exact<{ - text: Scalars['String']; -}>; - - -export type GetAisqlQueryQuery = { __typename?: 'Query', getAISQLQuery: { __typename?: 'AISQLQueryResult', sqlQuery: string, sqlQueryResult?: string | null, queryFailedErrorMessage?: string | null } }; - export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -1962,7 +2050,7 @@ export type TrackMutationFn = Apollo.MutationFunction; export type SkipSyncEmailOnboardingStepMutationResult = Apollo.MutationResult; export type SkipSyncEmailOnboardingStepMutationOptions = Apollo.BaseMutationOptions; -export const GetAisqlQueryDocument = gql` - query GetAISQLQuery($text: String!) { - getAISQLQuery(text: $text) { - sqlQuery - sqlQueryResult - queryFailedErrorMessage - } -} - `; - -/** - * __useGetAisqlQueryQuery__ - * - * To run a query within a React component, call `useGetAisqlQueryQuery` and pass it any options that fit your needs. - * When your component renders, `useGetAisqlQueryQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useGetAisqlQueryQuery({ - * variables: { - * text: // value for 'text' - * }, - * }); - */ -export function useGetAisqlQueryQuery(baseOptions: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(GetAisqlQueryDocument, options); - } -export function useGetAisqlQueryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(GetAisqlQueryDocument, options); - } -export type GetAisqlQueryQueryHookResult = ReturnType; -export type GetAisqlQueryLazyQueryHookResult = ReturnType; -export type GetAisqlQueryQueryResult = Apollo.QueryResult; export const DeleteUserAccountDocument = gql` mutation DeleteUserAccount { deleteUser { diff --git a/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx b/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx index c8de2f64c46a..b3ea1eda955e 100644 --- a/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx +++ b/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx @@ -4,7 +4,7 @@ import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; import { ANIMATION, BACKGROUND_LIGHT, GRAY_SCALE } from 'twenty-ui'; import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; -import { DESKTOP_NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/DesktopNavDrawerWidths'; +import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { MainNavigationDrawerItemsSkeletonLoader } from '~/loading/components/MainNavigationDrawerItemsSkeletonLoader'; @@ -47,14 +47,14 @@ const StyledSkeletonTitleContainer = styled.div` export const LeftPanelSkeletonLoader = () => { const isMobile = useIsMobile(); - const mobileWidth = isMobile ? 0 : '100%'; - const desktopWidth = !mobileWidth ? 12 : DESKTOP_NAV_DRAWER_WIDTHS.menu; return ( { await client.mutate({ mutation: gql` - mutation Track($type: String!, $sessionId: String!, $data: JSON!) { - track(type: $type, sessionId: $sessionId, data: $data) { + mutation Track($action: String!, $payload: JSON!) { + track(action: $action, payload: $payload) { success } } diff --git a/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavorites.tsx b/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavorites.tsx index c78cdd05c6fb..8d536f282cbc 100644 --- a/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavorites.tsx +++ b/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavorites.tsx @@ -13,6 +13,8 @@ import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/us import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { useFavorites } from '../hooks/useFavorites'; +import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper'; +import { NavigationDrawerItemsCollapsedContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer'; const StyledContainer = styled(NavigationDrawerSection)` width: 100%; @@ -39,7 +41,6 @@ export const CurrentWorkspaceMemberFavorites = () => { const { favorites, handleReorderFavorite } = useFavorites(); const loading = useIsPrefetchLoading(); - const { toggleNavigationSection, isNavigationSectionOpenState } = useNavigationSection('Favorites'); const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState); @@ -58,54 +59,65 @@ export const CurrentWorkspaceMemberFavorites = () => { ) return <>; - return ( - - toggleNavigationSection()} - /> - {isNavigationSectionOpen && ( - - {currentWorkspaceMemberFavorites.map((favorite, index) => { - const { - id, - labelIdentifier, - avatarUrl, - avatarType, - link, - recordId, - } = favorite; - - return ( - 1; + + const draggableListContent = ( + + {currentWorkspaceMemberFavorites.map((favorite, index) => { + const { + id, + labelIdentifier, + avatarUrl, + avatarType, + link, + recordId, + } = favorite; + + return ( + ( - - )} - to={link} + label={labelIdentifier} + Icon={() => ( + - } + )} + to={link} /> - ); - })} - - } + } + /> + ); + })} + + } + /> + ); + + return ( + + + toggleNavigationSection()} /> + + + {isNavigationSectionOpen && ( + + {draggableListContent} + )} ); diff --git a/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx b/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx index a6d3b1045cc9..258473603420 100644 --- a/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx +++ b/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx @@ -8,14 +8,14 @@ import { NavigationDrawer, NavigationDrawerProps, } from '@/ui/navigation/navigation-drawer/components/NavigationDrawer'; -import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState'; + import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI'; -import { useIsSettingsPage } from '../hooks/useIsSettingsPage'; -import { currentMobileNavigationDrawerState } from '../states/currentMobileNavigationDrawerState'; +import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer'; import { AdvancedSettingsToggle } from '@/ui/navigation/link/components/AdvancedSettingsToggle'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; import { MainNavigationDrawerItems } from './MainNavigationDrawerItems'; export type AppNavigationDrawerProps = { @@ -26,22 +26,14 @@ export const AppNavigationDrawer = ({ className, }: AppNavigationDrawerProps) => { const isMobile = useIsMobile(); - const isSettingsPage = useIsSettingsPage(); - const currentMobileNavigationDrawer = useRecoilValue( - currentMobileNavigationDrawerState, - ); - const setIsNavigationDrawerOpen = useSetRecoilState( - isNavigationDrawerOpenState, + const isSettingsDrawer = useIsSettingsDrawer(); + const setIsNavigationDrawerExpanded = useSetRecoilState( + isNavigationDrawerExpandedState, ); const currentWorkspace = useRecoilValue(currentWorkspaceState); - const isSettingsDrawer = isMobile - ? currentMobileNavigationDrawer === 'settings' - : isSettingsPage; - const drawerProps: NavigationDrawerProps = isSettingsDrawer ? { - isSubMenu: true, title: 'Exit Settings', children: , footer: , @@ -57,13 +49,12 @@ export const AppNavigationDrawer = ({ }; useEffect(() => { - setIsNavigationDrawerOpen(!isMobile); - }, [isMobile, setIsNavigationDrawerOpen]); + setIsNavigationDrawerExpanded(!isMobile); + }, [isMobile, setIsNavigationDrawerExpanded]); return ( { const isWorkspaceFavoriteEnabled = useIsFeatureEnabled( 'IS_WORKSPACE_FAVORITE_ENABLED', ); + const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] = + useRecoilState(isNavigationDrawerExpandedState); + const setNavigationDrawerExpandedMemorized = useSetRecoilState( + navigationDrawerExpandedMemorizedState, + ); return ( <> @@ -38,6 +45,8 @@ export const MainNavigationDrawerItems = () => { label="Settings" to={'/settings/profile'} onClick={() => { + setNavigationDrawerExpandedMemorized(isNavigationDrawerExpanded); + setIsNavigationDrawerExpanded(true); setNavigationMemorizedUrl(location.pathname + location.search); }} Icon={IconSettings} diff --git a/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx b/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx index c22887b74467..e64a8deda1d7 100644 --- a/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx @@ -1,11 +1,9 @@ -import { useRecoilState } from 'recoil'; -import { IconComponent, IconList, IconSearch, IconSettings } from 'twenty-ui'; - import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { NavigationBar } from '@/ui/navigation/navigation-bar/components/NavigationBar'; -import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState'; - +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; +import { useRecoilState } from 'recoil'; +import { IconComponent, IconList, IconSearch, IconSettings } from 'twenty-ui'; import { useIsSettingsPage } from '../hooks/useIsSettingsPage'; import { currentMobileNavigationDrawerState } from '../states/currentMobileNavigationDrawerState'; @@ -15,13 +13,12 @@ export const MobileNavigationBar = () => { const [isCommandMenuOpened] = useRecoilState(isCommandMenuOpenedState); const { closeCommandMenu, openCommandMenu } = useCommandMenu(); const isSettingsPage = useIsSettingsPage(); - const [isNavigationDrawerOpen, setIsNavigationDrawerOpen] = useRecoilState( - isNavigationDrawerOpenState, - ); + const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] = + useRecoilState(isNavigationDrawerExpandedState); const [currentMobileNavigationDrawer, setCurrentMobileNavigationDrawer] = useRecoilState(currentMobileNavigationDrawerState); - const activeItemName = isNavigationDrawerOpen + const activeItemName = isNavigationDrawerExpanded ? currentMobileNavigationDrawer : isCommandMenuOpened ? 'search' @@ -39,7 +36,7 @@ export const MobileNavigationBar = () => { Icon: IconList, onClick: () => { closeCommandMenu(); - setIsNavigationDrawerOpen( + setIsNavigationDrawerExpanded( (previousIsOpen) => activeItemName !== 'main' || !previousIsOpen, ); setCurrentMobileNavigationDrawer('main'); @@ -52,7 +49,7 @@ export const MobileNavigationBar = () => { if (!isCommandMenuOpened) { openCommandMenu(); } - setIsNavigationDrawerOpen(false); + setIsNavigationDrawerExpanded(false); }, }, { @@ -60,7 +57,7 @@ export const MobileNavigationBar = () => { Icon: IconSettings, onClick: () => { closeCommandMenu(); - setIsNavigationDrawerOpen( + setIsNavigationDrawerExpanded( (previousIsOpen) => activeItemName !== 'settings' || !previousIsOpen, ); setCurrentMobileNavigationDrawer('settings'); diff --git a/packages/twenty-front/src/modules/navigation/components/__stories__/AppNavigationDrawer.stories.tsx b/packages/twenty-front/src/modules/navigation/components/__stories__/AppNavigationDrawer.stories.tsx index 53acfde42242..3bb6f1acaf13 100644 --- a/packages/twenty-front/src/modules/navigation/components/__stories__/AppNavigationDrawer.stories.tsx +++ b/packages/twenty-front/src/modules/navigation/components/__stories__/AppNavigationDrawer.stories.tsx @@ -1,16 +1,17 @@ +import { Meta, StoryObj } from '@storybook/react'; import { useEffect } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { Meta, StoryObj } from '@storybook/react'; import { useSetRecoilState } from 'recoil'; import { currentMobileNavigationDrawerState } from '@/navigation/states/currentMobileNavigationDrawerState'; import { AppPath } from '@/types/AppPath'; -import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState'; + import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; import { AppNavigationDrawer, AppNavigationDrawerProps, @@ -22,8 +23,8 @@ const MobileNavigationDrawerStateSetterEffect = ({ mobileNavigationDrawer?: 'main' | 'settings'; }) => { const isMobile = useIsMobile(); - const setIsNavigationDrawerOpen = useSetRecoilState( - isNavigationDrawerOpenState, + const setIsNavigationDrawerExpanded = useSetRecoilState( + isNavigationDrawerExpandedState, ); const setCurrentMobileNavigationDrawer = useSetRecoilState( currentMobileNavigationDrawerState, @@ -32,13 +33,13 @@ const MobileNavigationDrawerStateSetterEffect = ({ useEffect(() => { if (!isMobile) return; - setIsNavigationDrawerOpen(true); + setIsNavigationDrawerExpanded(true); setCurrentMobileNavigationDrawer(mobileNavigationDrawer); }, [ isMobile, mobileNavigationDrawer, setCurrentMobileNavigationDrawer, - setIsNavigationDrawerOpen, + setIsNavigationDrawerExpanded, ]); return null; diff --git a/packages/twenty-front/src/modules/navigation/hooks/useIsSettingsDrawer.ts b/packages/twenty-front/src/modules/navigation/hooks/useIsSettingsDrawer.ts new file mode 100644 index 000000000000..1ee9d50614e2 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/hooks/useIsSettingsDrawer.ts @@ -0,0 +1,15 @@ +import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage'; +import { useRecoilValue } from 'recoil'; +import { currentMobileNavigationDrawerState } from '@/navigation/states/currentMobileNavigationDrawerState'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; + +export const useIsSettingsDrawer = () => { + const isMobile = useIsMobile(); + const isSettingsPage = useIsSettingsPage(); + const currentMobileNavigationDrawer = useRecoilValue( + currentMobileNavigationDrawerState, + ); + return isMobile + ? currentMobileNavigationDrawer === 'settings' + : isSettingsPage; +}; 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 b4cbd4e899e9..5e666e0cf542 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,18 @@ -import { useLocation } from 'react-router-dom'; -import { useRecoilValue } from 'recoil'; -import { useIcons } from 'twenty-ui'; - import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; 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 { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; +import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper'; import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem'; 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 { 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', @@ -38,107 +38,105 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({ const { getIcon } = useIcons(); const currentPath = useLocation().pathname; - const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView(); - // TODO: refactor this by splitting into separate components - return ( - objectMetadataItems.length > 0 && ( - - toggleNavigationSection()} - /> - - {isNavigationSectionOpen && - [ - ...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 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, ); - const lastVisitedViewId = - getLastVisitedViewIdFromObjectMetadataItemId( - objectMetadataItem.id, - ); - const viewId = lastVisitedViewId ?? objectMetadataViews[0]?.id; + } + 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 navigationPath = `/objects/${objectMetadataItem.namePlural}${ + viewId ? `?view=${viewId}` : '' + }`; - const shouldSubItemsBeDisplayed = - currentPath === `/objects/${objectMetadataItem.namePlural}` && - objectMetadataViews.length > 1; + 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 sortedObjectMetadataViews = [...objectMetadataViews].sort( + (viewA, viewB) => + viewA.key === 'INDEX' ? -1 : viewA.position - viewB.position, + ); - const selectedSubItemIndex = sortedObjectMetadataViews.findIndex( - (view) => viewId === view.id, - ); + const selectedSubItemIndex = sortedObjectMetadataViews.findIndex( + (view) => viewId === view.id, + ); - const subItemArrayLength = sortedObjectMetadataViews.length; + const subItemArrayLength = sortedObjectMetadataViews.length; - return ( -
- - {shouldSubItemsBeDisplayed && - sortedObjectMetadataViews.map((view, index) => ( - - ))} -
- ); - })} + return ( + + + {shouldSubItemsBeDisplayed && + sortedObjectMetadataViews.map((view, index) => ( + + ))} + + ); + }); + }; + + return ( + objectMetadataItems.length > 0 && ( + + + toggleNavigationSection()} + /> + + {isNavigationSectionOpen && renderObjectMetadataItems()} ) ); 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/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-index/components/RecordIndexBoardContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx index b34fc8b7ac98..73c2f372e126 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx @@ -46,9 +46,10 @@ export const RecordIndexBoardContainer = ({ createOneRecord, updateOneRecord, deleteOneRecord, + recordBoardId, }} > - + ); }; 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 a7e2854acda8..d6afaa0eaecf 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'; @@ -41,12 +39,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 = () => { @@ -54,17 +53,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,75 +114,59 @@ 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, - ), - ); - setContextStoreTargetedRecordsFilters( - 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), + ); + setContextStoreTargetedRecordsFilters( + 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, @@ -29,7 +26,7 @@ export const RecordIndexTableContainerEffect = ({ hasUserSelectedAllRowsState, unselectedRowIdsSelector, } = useRecordTable({ - recordTableId, + recordTableId: recordIndexId, }); const setcontextStoreTargetedRecords = 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-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/search/queries/getTextToSQL.ts b/packages/twenty-front/src/modules/search/queries/getTextToSQL.ts deleted file mode 100644 index 6758209824b6..000000000000 --- a/packages/twenty-front/src/modules/search/queries/getTextToSQL.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { gql } from '@apollo/client'; - -export const getCopilot = gql` - query GetAISQLQuery($text: String!) { - getAISQLQuery(text: $text) { - sqlQuery - sqlQueryResult - queryFailedErrorMessage - } - } -`; diff --git a/packages/twenty-front/src/modules/ui/layout/page/DefaultLayout.tsx b/packages/twenty-front/src/modules/ui/layout/page/DefaultLayout.tsx index 0a05da36515e..0f11f9d50e39 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/DefaultLayout.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/DefaultLayout.tsx @@ -8,7 +8,7 @@ import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage'; import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings'; import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/SignInBackgroundMockPage'; import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal'; -import { DESKTOP_NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/DesktopNavDrawerWidths'; +import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useScreenSize } from '@/ui/utilities/screen-size/hooks/useScreenSize'; import { css, Global, useTheme } from '@emotion/react'; @@ -84,7 +84,7 @@ export const DefaultLayout = () => { isSettingsPage && !isMobile ? (windowsWidth - (OBJECT_SETTINGS_WIDTH + - DESKTOP_NAV_DRAWER_WIDTHS.menu + + NAV_DRAWER_WIDTHS.menu.desktop.expanded + 64)) / 2 : 0, diff --git a/packages/twenty-front/src/modules/ui/layout/page/PageHeader.tsx b/packages/twenty-front/src/modules/ui/layout/page/PageHeader.tsx index ed9586bf969a..332dee6a219b 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/PageHeader.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/PageHeader.tsx @@ -13,7 +13,8 @@ import { import { IconButton } from '@/ui/input/button/components/IconButton'; import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton'; -import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState'; + +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; export const PAGE_BAR_MIN_HEIGHT = 40; @@ -108,12 +109,14 @@ export const PageHeader = ({ }: PageHeaderProps) => { const isMobile = useIsMobile(); const theme = useTheme(); - const isNavigationDrawerOpen = useRecoilValue(isNavigationDrawerOpenState); + const isNavigationDrawerExpanded = useRecoilValue( + isNavigationDrawerExpandedState, + ); return ( - {!isMobile && !isNavigationDrawerOpen && ( + {!isMobile && !isNavigationDrawerExpanded && ( diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx index 349c9cfe3edd..c66767a09039 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx @@ -53,7 +53,7 @@ export const TabList = ({ return ( - + {tabs .filter((tab) => !tab.hide) diff --git a/packages/twenty-front/src/modules/ui/layout/top-bar/TopBar.tsx b/packages/twenty-front/src/modules/ui/layout/top-bar/TopBar.tsx index f19e3efaf685..87bee383cbdf 100644 --- a/packages/twenty-front/src/modules/ui/layout/top-bar/TopBar.tsx +++ b/packages/twenty-front/src/modules/ui/layout/top-bar/TopBar.tsx @@ -1,5 +1,5 @@ -import { ReactNode } from 'react'; import styled from '@emotion/styled'; +import { ReactNode } from 'react'; type TopBarProps = { className?: string; @@ -10,14 +10,15 @@ type TopBarProps = { }; const StyledContainer = styled.div` + border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`}; display: flex; + flex-direction: column; `; -const StyledTopBar = styled.div<{ displayBottomBorder: boolean }>` +const StyledTopBar = styled.div` align-items: center; - border-bottom: ${({ displayBottomBorder, theme }) => - displayBottomBorder ? `1px solid ${theme.border.color.light}` : 'none'}; + box-sizing: border-box; color: ${({ theme }) => theme.font.color.secondary}; display: flex; @@ -26,6 +27,8 @@ const StyledTopBar = styled.div<{ displayBottomBorder: boolean }>` height: 39px; justify-content: space-between; padding-right: ${({ theme }) => theme.spacing(2)}; + padding-left: ${({ theme }) => theme.spacing(2)}; + z-index: 7; `; @@ -44,10 +47,9 @@ export const TopBar = ({ leftComponent, rightComponent, bottomComponent, - displayBottomBorder = true, }: TopBarProps) => ( - + {leftComponent} {rightComponent} diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx index 25d934045851..4fa845c6acfc 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx @@ -1,6 +1,6 @@ -import { useState } from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { useState } from 'react'; import { useRecoilValue } from 'recoil'; import { IconChevronDown } from 'twenty-ui'; @@ -15,6 +15,7 @@ import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/c import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching'; import { NavigationDrawerHotKeyScope } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope'; import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI'; +import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper'; const StyledLogo = styled.div<{ logo: string }>` background: url(${({ logo }) => logo}); @@ -37,8 +38,6 @@ const StyledContainer = styled.div` padding: calc(${({ theme }) => theme.spacing(1)} - 1px); width: 100%; - gap: ${({ theme }) => theme.spacing(1)}; - &:hover { background-color: ${({ theme }) => theme.background.transparent.lighter}; border: 1px solid ${({ theme }) => theme.border.color.medium}; @@ -48,6 +47,7 @@ const StyledContainer = styled.div` const StyledLabel = styled.div` align-items: center; display: flex; + margin: 0 ${({ theme }) => theme.spacing(1)}; `; const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>` @@ -95,11 +95,15 @@ export const MultiWorkspaceDropdownButton = ({ ) ?? '' } /> - {currentWorkspace?.displayName ?? ''} - + + {currentWorkspace?.displayName ?? ''} + + + + } dropdownComponents={ diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx index 997441038d29..8fbd853a56ed 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx @@ -1,49 +1,45 @@ -import { css, useTheme } from '@emotion/react'; +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { motion } from 'framer-motion'; import { ReactNode, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { MOBILE_VIEWPORT } from 'twenty-ui'; -import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage'; -import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import { DESKTOP_NAV_DRAWER_WIDTHS } from '../constants/DesktopNavDrawerWidths'; +import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths'; +import { isNavigationDrawerExpandedState } from '../../states/isNavigationDrawerExpanded'; import { NavigationDrawerBackButton } from './NavigationDrawerBackButton'; import { NavigationDrawerHeader } from './NavigationDrawerHeader'; +import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer'; export type NavigationDrawerProps = { children: ReactNode; className?: string; footer?: ReactNode; - isSubMenu?: boolean; logo?: string; title?: string; }; -const StyledAnimatedContainer = styled(motion.div)` - display: flex; - justify-content: end; -`; +const StyledAnimatedContainer = styled(motion.div)``; -const StyledContainer = styled.div<{ isSubMenu?: boolean }>` +const StyledContainer = styled.div<{ + isSettings?: boolean; + isMobile?: boolean; +}>` box-sizing: border-box; display: flex; flex-direction: column; + width: ${NAV_DRAWER_WIDTHS.menu.desktop.expanded}px; gap: ${({ theme }) => theme.spacing(3)}; height: 100%; - min-width: ${DESKTOP_NAV_DRAWER_WIDTHS.menu}px; - padding: ${({ theme }) => theme.spacing(3, 2, 4)}; - - ${({ isSubMenu, theme }) => - isSubMenu - ? css` - padding-left: ${theme.spacing(0)}; - padding-right: ${theme.spacing(8)}; - ` - : ''} + padding: ${({ theme, isSettings, isMobile }) => + isSettings + ? isMobile + ? theme.spacing(3, 8) + : theme.spacing(3, 8, 4, 0) + : theme.spacing(3, 2, 4)}; @media (max-width: ${MOBILE_VIEWPORT}px) { width: 100%; @@ -61,15 +57,16 @@ export const NavigationDrawer = ({ children, className, footer, - isSubMenu, logo, title, }: NavigationDrawerProps) => { const [isHovered, setIsHovered] = useState(false); const isMobile = useIsMobile(); + const isSettingsDrawer = useIsSettingsDrawer(); const theme = useTheme(); - const isNavigationDrawerOpen = useRecoilValue(isNavigationDrawerOpenState); - const isSettingsPage = useIsSettingsPage(); + const isNavigationDrawerExpanded = useRecoilValue( + isNavigationDrawerExpandedState, + ); const handleHover = () => { setIsHovered(true); @@ -79,30 +76,35 @@ export const NavigationDrawer = ({ setIsHovered(false); }; - const desktopWidth = !isNavigationDrawerOpen - ? 12 - : DESKTOP_NAV_DRAWER_WIDTHS.menu; + const desktopWidth = isNavigationDrawerExpanded + ? NAV_DRAWER_WIDTHS.menu.desktop.expanded + : NAV_DRAWER_WIDTHS.menu.desktop.collapsed; - const mobileWidth = isNavigationDrawerOpen ? '100%' : 0; + const mobileWidth = isNavigationDrawerExpanded + ? NAV_DRAWER_WIDTHS.menu.mobile.expanded + : NAV_DRAWER_WIDTHS.menu.mobile.collapsed; + + const navigationDrawerAnimate = { + width: isMobile ? mobileWidth : desktopWidth, + opacity: isNavigationDrawerExpanded || !isSettingsDrawer ? 1 : 0, + }; return ( - {isSubMenu && title ? ( + {isSettingsDrawer && title ? ( !isMobile && ) : ( { + const theme = useTheme(); + const isSettingsPage = useIsSettingsPage(); + const isNavigationDrawerExpanded = useRecoilValue( + isNavigationDrawerExpandedState, + ); + + if (isSettingsPage) { + return children; + } + + const animate: AnimationControls | TargetAndTransition = + isNavigationDrawerExpanded + ? { + opacity: 1, + width: 'auto', + height: 'auto', + pointerEvents: 'auto', + } + : { + opacity: 0, + width: 0, + height: 0, + pointerEvents: 'none', + }; + + return ( + + {children} + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx index 96490d7f0a7e..486027966164 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx @@ -1,9 +1,11 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { useRecoilValue } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { IconX } from 'twenty-ui'; import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; +import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; type NavigationDrawerBackButtonProps = { @@ -43,9 +45,22 @@ export const NavigationDrawerBackButton = ({ const theme = useTheme(); const navigationMemorizedUrl = useRecoilValue(navigationMemorizedUrlState); + const setIsNavigationDrawerExpanded = useSetRecoilState( + isNavigationDrawerExpandedState, + ); + const navigationDrawerExpandedMemorized = useRecoilValue( + navigationDrawerExpandedMemorizedState, + ); + return ( - + + setIsNavigationDrawerExpanded(navigationDrawerExpandedMemorized) + } + > theme.border.radius.md}; @@ -33,15 +32,17 @@ export const NavigationDrawerCollapseButton = ({ className, direction = 'left', }: NavigationDrawerCollapseButtonProps) => { - const setIsNavigationDrawerOpen = useSetRecoilState( - isNavigationDrawerOpenState, + const setIsNavigationDrawerExpanded = useSetRecoilState( + isNavigationDrawerExpandedState, ); return ( - setIsNavigationDrawerOpen((previousIsOpen) => !previousIsOpen) + setIsNavigationDrawerExpanded( + (previousIsExpanded) => !previousIsExpanded, + ) } > ` +const StyledContainer = styled.div` align-items: center; display: flex; - gap: ${({ theme, isMultiWorkspace }) => - !isMultiWorkspace ? theme.spacing(2) : null}; height: ${({ theme }) => theme.spacing(8)}; user-select: none; `; +const StyledSingleWorkspaceContainer = styled(StyledContainer)` + gap: ${({ theme }) => theme.spacing(2)}; +`; const StyledLogo = styled.div<{ logo: string }>` background: url(${({ logo }) => logo}); @@ -57,21 +60,25 @@ export const NavigationDrawerHeader = ({ const isMobile = useIsMobile(); const workspaces = useRecoilValue(workspacesState); const isMultiWorkspace = workspaces !== null && workspaces.length > 1; + const isNavigationDrawerExpanded = useRecoilValue( + isNavigationDrawerExpandedState, + ); return ( - + {isMultiWorkspace ? ( ) : ( - <> + - {name} - + + {name} + + )} - - {!isMobile && ( + {!isMobile && isNavigationDrawerExpanded && ( ; +> & { isNavigationDrawerExpanded: boolean }; const StyledItem = styled('div', { shouldForwardProp: (prop) => @@ -65,9 +68,8 @@ const StyledItem = styled('div', { }}; cursor: ${(props) => (props.soon ? 'default' : 'pointer')}; display: flex; - font-family: 'Inter'; + font-family: ${({ theme }) => theme.font.family}; font-size: ${({ theme }) => theme.font.size.md}; - gap: ${({ theme }) => theme.spacing(2)}; padding-bottom: ${({ theme }) => theme.spacing(1)}; padding-left: ${({ theme }) => theme.spacing(1)}; @@ -78,7 +80,12 @@ const StyledItem = styled('div', { indentationLevel === 2 ? '2px' : '0'}; pointer-events: ${(props) => (props.soon ? 'none' : 'auto')}; - width: 100%; + + width: ${(props) => + !props.isNavigationDrawerExpanded + ? `${NAV_DRAWER_WIDTHS.menu.desktop.collapsed - 24}px` + : '100%'}; + :hover { background: ${({ theme }) => theme.background.transparent.light}; color: ${(props) => @@ -96,9 +103,14 @@ const StyledItem = styled('div', { } `; +const StyledItemElementsContainer = styled.div` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; +`; + const StyledItemLabel = styled.div` font-weight: ${({ theme }) => theme.font.weight.medium}; - overflow: hidden; text-overflow: ellipsis; white-space: nowrap; `; @@ -111,7 +123,6 @@ const StyledItemCount = styled.div` display: flex; font-size: ${({ theme }) => theme.font.size.xs}; font-weight: ${({ theme }) => theme.font.weight.semiBold}; - height: 16px; justify-content: center; margin-left: auto; @@ -151,16 +162,15 @@ export const NavigationDrawerItem = ({ }: NavigationDrawerItemProps) => { const theme = useTheme(); const isMobile = useIsMobile(); + const isSettingsPage = useIsSettingsPage(); const navigate = useNavigate(); - const setIsNavigationDrawerOpen = useSetRecoilState( - isNavigationDrawerOpenState, - ); - + const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] = + useRecoilState(isNavigationDrawerExpandedState); const showBreadcrumb = indentationLevel === 2; const handleItemClick = () => { if (isMobile) { - setIsNavigationDrawerOpen(false); + setIsNavigationDrawerExpanded(false); } if (isDefined(onClick)) { @@ -185,25 +195,51 @@ export const NavigationDrawerItem = ({ as={to ? Link : 'div'} to={to ? to : undefined} indentationLevel={indentationLevel} + isNavigationDrawerExpanded={isNavigationDrawerExpanded} > {showBreadcrumb && ( - - )} - {Icon && ( - - )} - {label} - {soon && } - {!!count && {count}} - {keyboard && ( - - {keyboard} - + + + )} + + {Icon && ( + + )} + + + {label} + + + {soon && ( + + + + )} + + {!!count && ( + + {count} + + )} + + {keyboard && ( + + + {keyboard} + + + )} + ); diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemBreadcrumb.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemBreadcrumb.tsx index 47e8bd120864..6c3998544111 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemBreadcrumb.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemBreadcrumb.tsx @@ -6,9 +6,10 @@ export type NavigationDrawerItemBreadcrumbProps = { }; const StyledNavigationDrawerItemBreadcrumbContainer = styled.div` - margin-left: 7.5px; - height: 28px; + + margin-left: 7.5px; + margin-right: ${({ theme }) => theme.spacing(2)}; width: 9px; `; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer.tsx new file mode 100644 index 000000000000..550793621c0b --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer.tsx @@ -0,0 +1,52 @@ +import styled from '@emotion/styled'; +import { ReactNode } from 'react'; +import { useRecoilValue } from 'recoil'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; +import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage'; +import { AnimationControls, motion, TargetAndTransition } from 'framer-motion'; +import { useTheme } from '@emotion/react'; + +const StyledAnimationGroupContainer = styled(motion.div)``; + +type NavigationDrawerItemsCollapsedContainerProps = { + isGroup?: boolean; + children: ReactNode; +}; + +export const NavigationDrawerItemsCollapsedContainer = ({ + isGroup = false, + children, +}: NavigationDrawerItemsCollapsedContainerProps) => { + const theme = useTheme(); + const isSettingsPage = useIsSettingsPage(); + const isNavigationDrawerExpanded = useRecoilValue( + isNavigationDrawerExpandedState, + ); + const isExpanded = isNavigationDrawerExpanded || isSettingsPage; + let animate: AnimationControls | TargetAndTransition = { + width: 'auto', + backgroundColor: 'transparent', + border: 'none', + }; + if (!isExpanded) { + animate = { width: 24 }; + if (isGroup) { + animate = { + width: 24, + backgroundColor: theme.background.transparent.lighter, + border: `1px solid ${theme.background.transparent.lighter}`, + borderRadius: theme.border.radius.sm, + }; + } + } + + return ( + + {children} + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx index d208dccf111a..aeeffa88d343 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx @@ -1,8 +1,10 @@ import styled from '@emotion/styled'; import { currentUserState } from '@/auth/states/currentUserState'; +import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; import { NavigationDrawerSectionTitleSkeletonLoader } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitleSkeletonLoader'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-ui'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; @@ -37,9 +39,22 @@ export const NavigationDrawerSectionTitle = ({ }: NavigationDrawerSectionTitleProps) => { const currentUser = useRecoilValue(currentUserState); const loading = useIsPrefetchLoading(); + const isNavigationDrawerExpanded = useRecoilValue( + isNavigationDrawerExpandedState, + ); + + const isSettingsPage = useIsSettingsPage(); if (loading && isDefined(currentUser)) { return ; } - return {label}; + return ( + + {label} + + ); }; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawer.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawer.stories.tsx index dfbe324297a4..1fe45b7822ac 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawer.stories.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawer.stories.tsx @@ -53,6 +53,11 @@ export const Default: Story = { Icon={IconBell} soon={true} /> + diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/constants/DesktopNavDrawerWidths.ts b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/constants/DesktopNavDrawerWidths.ts deleted file mode 100644 index fe5462310dce..000000000000 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/constants/DesktopNavDrawerWidths.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DESKTOP_NAV_DRAWER_WIDTHS = { - menu: 220, -}; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/constants/NavDrawerWidths.ts b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/constants/NavDrawerWidths.ts new file mode 100644 index 000000000000..3abe70079807 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/constants/NavDrawerWidths.ts @@ -0,0 +1,12 @@ +export const NAV_DRAWER_WIDTHS = { + menu: { + mobile: { + collapsed: 0, + expanded: '100%', + }, + desktop: { + collapsed: 40, + expanded: 220, + }, + }, +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/states/isNavigationDrawerOpenState.ts b/packages/twenty-front/src/modules/ui/navigation/states/isNavigationDrawerExpanded.ts similarity index 63% rename from packages/twenty-front/src/modules/ui/navigation/states/isNavigationDrawerOpenState.ts rename to packages/twenty-front/src/modules/ui/navigation/states/isNavigationDrawerExpanded.ts index b5496b4a57ac..7155d06078de 100644 --- a/packages/twenty-front/src/modules/ui/navigation/states/isNavigationDrawerOpenState.ts +++ b/packages/twenty-front/src/modules/ui/navigation/states/isNavigationDrawerExpanded.ts @@ -3,7 +3,7 @@ import { MOBILE_VIEWPORT } from 'twenty-ui'; const isMobile = window.innerWidth <= MOBILE_VIEWPORT; -export const isNavigationDrawerOpenState = atom({ - key: 'isNavigationDrawerOpen', +export const isNavigationDrawerExpandedState = atom({ + key: 'isNavigationDrawerExpanded', default: !isMobile, }); diff --git a/packages/twenty-front/src/modules/ui/navigation/states/navigationDrawerExpandedMemorizedState.ts b/packages/twenty-front/src/modules/ui/navigation/states/navigationDrawerExpandedMemorizedState.ts new file mode 100644 index 000000000000..cdeff16e74ac --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/states/navigationDrawerExpandedMemorizedState.ts @@ -0,0 +1,9 @@ +import { atom } from 'recoil'; +import { MOBILE_VIEWPORT } from 'twenty-ui'; + +const isMobile = window.innerWidth <= MOBILE_VIEWPORT; + +export const navigationDrawerExpandedMemorizedState = atom({ + key: 'navigationDrawerExpandedMemorized', + default: !isMobile, +}); diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx index d4e5c2e79f78..97d4560694fd 100644 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx @@ -26,16 +26,16 @@ const StyledScrollWrapper = styled.div` export type ScrollWrapperProps = { children: React.ReactNode; className?: string; - hideY?: boolean; - hideX?: boolean; + enableXScroll?: boolean; + enableYScroll?: boolean; contextProviderName: ContextProviderName; }; export const ScrollWrapper = ({ children, className, - hideX, - hideY, + enableXScroll = true, + enableYScroll = true, contextProviderName, }: ScrollWrapperProps) => { const scrollableRef = useRef(null); @@ -58,8 +58,8 @@ export const ScrollWrapper = ({ options: { scrollbars: { autoHide: 'scroll' }, overflow: { - y: hideY ? 'hidden' : undefined, - x: hideX ? 'hidden' : undefined, + x: enableXScroll ? undefined : 'hidden', + y: enableYScroll ? undefined : 'hidden', }, }, events: { diff --git a/packages/twenty-front/src/modules/views/components/ViewBar.tsx b/packages/twenty-front/src/modules/views/components/ViewBar.tsx index 724b80e461c3..186f54b691ba 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBar.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBar.tsx @@ -60,7 +60,6 @@ export const ViewBar = ({ leftComponent={ loading ? : } - displayBottomBorder={false} rightComponent={ <> theme.spacing(6)}; @@ -37,6 +37,7 @@ type WorkflowEditActionFormSendEmailProps = type SendEmailFormData = { connectedAccountId: string; + email: string; subject: string; body: string; }; @@ -53,6 +54,7 @@ export const WorkflowEditActionFormSendEmail = ( const form = useForm({ defaultValues: { connectedAccountId: '', + email: '', subject: '', body: '', }, @@ -83,10 +85,11 @@ export const WorkflowEditActionFormSendEmail = ( useEffect(() => { form.setValue( 'connectedAccountId', - props.action.settings.connectedAccountId ?? '', + props.action.settings.input.connectedAccountId ?? '', ); - form.setValue('subject', props.action.settings.subject ?? ''); - form.setValue('body', props.action.settings.body ?? ''); + form.setValue('email', props.action.settings.input.email ?? ''); + form.setValue('subject', props.action.settings.input.subject ?? ''); + form.setValue('body', props.action.settings.input.body ?? ''); }, [props.action.settings, form]); const saveAction = useDebouncedCallback( @@ -99,9 +102,12 @@ export const WorkflowEditActionFormSendEmail = ( ...props.action, settings: { ...props.action.settings, - connectedAccountId: formData.connectedAccountId, - subject: formData.subject, - body: formData.body, + input: { + connectedAccountId: formData.connectedAccountId, + email: formData.email, + subject: formData.subject, + body: formData.body, + }, }, }); @@ -134,12 +140,12 @@ export const WorkflowEditActionFormSendEmail = ( }; if ( - isDefined(props.action.settings.connectedAccountId) && - props.action.settings.connectedAccountId !== '' + isDefined(props.action.settings.input.connectedAccountId) && + props.action.settings.input.connectedAccountId !== '' ) { filter.or.push({ id: { - eq: props.action.settings.connectedAccountId, + eq: props.action.settings.input.connectedAccountId, }, }); } @@ -198,6 +204,21 @@ export const WorkflowEditActionFormSendEmail = ( /> )} /> + ( + { + field.onChange(email); + handleSave(); + }} + /> + )} + /> { @@ -66,7 +66,9 @@ export const WorkflowEditActionFormServerlessFunction = ( ...props.action, settings: { ...props.action.settings, - serverlessFunctionId: updatedFunction, + input: { + serverlessFunctionId: updatedFunction, + }, }, }); }} diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts index 0ed8422846b9..65b2e9a25a15 100644 --- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -10,13 +10,18 @@ type BaseWorkflowStepSettings = { }; export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & { - serverlessFunctionId: string; + input: { + serverlessFunctionId: string; + }; }; export type WorkflowSendEmailStepSettings = BaseWorkflowStepSettings & { - connectedAccountId: string; - subject?: string; - body?: string; + input: { + connectedAccountId: string; + email: string; + subject?: string; + body?: string; + }; }; type BaseWorkflowStep = { diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/addCreateStepNodes.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/addCreateStepNodes.test.ts index d0f46dde3576..c2c9d760fe15 100644 --- a/packages/twenty-front/src/modules/workflow/utils/__tests__/addCreateStepNodes.test.ts +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/addCreateStepNodes.test.ts @@ -21,7 +21,9 @@ describe('addCreateStepNodes', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, }, { @@ -34,7 +36,9 @@ describe('addCreateStepNodes', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, }, ]; diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts index ebf4f3c21020..663e1ef5661c 100644 --- a/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts @@ -42,7 +42,9 @@ describe('generateWorkflowDiagram', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, }, { @@ -55,7 +57,9 @@ describe('generateWorkflowDiagram', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, }, ]; @@ -96,7 +100,9 @@ describe('generateWorkflowDiagram', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, }, { @@ -109,7 +115,9 @@ describe('generateWorkflowDiagram', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, }, ]; diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/getWorkflowVersionDiagram.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/getWorkflowVersionDiagram.test.ts index 907524a725e7..9c10c2af4f9f 100644 --- a/packages/twenty-front/src/modules/workflow/utils/__tests__/getWorkflowVersionDiagram.test.ts +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/getWorkflowVersionDiagram.test.ts @@ -80,7 +80,9 @@ describe('getWorkflowVersionDiagram', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, type: 'CODE', valid: true, diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/insertStep.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/insertStep.test.ts index ba93aa6c341e..b264d5edafbc 100644 --- a/packages/twenty-front/src/modules/workflow/utils/__tests__/insertStep.test.ts +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/insertStep.test.ts @@ -25,7 +25,9 @@ describe('insertStep', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, type: 'CODE', valid: true, @@ -63,7 +65,9 @@ describe('insertStep', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, type: 'CODE', valid: true, @@ -95,7 +99,9 @@ describe('insertStep', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, type: 'CODE', valid: true, @@ -108,7 +114,9 @@ describe('insertStep', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, type: 'CODE', valid: true, @@ -129,7 +137,9 @@ describe('insertStep', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, type: 'CODE', valid: true, @@ -165,7 +175,9 @@ describe('insertStep', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, type: 'CODE', valid: true, @@ -178,7 +190,9 @@ describe('insertStep', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, type: 'CODE', valid: true, @@ -199,7 +213,9 @@ describe('insertStep', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, type: 'CODE', valid: true, diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/removeStep.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/removeStep.test.ts index 01385411bf02..349d2f74200b 100644 --- a/packages/twenty-front/src/modules/workflow/utils/__tests__/removeStep.test.ts +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/removeStep.test.ts @@ -10,7 +10,9 @@ it('returns a deep copy of the provided steps array instead of mutating it', () retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'first', + input: { + serverlessFunctionId: 'first', + }, }, type: 'CODE', valid: true, @@ -47,7 +49,9 @@ it('removes a step in a non-empty steps array', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, type: 'CODE', valid: true, @@ -67,7 +71,9 @@ it('removes a step in a non-empty steps array', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, type: 'CODE', valid: true, @@ -81,7 +87,9 @@ it('removes a step in a non-empty steps array', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, type: 'CODE', valid: true, diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/replaceStep.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/replaceStep.test.ts index 93286c59013e..41e4f8cbae8b 100644 --- a/packages/twenty-front/src/modules/workflow/utils/__tests__/replaceStep.test.ts +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/replaceStep.test.ts @@ -11,7 +11,9 @@ describe('replaceStep', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'first', + input: { + serverlessFunctionId: 'first', + }, }, type: 'CODE', valid: true, @@ -39,7 +41,9 @@ describe('replaceStep', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'second', + input: { + serverlessFunctionId: 'second', + }, }, }, stepId: stepToBeReplaced.id, @@ -57,7 +61,9 @@ describe('replaceStep', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, type: 'CODE', valid: true, @@ -77,7 +83,9 @@ describe('replaceStep', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, type: 'CODE', valid: true, @@ -91,7 +99,9 @@ describe('replaceStep', () => { retryOnFailure: { value: true }, continueOnFailure: { value: false }, }, - serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + input: { + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, }, type: 'CODE', valid: true, diff --git a/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts b/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts index 48e8f9bd448f..fd63158d2548 100644 --- a/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts +++ b/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts @@ -14,7 +14,9 @@ export const getStepDefaultDefinition = ( type: 'CODE', valid: false, settings: { - serverlessFunctionId: '', + input: { + serverlessFunctionId: '', + }, errorHandlingOptions: { continueOnFailure: { value: false, @@ -33,9 +35,12 @@ export const getStepDefaultDefinition = ( type: 'SEND_EMAIL', valid: false, settings: { - connectedAccountId: '', - subject: '', - body: '', + input: { + connectedAccountId: '', + email: '', + subject: '', + body: '', + }, errorHandlingOptions: { continueOnFailure: { value: false, diff --git a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts index cde44977047b..5471c5d4d59e 100644 --- a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts +++ b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts @@ -10,9 +10,7 @@ export type FeatureFlagKey = | 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED' | 'IS_WORKFLOW_ENABLED' | 'IS_WORKSPACE_FAVORITE_ENABLED' - | 'IS_SEARCH_ENABLED' | 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED' | 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED' - | 'IS_WORKSPACE_MIGRATED_FOR_SEARCH' | 'IS_ANALYTICS_V2_ENABLED' | 'IS_UNIQUE_INDEXES_ENABLED'; diff --git a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx index 7700073a611a..0473832f5d2f 100644 --- a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx @@ -62,6 +62,7 @@ export const RecordIndexPage = () => { recordIndexId, objectNamePlural, objectNameSingular, + objectMetadataItem, onIndexRecordsLoaded: handleIndexRecordsLoaded, onIndexIdentifierClick: handleIndexIdentifierClick, onCreateRecord: handleCreateRecord, diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsTimeZoneSelect.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsTimeZoneSelect.tsx index 348cfded0779..237550a1a72b 100644 --- a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsTimeZoneSelect.tsx +++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsTimeZoneSelect.tsx @@ -2,6 +2,7 @@ import { detectTimeZone } from '@/localization/utils/detectTimeZone'; import { findAvailableTimeZoneOption } from '@/localization/utils/findAvailableTimeZoneOption'; import { AVAILABLE_TIMEZONE_OPTIONS } from '@/settings/accounts/constants/AvailableTimezoneOptions'; import { Select } from '@/ui/input/components/Select'; +import { isDefined } from '~/utils/isDefined'; type DateTimeSettingsTimeZoneSelectProps = { value?: string; @@ -25,7 +26,9 @@ export const DateTimeSettingsTimeZoneSelect = ({ value={value} options={[ { - label: `System settings - ${systemTimeZoneOption.label}`, + label: isDefined(systemTimeZoneOption) + ? `System settings - ${systemTimeZoneOption.label}` + : 'System settings', value: 'system', }, ...AVAILABLE_TIMEZONE_OPTIONS, diff --git a/packages/twenty-front/src/testing/graphqlMocks.ts b/packages/twenty-front/src/testing/graphqlMocks.ts index a69d6c17c660..1cb7c4b3aef5 100644 --- a/packages/twenty-front/src/testing/graphqlMocks.ts +++ b/packages/twenty-front/src/testing/graphqlMocks.ts @@ -62,23 +62,47 @@ export const graphqlMocks = { }); }, ), - graphql.query('FindManyViews', ({ variables }) => { - 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, + startCursor: null, + endCursor: null, + }, + }, + }, + }); + }), + 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, 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/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/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..cfd570281187 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 @@ -10,10 +10,6 @@ import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-qu 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 { 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'; @@ -100,20 +96,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-query-builder/factories/__tests__/args-string.factory.spec.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/__tests__/args-string.factory.spec.ts deleted file mode 100644 index f4be0b8fb204..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/__tests__/args-string.factory.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { TestingModule, Test } from '@nestjs/testing'; - -import { ArgsAliasFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory'; -import { ArgsStringFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/args-string.factory'; - -describe('ArgsStringFactory', () => { - let service: ArgsStringFactory; - const argsAliasCreate = jest.fn(); - - beforeEach(async () => { - jest.resetAllMocks(); - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - ArgsStringFactory, - { - provide: ArgsAliasFactory, - useValue: { - create: argsAliasCreate, - }, - }, - ], - }).compile(); - - service = module.get(ArgsStringFactory); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('create', () => { - it('should return null when args are missing', () => { - const args = undefined; - - const result = service.create(args, []); - - expect(result).toBeNull(); - }); - - it('should return a string with the args when args are present', () => { - const args = { - id: '1', - name: 'field_name', - }; - - argsAliasCreate.mockReturnValue(args); - - const result = service.create(args, []); - - expect(result).toEqual('id: "1", name: "field_name"'); - }); - - it('should return a string with the args when args are present and the value is an object', () => { - const args = { - id: '1', - name: { - firstName: 'test', - }, - }; - - argsAliasCreate.mockReturnValue(args); - - const result = service.create(args, []); - - expect(result).toEqual('id: "1", name: {firstName:"test"}'); - }); - - it('when orderBy is present, should return an array of objects', () => { - const args = { - orderBy: [{ id: 'AscNullsFirst' }, { name: 'AscNullsFirst' }], - }; - - argsAliasCreate.mockReturnValue(args); - - const result = service.create(args, []); - - expect(result).toEqual( - 'orderBy: [{id: AscNullsFirst}, {name: AscNullsFirst}]', - ); - }); - - it('when orderBy is present with position criteria, should return position at the end of the list', () => { - const args = { - orderBy: [ - { position: 'AscNullsFirst' }, - { id: 'AscNullsFirst' }, - { name: 'AscNullsFirst' }, - ], - }; - - argsAliasCreate.mockReturnValue(args); - - const result = service.create(args, []); - - expect(result).toEqual( - 'orderBy: [{id: AscNullsFirst}, {name: AscNullsFirst}, {position: AscNullsFirst}]', - ); - }); - - it('when orderBy is present with position in the middle, should return position at the end of the list', () => { - const args = { - orderBy: [ - { id: 'AscNullsFirst' }, - { position: 'AscNullsFirst' }, - { name: 'AscNullsFirst' }, - ], - }; - - argsAliasCreate.mockReturnValue(args); - - const result = service.create(args, []); - - expect(result).toEqual( - 'orderBy: [{id: AscNullsFirst}, {name: AscNullsFirst}, {position: AscNullsFirst}]', - ); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory.ts deleted file mode 100644 index f5ccbf97515d..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; - -import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; -import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; - -@Injectable() -export class ArgsAliasFactory { - private readonly logger = new Logger(ArgsAliasFactory.name); - - create( - args: Record, - fieldMetadataCollection: FieldMetadataInterface[], - ): Record { - const fieldMetadataMap = new Map( - fieldMetadataCollection.map((fieldMetadata) => [ - fieldMetadata.name, - fieldMetadata, - ]), - ); - - return this.createArgsObjectRecursive(args, fieldMetadataMap); - } - - private createArgsObjectRecursive( - args: Record, - fieldMetadataMap: Map, - ) { - // If it's not an object, we don't need to do anything - if (typeof args !== 'object' || args === null) { - return args; - } - - // If it's an array, we need to map all items - if (Array.isArray(args)) { - return args.map((arg) => - this.createArgsObjectRecursive(arg, fieldMetadataMap), - ); - } - - const newArgs = {}; - - for (const [key, value] of Object.entries(args)) { - const fieldMetadata = fieldMetadataMap.get(key); - - // If it's a composite type, we need to transform args to properly map column name - if ( - fieldMetadata && - value !== null && - isCompositeFieldMetadataType(fieldMetadata.type) - ) { - // Get composite type definition - const compositeType = compositeTypeDefinitions.get(fieldMetadata.type); - - if (!compositeType) { - this.logger.error( - `Composite type definition not found for type: ${fieldMetadata.type}`, - ); - throw new Error( - `Composite type definition not found for type: ${fieldMetadata.type}`, - ); - } - - // Loop through sub values and map them to composite property - for (const [subKey, subValue] of Object.entries(value)) { - // Find composite property - const compositeProperty = compositeType.properties.find( - (property) => property.name === subKey, - ); - - if (compositeProperty) { - const columnName = computeCompositeColumnName( - fieldMetadata, - compositeProperty, - ); - - newArgs[columnName] = subValue; - } - } - } else if (fieldMetadata) { - newArgs[key] = value; - } else { - // Recurse if value is a nested object, otherwise append field or alias - newArgs[key] = this.createArgsObjectRecursive(value, fieldMetadataMap); - } - } - - return newArgs; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/args-string.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/args-string.factory.ts deleted file mode 100644 index 5755fe413489..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/args-string.factory.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; - -import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util'; -import { isDefined } from 'src/utils/is-defined'; - -import { ArgsAliasFactory } from './args-alias.factory'; - -@Injectable() -export class ArgsStringFactory { - constructor(private readonly argsAliasFactory: ArgsAliasFactory) {} - - create( - initialArgs: Record | undefined, - fieldMetadataCollection: FieldMetadataInterface[], - softDeletable?: boolean, - ): string | null { - if (!initialArgs) { - return null; - } - if (softDeletable) { - initialArgs.filter = { - and: [initialArgs.filter, { deletedAt: { is: 'NULL' } }].filter( - isDefined, - ), - }; - } - let argsString = ''; - const computedArgs = this.argsAliasFactory.create( - initialArgs, - fieldMetadataCollection, - ); - - for (const key in computedArgs) { - // Check if the value is not undefined - if (computedArgs[key] === undefined) { - continue; - } - - if (typeof computedArgs[key] === 'string') { - // If it's a string, add quotes - argsString += `${key}: "${computedArgs[key]}", `; - } else if ( - typeof computedArgs[key] === 'object' && - computedArgs[key] !== null - ) { - if (key === 'orderBy') { - argsString += `${key}: ${this.buildStringifiedOrderBy( - computedArgs[key], - )}, `; - } else { - // If it's an object (and not null), stringify it - argsString += `${key}: ${stringifyWithoutKeyQuote( - computedArgs[key], - )}, `; - } - } else { - // For other types (number, boolean), add as is - argsString += `${key}: ${computedArgs[key]}, `; - } - } - - // Remove trailing comma and space, if present - if (argsString.endsWith(', ')) { - argsString = argsString.slice(0, -2); - } - - return argsString; - } - - private buildStringifiedOrderBy( - keyValuePairArray: Array>, - ): string { - if ( - keyValuePairArray.length !== 0 && - Object.keys(keyValuePairArray[0]).length === 0 - ) { - return `[]`; - } - // if position argument is present we want to put it at the very last - let orderByString = keyValuePairArray - .sort((_, obj) => (Object.hasOwnProperty.call(obj, 'position') ? -1 : 0)) - .map((obj) => { - const [key] = Object.keys(obj); - const value = obj[key]; - - return `{${key}: ${value}}`; - }) - .join(', '); - - if (orderByString.endsWith(', ')) { - orderByString = orderByString.slice(0, -2); - } - - return `[${orderByString}]`; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/create-many-query.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/create-many-query.factory.ts deleted file mode 100644 index 85dac955cda5..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/create-many-query.factory.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { v4 as uuidv4 } from 'uuid'; - -import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; -import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface'; -import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; - -import { ArgsAliasFactory } from './args-alias.factory'; -import { FieldsStringFactory } from './fields-string.factory'; - -@Injectable() -export class CreateManyQueryFactory { - constructor( - private readonly fieldsStringFactory: FieldsStringFactory, - private readonly argsAliasFactory: ArgsAliasFactory, - ) {} - - async create( - args: CreateManyResolverArgs>, - options: WorkspaceQueryBuilderOptions, - ) { - const fieldsString = await this.fieldsStringFactory.create( - options.info, - options.fieldMetadataCollection, - options.objectMetadataCollection, - ); - - const computedArgsData = this.argsAliasFactory.create( - args.data, - options.fieldMetadataCollection, - ); - - return ` - mutation { - insertInto${computeObjectTargetTable( - options.objectMetadataItem, - )}Collection(objects: ${stringifyWithoutKeyQuote( - computedArgsData.map((datum) => ({ - id: uuidv4(), - ...datum, - })), - )}) { - affectedCount - records { - ${fieldsString} - } - } - } - `; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/delete-many-query.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/delete-many-query.factory.ts deleted file mode 100644 index f24090c66540..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/delete-many-query.factory.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface'; -import { DeleteManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; - -import { FieldsStringFactory } from './fields-string.factory'; - -export interface DeleteManyQueryFactoryOptions - extends WorkspaceQueryBuilderOptions { - atMost?: number; -} - -@Injectable() -export class DeleteManyQueryFactory { - constructor(private readonly fieldsStringFactory: FieldsStringFactory) {} - - async create( - args: DeleteManyResolverArgs, - options: DeleteManyQueryFactoryOptions, - ) { - const fieldsString = await this.fieldsStringFactory.create( - options.info, - options.fieldMetadataCollection, - options.objectMetadataCollection, - ); - - return ` - mutation { - deleteFrom${computeObjectTargetTable( - options.objectMetadataItem, - )}Collection(filter: ${stringifyWithoutKeyQuote( - args.filter, - )}, atMost: ${options.atMost ?? 1}) { - affectedCount - records { - ${fieldsString} - } - } - } - `; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/delete-one-query.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/delete-one-query.factory.ts deleted file mode 100644 index f4e4e948a670..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/delete-one-query.factory.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface'; -import { DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; - -import { FieldsStringFactory } from './fields-string.factory'; - -@Injectable() -export class DeleteOneQueryFactory { - constructor(private readonly fieldsStringFactory: FieldsStringFactory) {} - - async create( - args: DeleteOneResolverArgs, - options: WorkspaceQueryBuilderOptions, - ) { - const fieldsString = await this.fieldsStringFactory.create( - options.info, - options.fieldMetadataCollection, - options.objectMetadataCollection, - ); - - return ` - mutation { - deleteFrom${computeObjectTargetTable( - options.objectMetadataItem, - )}Collection(filter: { id: { eq: "${args.id}" } }) { - affectedCount - records { - ${fieldsString} - } - } - } - `; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts index 58c97cd2675f..ca9778a33f11 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts @@ -1,34 +1,8 @@ import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory'; -import { ArgsAliasFactory } from './args-alias.factory'; -import { ArgsStringFactory } from './args-string.factory'; -import { CreateManyQueryFactory } from './create-many-query.factory'; -import { DeleteManyQueryFactory } from './delete-many-query.factory'; -import { DeleteOneQueryFactory } from './delete-one-query.factory'; -import { FieldAliasFactory } from './field-alias.factory'; -import { FieldsStringFactory } from './fields-string.factory'; -import { FindDuplicatesQueryFactory } from './find-duplicates-query.factory'; -import { FindManyQueryFactory } from './find-many-query.factory'; -import { FindOneQueryFactory } from './find-one-query.factory'; import { RecordPositionQueryFactory } from './record-position-query.factory'; -import { RelationFieldAliasFactory } from './relation-field-alias.factory'; -import { UpdateManyQueryFactory } from './update-many-query.factory'; -import { UpdateOneQueryFactory } from './update-one-query.factory'; export const workspaceQueryBuilderFactories = [ - ArgsAliasFactory, - ArgsStringFactory, - RelationFieldAliasFactory, - CreateManyQueryFactory, - DeleteOneQueryFactory, - FieldAliasFactory, - FieldsStringFactory, - FindManyQueryFactory, - FindOneQueryFactory, - FindDuplicatesQueryFactory, RecordPositionQueryFactory, - UpdateOneQueryFactory, - UpdateManyQueryFactory, - DeleteManyQueryFactory, ForeignDataWrapperServerQueryFactory, ]; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/field-alias.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/field-alias.factory.ts deleted file mode 100644 index e50028109e6f..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/field-alias.factory.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; - -import { createCompositeFieldKey } from 'src/engine/api/graphql/workspace-query-builder/utils/composite-field-metadata.util'; -import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; -import { - computeColumnName, - computeCompositeColumnName, -} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; - -@Injectable() -export class FieldAliasFactory { - private readonly logger = new Logger(FieldAliasFactory.name); - - create(fieldKey: string, fieldMetadata: FieldMetadataInterface) { - // If it's not a composite field, we can just return the alias - if (!isCompositeFieldMetadataType(fieldMetadata.type)) { - const alias = computeColumnName(fieldMetadata); - - return `${fieldKey}: ${alias}`; - } - - // If it's a composite field, we need to get the definition - const compositeType = compositeTypeDefinitions.get(fieldMetadata.type); - - if (!compositeType) { - this.logger.error( - `Composite type not found for field metadata type: ${fieldMetadata.type}`, - ); - throw new Error( - `Composite type not found for field metadata type: ${fieldMetadata.type}`, - ); - } - - return compositeType.properties - .map((property) => { - // Generate a prefixed key for the composite field, this will be computed when the query has ran - const compositeKey = createCompositeFieldKey( - fieldMetadata.name, - property.name, - ); - const alias = computeCompositeColumnName(fieldMetadata, property); - - return `${compositeKey}: ${alias}`; - }) - .join('\n'); - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/fields-string.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/fields-string.factory.ts deleted file mode 100644 index 3b5c73d819e9..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/fields-string.factory.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { GraphQLResolveInfo } from 'graphql'; -import graphqlFields from 'graphql-fields'; -import isEmpty from 'lodash.isempty'; - -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; -import { Record } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; - -import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; - -import { FieldAliasFactory } from './field-alias.factory'; -import { RelationFieldAliasFactory } from './relation-field-alias.factory'; - -@Injectable() -export class FieldsStringFactory { - constructor( - private readonly fieldAliasFactory: FieldAliasFactory, - private readonly relationFieldAliasFactory: RelationFieldAliasFactory, - ) {} - - async create( - info: GraphQLResolveInfo, - fieldMetadataCollection: FieldMetadataInterface[], - objectMetadataCollection: ObjectMetadataInterface[], - withSoftDeleted?: boolean, - ): Promise { - const selectedFields: Partial = graphqlFields(info); - - const res = await this.createFieldsStringRecursive( - info, - selectedFields, - fieldMetadataCollection, - objectMetadataCollection, - withSoftDeleted ?? false, - ); - - return res; - } - - async createFieldsStringRecursive( - info: GraphQLResolveInfo, - selectedFields: Partial, - fieldMetadataCollection: FieldMetadataInterface[], - objectMetadataCollection: ObjectMetadataInterface[], - withSoftDeleted: boolean, - accumulator = '', - ): Promise { - const fieldMetadataMap = new Map( - fieldMetadataCollection.map((metadata) => [metadata.name, metadata]), - ); - - for (const [fieldKey, fieldValue] of Object.entries(selectedFields)) { - let fieldAlias: string | null; - - if (fieldMetadataMap.has(fieldKey)) { - // We're sure that the field exists in the map after this if condition - // ES6 should tackle that more properly - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const fieldMetadata = fieldMetadataMap.get(fieldKey)!; - - // If the field is a relation field, we need to create a special alias - if (isRelationFieldMetadataType(fieldMetadata.type)) { - const alias = await this.relationFieldAliasFactory.create( - fieldKey, - fieldValue, - fieldMetadata, - objectMetadataCollection, - info, - withSoftDeleted, - ); - - fieldAlias = alias; - } else { - // Otherwise we just need to create a simple alias - const alias = this.fieldAliasFactory.create(fieldKey, fieldMetadata); - - fieldAlias = alias; - } - } - - fieldAlias ??= fieldKey; - - // Recurse if value is a nested object, otherwise append field or alias - if ( - !fieldMetadataMap.has(fieldKey) && - fieldValue && - typeof fieldValue === 'object' && - !isEmpty(fieldValue) - ) { - accumulator += `${fieldKey} {\n`; - accumulator = await this.createFieldsStringRecursive( - info, - fieldValue, - fieldMetadataCollection, - objectMetadataCollection, - withSoftDeleted, - accumulator, - ); - accumulator += `}\n`; - } else { - accumulator += `${fieldAlias}\n`; - } - } - - return accumulator; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/find-duplicates-query.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/find-duplicates-query.factory.ts deleted file mode 100644 index 2d21b6aadc5e..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/find-duplicates-query.factory.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import isEmpty from 'lodash.isempty'; - -import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface'; -import { Record } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; -import { FindDuplicatesResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util'; -import { ArgsAliasFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory'; -import { DuplicateService } from 'src/engine/core-modules/duplicate/duplicate.service'; - -import { FieldsStringFactory } from './fields-string.factory'; - -@Injectable() -export class FindDuplicatesQueryFactory { - constructor( - private readonly fieldsStringFactory: FieldsStringFactory, - private readonly argsAliasFactory: ArgsAliasFactory, - private readonly duplicateService: DuplicateService, - ) {} - - async create( - args: FindDuplicatesResolverArgs, - options: WorkspaceQueryBuilderOptions, - existingRecords?: Record[], - ) { - const fieldsString = await this.fieldsStringFactory.create( - options.info, - options.fieldMetadataCollection, - options.objectMetadataCollection, - ); - - if (existingRecords) { - const query = existingRecords.reduce((acc, record, index) => { - return ( - acc + this.buildQuery(fieldsString, options, undefined, record, index) - ); - }, ''); - - return `query { - ${query} - }`; - } - - const query = args.data?.reduce((acc, dataItem, index) => { - const argsData = this.argsAliasFactory.create( - dataItem ?? {}, - options.fieldMetadataCollection, - ); - - return ( - acc + - this.buildQuery( - fieldsString, - options, - argsData as Record, - undefined, - index, - ) - ); - }, ''); - - return `query { - ${query} - }`; - } - - buildQuery( - fieldsString: string, - options: WorkspaceQueryBuilderOptions, - data?: Record, - existingRecord?: Record, - index?: number, - ) { - const duplicateCondition = - this.duplicateService.buildDuplicateConditionForGraphQL( - options.objectMetadataItem, - data ?? existingRecord, - existingRecord?.id, - ); - - const filters = stringifyWithoutKeyQuote(duplicateCondition); - - return `${computeObjectTargetTable( - options.objectMetadataItem, - )}Collection${index}: ${computeObjectTargetTable( - options.objectMetadataItem, - )}Collection${ - isEmpty(duplicateCondition?.or) ? '(first: 0)' : `(filter: ${filters})` - } { - ${fieldsString} - } - `; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/find-many-query.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/find-many-query.factory.ts deleted file mode 100644 index f7bec67e371a..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/find-many-query.factory.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { - RecordFilter, - RecordOrderBy, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; -import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface'; -import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; - -import { ArgsStringFactory } from './args-string.factory'; -import { FieldsStringFactory } from './fields-string.factory'; - -@Injectable() -export class FindManyQueryFactory { - constructor( - private readonly fieldsStringFactory: FieldsStringFactory, - private readonly argsStringFactory: ArgsStringFactory, - ) {} - - async create< - Filter extends RecordFilter = RecordFilter, - OrderBy extends RecordOrderBy = RecordOrderBy, - >( - args: FindManyResolverArgs, - options: WorkspaceQueryBuilderOptions, - ) { - const fieldsString = await this.fieldsStringFactory.create( - options.info, - options.fieldMetadataCollection, - options.objectMetadataCollection, - ); - const argsString = this.argsStringFactory.create( - args, - options.fieldMetadataCollection, - !options.withSoftDeleted, - ); - - return ` - query { - ${computeObjectTargetTable(options.objectMetadataItem)}Collection${ - argsString ? `(${argsString})` : '' - } { - ${fieldsString} - } - } - `; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/find-one-query.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/find-one-query.factory.ts deleted file mode 100644 index 31f13dcbaf7d..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/find-one-query.factory.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; -import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface'; -import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; - -import { ArgsStringFactory } from './args-string.factory'; -import { FieldsStringFactory } from './fields-string.factory'; - -@Injectable() -export class FindOneQueryFactory { - constructor( - private readonly fieldsStringFactory: FieldsStringFactory, - private readonly argsStringFactory: ArgsStringFactory, - ) {} - - async create( - args: FindOneResolverArgs, - options: WorkspaceQueryBuilderOptions, - ) { - const fieldsString = await this.fieldsStringFactory.create( - options.info, - options.fieldMetadataCollection, - options.objectMetadataCollection, - options.withSoftDeleted, - ); - const argsString = this.argsStringFactory.create( - args, - options.fieldMetadataCollection, - !options.withSoftDeleted, - ); - - return ` - query { - ${computeObjectTargetTable(options.objectMetadataItem)}Collection${ - argsString ? `(${argsString})` : '' - } { - edges { - node { - ${fieldsString} - } - } - } - } - `; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/relation-field-alias.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/relation-field-alias.factory.ts deleted file mode 100644 index 36cae905aae1..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/relation-field-alias.factory.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { forwardRef, Inject, Injectable } from '@nestjs/common'; - -import { GraphQLResolveInfo } from 'graphql'; - -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; - -import { getFieldArgumentsByKey } from 'src/engine/api/graphql/workspace-query-builder/utils/get-field-arguments-by-key.util'; -import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { - deduceRelationDirection, - RelationDirection, -} from 'src/engine/utils/deduce-relation-direction.util'; -import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; - -import { ArgsStringFactory } from './args-string.factory'; -import { FieldsStringFactory } from './fields-string.factory'; - -@Injectable() -export class RelationFieldAliasFactory { - constructor( - @Inject(forwardRef(() => FieldsStringFactory)) - private readonly fieldsStringFactory: CircularDep, - private readonly argsStringFactory: ArgsStringFactory, - ) {} - - create( - fieldKey: string, - fieldValue: any, - fieldMetadata: FieldMetadataInterface, - objectMetadataCollection: ObjectMetadataInterface[], - info: GraphQLResolveInfo, - withSoftDeleted?: boolean, - ): Promise { - if (!isRelationFieldMetadataType(fieldMetadata.type)) { - throw new Error(`Field ${fieldMetadata.name} is not a relation field`); - } - - return this.createRelationAlias( - fieldKey, - fieldValue, - fieldMetadata, - objectMetadataCollection, - info, - withSoftDeleted, - ); - } - - private async createRelationAlias( - fieldKey: string, - fieldValue: any, - fieldMetadata: FieldMetadataInterface, - objectMetadataCollection: ObjectMetadataInterface[], - info: GraphQLResolveInfo, - withSoftDeleted?: boolean, - ): Promise { - const relationMetadata = - fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata; - - if (!relationMetadata) { - throw new Error( - `Relation metadata not found for field ${fieldMetadata.name}`, - ); - } - - if (!fieldMetadata.workspaceId) { - throw new Error( - `Workspace id not found for field ${fieldMetadata.name} in object metadata ${fieldMetadata.objectMetadataId}`, - ); - } - - const relationDirection = deduceRelationDirection( - fieldMetadata, - relationMetadata, - ); - // Retrieve the referenced object metadata based on the relation direction - // Mandatory to handle n+n relations - const referencedObjectMetadata = objectMetadataCollection.find( - (objectMetadata) => - objectMetadata.id === - (relationDirection == RelationDirection.TO - ? relationMetadata.fromObjectMetadataId - : relationMetadata.toObjectMetadataId), - ); - - if (!referencedObjectMetadata) { - throw new Error( - `Referenced object metadata not found for relation ${relationMetadata.id}`, - ); - } - - // If it's a relation destination is of kind MANY, we need to add the collection suffix and extract the args - if ( - relationMetadata.relationType === RelationMetadataType.ONE_TO_MANY && - relationDirection === RelationDirection.FROM - ) { - const args = getFieldArgumentsByKey(info, fieldKey); - - const argsString = this.argsStringFactory.create( - args, - referencedObjectMetadata.fields ?? [], - !withSoftDeleted, - ); - const fieldsString = - await this.fieldsStringFactory.createFieldsStringRecursive( - info, - fieldValue, - referencedObjectMetadata.fields ?? [], - objectMetadataCollection, - withSoftDeleted ?? false, - ); - - return ` - ${fieldKey}: ${computeObjectTargetTable( - referencedObjectMetadata, - )}Collection${argsString ? `(${argsString})` : ''} { - ${fieldsString} - } - `; - } - - let relationAlias = `${fieldKey}: ${computeColumnName(fieldMetadata)}`; - - // For one to one relations, pg_graphql use the target TableName on the side that is not storing the foreign key - // so we need to alias it to the field key - if ( - relationMetadata.relationType === RelationMetadataType.ONE_TO_ONE && - relationDirection === RelationDirection.FROM - ) { - relationAlias = `${fieldKey}: ${computeObjectTargetTable( - referencedObjectMetadata, - )}`; - } - const fieldsString = - await this.fieldsStringFactory.createFieldsStringRecursive( - info, - fieldValue, - referencedObjectMetadata.fields ?? [], - objectMetadataCollection, - withSoftDeleted ?? false, - ); - - // Otherwise it means it's a relation destination is of kind ONE - return ` - ${relationAlias} { - ${fieldsString} - } - `; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/update-many-query.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/update-many-query.factory.ts deleted file mode 100644 index b9e6bf5d4578..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/update-many-query.factory.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { - Record as IRecord, - RecordFilter, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; -import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface'; -import { UpdateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { ArgsAliasFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory'; -import { FieldsStringFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/fields-string.factory'; -import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; - -export interface UpdateManyQueryFactoryOptions - extends WorkspaceQueryBuilderOptions { - atMost?: number; -} - -@Injectable() -export class UpdateManyQueryFactory { - constructor( - private readonly fieldsStringFactory: FieldsStringFactory, - private readonly argsAliasFactory: ArgsAliasFactory, - ) {} - - async create< - Record extends IRecord = IRecord, - Filter extends RecordFilter = RecordFilter, - >( - args: UpdateManyResolverArgs, Filter>, - options: UpdateManyQueryFactoryOptions, - ) { - const fieldsString = await this.fieldsStringFactory.create( - options.info, - options.fieldMetadataCollection, - options.objectMetadataCollection, - ); - - const computedArgsData = this.argsAliasFactory.create( - args.data, - options.fieldMetadataCollection, - ); - - const argsData = { - ...computedArgsData, - updatedAt: new Date().toISOString(), - }; - - return ` - mutation { - update${computeObjectTargetTable(options.objectMetadataItem)}Collection( - set: ${stringifyWithoutKeyQuote(argsData)}, - filter: ${stringifyWithoutKeyQuote(args.filter)}, - atMost: ${options.atMost ?? 1}, - ) { - affectedCount - records { - ${fieldsString} - } - } - }`; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/update-one-query.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/update-one-query.factory.ts deleted file mode 100644 index e80697a74e82..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/update-one-query.factory.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; -import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface'; -import { UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; - -import { ArgsAliasFactory } from './args-alias.factory'; -import { FieldsStringFactory } from './fields-string.factory'; - -@Injectable() -export class UpdateOneQueryFactory { - constructor( - private readonly fieldsStringFactory: FieldsStringFactory, - private readonly argsAliasFactory: ArgsAliasFactory, - ) {} - - async create( - args: UpdateOneResolverArgs>, - options: WorkspaceQueryBuilderOptions, - ) { - const fieldsString = await this.fieldsStringFactory.create( - options.info, - options.fieldMetadataCollection, - options.objectMetadataCollection, - ); - - const computedArgsData = this.argsAliasFactory.create( - args.data, - options.fieldMetadataCollection, - ); - - const argsData = { - ...computedArgsData, - id: undefined, // do not allow updating an existing object's id - updatedAt: new Date().toISOString(), - }; - - return ` - mutation { - update${computeObjectTargetTable( - options.objectMetadataItem, - )}Collection(set: ${stringifyWithoutKeyQuote( - argsData, - )}, filter: { id: { eq: "${args.id}" } }) { - affectedCount - records { - ${fieldsString} - } - } - } - `; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/workspace-query-builder.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/workspace-query-builder.factory.ts deleted file mode 100644 index 1403edffc0a2..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/workspace-query-builder.factory.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface'; -import { - Record as IRecord, - RecordFilter, - RecordOrderBy, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; -import { - FindManyResolverArgs, - FindOneResolverArgs, - CreateManyResolverArgs, - UpdateOneResolverArgs, - DeleteOneResolverArgs, - UpdateManyResolverArgs, - DeleteManyResolverArgs, - FindDuplicatesResolverArgs, -} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { FindManyQueryFactory } from './factories/find-many-query.factory'; -import { FindOneQueryFactory } from './factories/find-one-query.factory'; -import { CreateManyQueryFactory } from './factories/create-many-query.factory'; -import { UpdateOneQueryFactory } from './factories/update-one-query.factory'; -import { DeleteOneQueryFactory } from './factories/delete-one-query.factory'; -import { - UpdateManyQueryFactory, - UpdateManyQueryFactoryOptions, -} from './factories/update-many-query.factory'; -import { - DeleteManyQueryFactory, - DeleteManyQueryFactoryOptions, -} from './factories/delete-many-query.factory'; -import { FindDuplicatesQueryFactory } from './factories/find-duplicates-query.factory'; - -@Injectable() -export class WorkspaceQueryBuilderFactory { - constructor( - private readonly findManyQueryFactory: FindManyQueryFactory, - private readonly findOneQueryFactory: FindOneQueryFactory, - private readonly findDuplicatesQueryFactory: FindDuplicatesQueryFactory, - private readonly createManyQueryFactory: CreateManyQueryFactory, - private readonly updateOneQueryFactory: UpdateOneQueryFactory, - private readonly deleteOneQueryFactory: DeleteOneQueryFactory, - private readonly updateManyQueryFactory: UpdateManyQueryFactory, - private readonly deleteManyQueryFactory: DeleteManyQueryFactory, - ) {} - - findMany< - Filter extends RecordFilter = RecordFilter, - OrderBy extends RecordOrderBy = RecordOrderBy, - >( - args: FindManyResolverArgs, - options: WorkspaceQueryBuilderOptions, - ): Promise { - return this.findManyQueryFactory.create(args, options); - } - - findOne( - args: FindOneResolverArgs, - options: WorkspaceQueryBuilderOptions, - ): Promise { - return this.findOneQueryFactory.create(args, options); - } - - findDuplicates( - args: FindDuplicatesResolverArgs, - options: WorkspaceQueryBuilderOptions, - existingRecords?: IRecord[], - ): Promise { - return this.findDuplicatesQueryFactory.create( - args, - options, - existingRecords, - ); - } - - createMany( - args: CreateManyResolverArgs>, - options: WorkspaceQueryBuilderOptions, - ): Promise { - return this.createManyQueryFactory.create(args, options); - } - - updateOne( - initialArgs: UpdateOneResolverArgs>, - options: WorkspaceQueryBuilderOptions, - ): Promise { - return this.updateOneQueryFactory.create(initialArgs, options); - } - - deleteOne( - args: DeleteOneResolverArgs, - options: WorkspaceQueryBuilderOptions, - ): Promise { - return this.deleteOneQueryFactory.create(args, options); - } - - updateMany< - Record extends IRecord = IRecord, - Filter extends RecordFilter = RecordFilter, - >( - args: UpdateManyResolverArgs, Filter>, - options: UpdateManyQueryFactoryOptions, - ): Promise { - return this.updateManyQueryFactory.create(args, options); - } - - deleteMany( - args: DeleteManyResolverArgs, - options: DeleteManyQueryFactoryOptions, - ): Promise { - return this.deleteManyQueryFactory.create(args, options); - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module.ts index 0db5486c63e7..4dbb652bdd5c 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module.ts @@ -1,21 +1,13 @@ import { Module } from '@nestjs/common'; -import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; -import { FieldsStringFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/fields-string.factory'; import { RecordPositionQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory'; -import { DuplicateModule } from 'src/engine/core-modules/duplicate/duplicate.module'; - -import { WorkspaceQueryBuilderFactory } from './workspace-query-builder.factory'; +import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { workspaceQueryBuilderFactories } from './factories/factories'; @Module({ - imports: [ObjectMetadataModule, DuplicateModule], - providers: [...workspaceQueryBuilderFactories, WorkspaceQueryBuilderFactory], - exports: [ - WorkspaceQueryBuilderFactory, - FieldsStringFactory, - RecordPositionQueryFactory, - ], + imports: [ObjectMetadataModule], + providers: [...workspaceQueryBuilderFactories], + exports: [RecordPositionQueryFactory], }) export class WorkspaceQueryBuilderModule {} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts index 1e166806be8d..8c348e726ff5 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts @@ -8,7 +8,6 @@ import { TelemetryListener } from 'src/engine/api/graphql/workspace-query-runner import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module'; import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; -import { DuplicateModule } from 'src/engine/core-modules/duplicate/duplicate.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; @@ -17,8 +16,6 @@ import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repos import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; -import { WorkspaceQueryRunnerService } from './workspace-query-runner.service'; - import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listener'; @Module({ @@ -31,17 +28,15 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), AnalyticsModule, TelemetryModule, - DuplicateModule, FileModule, FeatureFlagModule, ], providers: [ - WorkspaceQueryRunnerService, ...workspaceQueryRunnerFactories, EntityEventsToDbListener, TelemetryListener, RecordPositionBackfillCommand, ], - exports: [WorkspaceQueryRunnerService, ...workspaceQueryRunnerFactories], + exports: [...workspaceQueryRunnerFactories], }) export class WorkspaceQueryRunnerModule {} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts deleted file mode 100644 index 26526cf1fb39..000000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts +++ /dev/null @@ -1,942 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import isEmpty from 'lodash.isempty'; -import { DataSource, In } from 'typeorm'; - -import { - Record as IRecord, - 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 { - CreateManyResolverArgs, - CreateOneResolverArgs, - DeleteManyResolverArgs, - DeleteOneResolverArgs, - DestroyManyResolverArgs, - FindDuplicatesResolverArgs, - ResolverArgsType, - RestoreManyResolverArgs, - UpdateManyResolverArgs, - UpdateOneResolverArgs, -} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; - -import { WorkspaceQueryBuilderFactory } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.factory'; -import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory'; -import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory'; -import { - CallWebhookJobsJob, - CallWebhookJobsJobData, - CallWebhookJobsJobOperation, -} from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job'; -import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; -import { parseResult } from 'src/engine/api/graphql/workspace-query-runner/utils/parse-result.util'; -import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service'; -import { - WorkspaceQueryRunnerException, - WorkspaceQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; -import { DuplicateService } from 'src/engine/core-modules/duplicate/duplicate.service'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; -import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; -import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; -import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; -import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; -import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; -import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { isQueryTimeoutError } from 'src/engine/utils/query-timeout.util'; -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; -import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; -import { isDefined } from 'src/utils/is-defined'; - -import { - PGGraphQLMutation, - PGGraphQLResult, -} from './interfaces/pg-graphql.interface'; -import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface'; -import { - PgGraphQLConfig, - computePgGraphQLError, -} from './utils/compute-pg-graphql-error.util'; - -@Injectable() -export class WorkspaceQueryRunnerService { - private readonly logger = new Logger(WorkspaceQueryRunnerService.name); - - constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - private readonly workspaceQueryBuilderFactory: WorkspaceQueryBuilderFactory, - private readonly workspaceDataSourceService: WorkspaceDataSourceService, - private readonly queryRunnerArgsFactory: QueryRunnerArgsFactory, - private readonly queryResultGettersFactory: QueryResultGettersFactory, - @InjectMessageQueue(MessageQueue.webhookQueue) - private readonly messageQueueService: MessageQueueService, - private readonly workspaceEventEmitter: WorkspaceEventEmitter, - private readonly workspaceQueryHookService: WorkspaceQueryHookService, - private readonly environmentService: EnvironmentService, - private readonly duplicateService: DuplicateService, - ) {} - - async findDuplicates( - args: FindDuplicatesResolverArgs>, - options: WorkspaceQueryRunnerOptions, - ): Promise | undefined> { - if (!args.data && !args.ids) { - throw new WorkspaceQueryRunnerException( - 'You have to provide either "data" or "id" argument', - WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } - - if (!args.ids && isEmpty(args.data)) { - throw new WorkspaceQueryRunnerException( - 'The "data" condition can not be empty when ID input not provided', - WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } - - const { authContext, objectMetadataItem } = options; - - console.log( - `running findDuplicates for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`, - ); - - const hookedArgs = - await this.workspaceQueryHookService.executePreQueryHooks( - authContext, - objectMetadataItem.nameSingular, - 'findDuplicates', - args, - ); - - const computedArgs = (await this.queryRunnerArgsFactory.create( - hookedArgs, - options, - ResolverArgsType.FindDuplicates, - )) as FindDuplicatesResolverArgs; - - let existingRecords: IRecord[] | undefined = undefined; - - if (computedArgs.ids && computedArgs.ids.length > 0) { - existingRecords = await this.duplicateService.findExistingRecords( - computedArgs.ids, - objectMetadataItem, - authContext.workspace.id, - ); - - if (!existingRecords || existingRecords.length === 0) { - throw new WorkspaceQueryRunnerException( - `Object with id ${args.ids} not found`, - WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND, - ); - } - } - - const query = await this.workspaceQueryBuilderFactory.findDuplicates( - computedArgs, - options, - existingRecords, - ); - - const result = await this.execute(query, authContext.workspace.id); - - return this.parseResult>( - result, - objectMetadataItem, - '', - authContext.workspace.id, - true, - ); - } - - async createMany( - args: CreateManyResolverArgs>, - options: WorkspaceQueryRunnerOptions, - ): Promise { - const { authContext, objectMetadataItem } = options; - - assertMutationNotOnRemoteObject(objectMetadataItem); - - if (args.upsert) { - return await this.upsertMany(args, options); - } - - args.data.forEach((record) => { - if (record?.id) { - assertIsValidUuid(record.id); - } - }); - - const hookedArgs = - await this.workspaceQueryHookService.executePreQueryHooks( - authContext, - objectMetadataItem.nameSingular, - 'createMany', - args, - ); - - const computedArgs = (await this.queryRunnerArgsFactory.create( - hookedArgs, - options, - ResolverArgsType.CreateMany, - )) as CreateManyResolverArgs; - - const query = await this.workspaceQueryBuilderFactory.createMany( - computedArgs, - options, - ); - - const result = await this.execute(query, authContext.workspace.id); - - const parsedResults = ( - await this.parseResult>( - result, - objectMetadataItem, - 'insertInto', - authContext.workspace.id, - ) - )?.records; - - await this.workspaceQueryHookService.executePostQueryHooks( - authContext, - objectMetadataItem.nameSingular, - 'createMany', - parsedResults, - ); - - await this.triggerWebhooks( - parsedResults, - CallWebhookJobsJobOperation.create, - options, - ); - - this.workspaceEventEmitter.emit( - `${objectMetadataItem.nameSingular}.created`, - parsedResults.map( - (record) => - ({ - userId: authContext.user?.id, - recordId: record.id, - objectMetadata: objectMetadataItem, - properties: { - after: record, - }, - }) satisfies ObjectRecordCreateEvent, - ), - authContext.workspace.id, - ); - - return parsedResults; - } - - async upsertMany( - args: CreateManyResolverArgs>, - options: WorkspaceQueryRunnerOptions, - ): Promise { - console.log( - `running upsertMany for ${options.objectMetadataItem.nameSingular} on workspace ${options.authContext.workspace.id}`, - ); - const ids = args.data - .map((item) => item.id) - .filter((id) => id !== undefined); - - const existingRecords = - ids.length > 0 - ? await this.duplicateService.findExistingRecords( - ids as string[], - options.objectMetadataItem, - options.authContext.workspace.id, - ) - : []; - - const existingRecordsMap = new Map( - existingRecords.map((record) => [record.id, record]), - ); - - const results: Record[] = []; - const recordsToCreate: Partial[] = []; - - for (const payload of args.data) { - if (payload.id && existingRecordsMap.has(payload.id)) { - const result = await this.updateOne( - { id: payload.id, data: payload }, - options, - ); - - if (result) { - results.push(result); - } - } else { - recordsToCreate.push(payload); - } - } - - if (recordsToCreate.length > 0) { - const createResults = await this.createMany( - { data: recordsToCreate } as CreateManyResolverArgs>, - options, - ); - - if (createResults) { - results.push(...createResults); - } - } - - return results; - } - - async createOne( - args: CreateOneResolverArgs>, - options: WorkspaceQueryRunnerOptions, - ): Promise { - const results = await this.createMany( - { data: [args.data], upsert: args.upsert }, - options, - ); - - return results?.[0]; - } - - async updateOne( - args: UpdateOneResolverArgs>, - options: WorkspaceQueryRunnerOptions, - ): Promise { - const { authContext, objectMetadataItem } = options; - - console.log( - `running updateOne for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`, - ); - - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - authContext.workspace.id, - objectMetadataItem.nameSingular, - ); - - assertMutationNotOnRemoteObject(objectMetadataItem); - assertIsValidUuid(args.id); - - const existingRecord = await repository.findOne({ - where: { id: args.id }, - }); - - if (!existingRecord) { - throw new WorkspaceQueryRunnerException( - `Object with id ${args.id} not found`, - WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND, - ); - } - - const hookedArgs = - await this.workspaceQueryHookService.executePreQueryHooks( - authContext, - objectMetadataItem.nameSingular, - 'updateOne', - args, - ); - - const query = await this.workspaceQueryBuilderFactory.updateOne( - hookedArgs, - options, - ); - - const result = await this.execute(query, authContext.workspace.id); - - const parsedResults = ( - await this.parseResult>( - result, - objectMetadataItem, - 'update', - authContext.workspace.id, - ) - )?.records; - - await this.triggerWebhooks( - parsedResults, - CallWebhookJobsJobOperation.update, - options, - ); - - this.workspaceEventEmitter.emit( - `${objectMetadataItem.nameSingular}.updated`, - [ - { - userId: authContext.user?.id, - recordId: existingRecord.id, - objectMetadata: objectMetadataItem, - properties: { - updatedFields: Object.keys(args.data), - before: this.removeNestedProperties(existingRecord as Record), - after: this.removeNestedProperties(parsedResults?.[0]), - }, - } satisfies ObjectRecordUpdateEvent, - ], - authContext.workspace.id, - ); - - return parsedResults?.[0]; - } - - async updateMany( - args: UpdateManyResolverArgs>, - options: WorkspaceQueryRunnerOptions, - ): Promise { - const { authContext, objectMetadataItem } = options; - - console.log( - `running updateMany for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`, - ); - - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - authContext.workspace.id, - objectMetadataItem.nameSingular, - ); - - assertMutationNotOnRemoteObject(objectMetadataItem); - args.filter?.id?.in?.forEach((id) => assertIsValidUuid(id)); - - const existingRecords = await repository.find({ - where: { id: In(args.filter?.id?.in) }, - }); - const mappedRecords = new Map( - existingRecords.map((record) => [record.id, record]), - ); - const maximumRecordAffected = this.environmentService.get( - 'MUTATION_MAXIMUM_AFFECTED_RECORDS', - ); - - const hookedArgs = - await this.workspaceQueryHookService.executePreQueryHooks( - authContext, - objectMetadataItem.nameSingular, - 'updateMany', - args, - ); - - const query = await this.workspaceQueryBuilderFactory.updateMany( - hookedArgs, - { - ...options, - atMost: maximumRecordAffected, - }, - ); - - const result = await this.execute(query, authContext.workspace.id); - - const parsedResults = ( - await this.parseResult>( - result, - objectMetadataItem, - 'update', - authContext.workspace.id, - ) - )?.records; - - await this.triggerWebhooks( - parsedResults, - CallWebhookJobsJobOperation.update, - options, - ); - - const eventsToEmit: ObjectRecordUpdateEvent[] = parsedResults - .map((record) => { - const existingRecord = mappedRecords.get(record.id); - - if (!existingRecord) { - this.logger.warn( - `Record with id ${record.id} not found in the database`, - ); - - return; - } - - return { - userId: authContext.user?.id, - recordId: existingRecord.id, - objectMetadata: objectMetadataItem, - properties: { - updatedFields: Object.keys(args.data), - before: this.removeNestedProperties(existingRecord as Record), - after: this.removeNestedProperties(record), - }, - }; - }) - .filter(isDefined); - - this.workspaceEventEmitter.emit( - `${objectMetadataItem.nameSingular}.updated`, - eventsToEmit, - authContext.workspace.id, - ); - - return parsedResults; - } - - async deleteMany< - Record extends IRecord = IRecord, - Filter extends RecordFilter = RecordFilter, - >( - args: DeleteManyResolverArgs, - options: WorkspaceQueryRunnerOptions, - ): Promise { - const { authContext, objectMetadataItem } = options; - - console.log( - `running deleteMany for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`, - ); - - assertMutationNotOnRemoteObject(objectMetadataItem); - - const maximumRecordAffected = this.environmentService.get( - 'MUTATION_MAXIMUM_AFFECTED_RECORDS', - ); - - const hookedArgs = - await this.workspaceQueryHookService.executePreQueryHooks( - authContext, - objectMetadataItem.nameSingular, - 'deleteMany', - args, - ); - - const query = await this.workspaceQueryBuilderFactory.updateMany( - { - filter: hookedArgs.filter, - data: { - deletedAt: new Date().toISOString(), - }, - }, - { - ...options, - atMost: maximumRecordAffected, - }, - ); - - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - authContext.workspace.id, - objectMetadataItem.nameSingular, - ); - - const existingRecords = await repository.find({ - where: { id: In(args.filter?.id?.in) }, - }); - const mappedRecords = new Map( - existingRecords.map((record) => [record.id, record]), - ); - - const result = await this.execute(query, authContext.workspace.id); - - const parsedResults = ( - await this.parseResult>( - result, - objectMetadataItem, - 'update', - authContext.workspace.id, - ) - )?.records; - - await this.triggerWebhooks( - parsedResults, - CallWebhookJobsJobOperation.delete, - options, - ); - - this.workspaceEventEmitter.emit( - `${objectMetadataItem.nameSingular}.deleted`, - parsedResults.map((record) => { - const existingRecord = mappedRecords.get(record.id); - - return { - userId: authContext.user?.id, - recordId: record.id, - objectMetadata: objectMetadataItem, - properties: { - before: this.removeNestedProperties({ - ...existingRecord, - ...record, - }), - }, - } satisfies ObjectRecordDeleteEvent; - }), - authContext.workspace.id, - ); - - return parsedResults; - } - - async destroyMany< - Record extends IRecord = IRecord, - Filter extends RecordFilter = RecordFilter, - >( - args: DestroyManyResolverArgs, - options: WorkspaceQueryRunnerOptions, - ): Promise { - const { authContext, objectMetadataItem } = options; - - console.log( - `running destroyMany for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`, - ); - - assertMutationNotOnRemoteObject(objectMetadataItem); - - const maximumRecordAffected = this.environmentService.get( - 'MUTATION_MAXIMUM_AFFECTED_RECORDS', - ); - - const hookedArgs = - await this.workspaceQueryHookService.executePreQueryHooks( - authContext, - objectMetadataItem.nameSingular, - 'destroyMany', - args, - ); - - const query = await this.workspaceQueryBuilderFactory.deleteMany( - { - filter: { - ...hookedArgs.filter, - deletedAt: { is: 'NOT_NULL' }, - }, - }, - { - ...options, - atMost: maximumRecordAffected, - }, - ); - - const result = await this.execute(query, authContext.workspace.id); - - const parsedResults = ( - await this.parseResult>( - result, - objectMetadataItem, - 'deleteFrom', - authContext.workspace.id, - ) - )?.records; - - await this.triggerWebhooks( - parsedResults, - CallWebhookJobsJobOperation.delete, - options, - ); - - return parsedResults; - } - - async restoreMany< - Record extends IRecord = IRecord, - Filter extends RecordFilter = RecordFilter, - >( - args: RestoreManyResolverArgs, - options: WorkspaceQueryRunnerOptions, - ): Promise { - const { authContext, objectMetadataItem } = options; - - console.log( - `running restoreMany for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`, - ); - - assertMutationNotOnRemoteObject(objectMetadataItem); - - const maximumRecordAffected = this.environmentService.get( - 'MUTATION_MAXIMUM_AFFECTED_RECORDS', - ); - - const hookedArgs = - await this.workspaceQueryHookService.executePreQueryHooks( - authContext, - objectMetadataItem.nameSingular, - 'restoreMany', - args, - ); - - const query = await this.workspaceQueryBuilderFactory.updateMany( - { - filter: { - ...hookedArgs.filter, - deletedAt: { is: 'NOT_NULL' }, - }, - data: { - deletedAt: null, - }, - }, - { - ...options, - atMost: maximumRecordAffected, - }, - ); - - const result = await this.execute(query, authContext.workspace.id); - - const parsedResults = ( - await this.parseResult>( - result, - objectMetadataItem, - 'update', - authContext.workspace.id, - ) - )?.records; - - await this.triggerWebhooks( - parsedResults, - CallWebhookJobsJobOperation.create, - options, - ); - - this.workspaceEventEmitter.emit( - `${objectMetadataItem.nameSingular}.created`, - parsedResults.map( - (record) => - ({ - userId: authContext.user?.id, - recordId: record.id, - objectMetadata: objectMetadataItem, - properties: { - after: this.removeNestedProperties(record), - }, - }) satisfies ObjectRecordCreateEvent, - ), - authContext.workspace.id, - ); - - return parsedResults; - } - - async deleteOne( - args: DeleteOneResolverArgs, - options: WorkspaceQueryRunnerOptions, - ): Promise { - const { authContext, objectMetadataItem } = options; - - console.log( - `running deleteOne for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`, - ); - - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - authContext.workspace.id, - objectMetadataItem.nameSingular, - ); - - assertMutationNotOnRemoteObject(objectMetadataItem); - assertIsValidUuid(args.id); - - const hookedArgs = - await this.workspaceQueryHookService.executePreQueryHooks( - authContext, - objectMetadataItem.nameSingular, - 'deleteOne', - args, - ); - - const query = await this.workspaceQueryBuilderFactory.updateOne( - { - id: hookedArgs.id, - data: { - deletedAt: new Date().toISOString(), - }, - }, - options, - ); - - const existingRecord = await repository.findOne({ - where: { id: args.id }, - }); - - const result = await this.execute(query, authContext.workspace.id); - - const parsedResults = ( - await this.parseResult>( - result, - objectMetadataItem, - 'update', - authContext.workspace.id, - ) - )?.records; - - await this.triggerWebhooks( - parsedResults, - CallWebhookJobsJobOperation.delete, - options, - ); - - this.workspaceEventEmitter.emit( - `${objectMetadataItem.nameSingular}.deleted`, - [ - { - userId: authContext.user?.id, - recordId: args.id, - objectMetadata: objectMetadataItem, - properties: { - before: { - ...(existingRecord ?? {}), - ...this.removeNestedProperties(parsedResults?.[0]), - }, - }, - } satisfies ObjectRecordDeleteEvent, - ], - authContext.workspace.id, - ); - - return parsedResults?.[0]; - } - - private removeNestedProperties( - record: Record, - ) { - if (!record) { - return; - } - - const sanitizedRecord = {}; - - for (const [key, value] of Object.entries(record)) { - if (value && typeof value === 'object' && value['edges']) { - continue; - } - - if (key === '__typename') { - continue; - } - - sanitizedRecord[key] = value; - } - - return sanitizedRecord; - } - - async executeSQL( - workspaceDataSource: DataSource, - workspaceId: string, - sqlQuery: string, - parameters?: any[], - ) { - try { - return await workspaceDataSource?.transaction( - async (transactionManager) => { - await transactionManager.query(` - SET LOCAL search_path TO ${this.workspaceDataSourceService.getSchemaName( - workspaceId, - )}; - `); - - const results = transactionManager.query(sqlQuery, parameters); - - return results; - }, - ); - } catch (error) { - if (isQueryTimeoutError(error)) { - throw new WorkspaceQueryRunnerException( - 'The SQL request took too long to process, resulting in a query read timeout. To resolve this issue, consider modifying your query by reducing the depth of relationships or limiting the number of records being fetched.', - WorkspaceQueryRunnerExceptionCode.QUERY_TIMEOUT, - ); - } - - throw error; - } - } - - async execute( - query: string, - workspaceId: string, - ): Promise { - const workspaceDataSource = - await this.workspaceDataSourceService.connectToWorkspaceDataSource( - workspaceId, - ); - - return this.executeSQL( - workspaceDataSource, - workspaceId, - `SELECT graphql.resolve($1);`, - [query], - ); - } - - private async parseResult( - graphqlResult: PGGraphQLResult | undefined, - objectMetadataItem: ObjectMetadataInterface, - command: string, - workspaceId: string, - isMultiQuery = false, - ): Promise { - const entityKey = `${command}${computeObjectTargetTable( - objectMetadataItem, - )}Collection`; - const result = !isMultiQuery - ? graphqlResult?.[0]?.resolve?.data?.[entityKey] - : Object.keys(graphqlResult?.[0]?.resolve?.data).reduce( - (acc: IRecord[], dataItem, index) => { - acc.push(graphqlResult?.[0]?.resolve?.data[`${entityKey}${index}`]); - - return acc; - }, - [], - ); - const errors = graphqlResult?.[0]?.resolve?.errors; - - if ( - result && - ['update', 'deleteFrom'].includes(command) && - !result.affectedCount - ) { - throw new WorkspaceQueryRunnerException( - 'No rows were affected.', - WorkspaceQueryRunnerExceptionCode.NO_ROWS_AFFECTED, - ); - } - - if (errors && errors.length > 0) { - const error = computePgGraphQLError( - command, - objectMetadataItem.nameSingular, - errors, - { - atMost: this.environmentService.get( - 'MUTATION_MAXIMUM_AFFECTED_RECORDS', - ), - } satisfies PgGraphQLConfig, - ); - - throw error; - } - - const resultWithGetters = await this.queryResultGettersFactory.create( - result, - objectMetadataItem, - workspaceId, - ); - - return parseResult(resultWithGetters); - } - - async triggerWebhooks( - jobsData: Record[] | undefined, - operation: CallWebhookJobsJobOperation, - options: WorkspaceQueryRunnerOptions, - ) { - if (!Array.isArray(jobsData)) { - return; - } - jobsData.forEach((jobData) => { - this.messageQueueService.add( - CallWebhookJobsJob.name, - { - record: jobData, - workspaceId: options.authContext.workspace.id, - operation, - objectMetadataItem: options.objectMetadataItem, - }, - { retryLimit: 3 }, - ); - }); - } -} diff --git a/packages/twenty-server/src/engine/core-modules/ai-sql-query/ai-sql-query.module.ts b/packages/twenty-server/src/engine/core-modules/ai-sql-query/ai-sql-query.module.ts deleted file mode 100644 index 0e0e9997c192..000000000000 --- a/packages/twenty-server/src/engine/core-modules/ai-sql-query/ai-sql-query.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; -import { UserModule } from 'src/engine/core-modules/user/user.module'; -import { AISQLQueryResolver } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.resolver'; -import { AISQLQueryService } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.service'; -import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; -import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module'; -import { LLMChatModelModule } from 'src/engine/core-modules/llm-chat-model/llm-chat-model.module'; -import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module'; -import { LLMTracingModule } from 'src/engine/core-modules/llm-tracing/llm-tracing.module'; -import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; -import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module'; -@Module({ - imports: [ - WorkspaceDataSourceModule, - WorkspaceQueryRunnerModule, - UserModule, - TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), - LLMChatModelModule, - LLMTracingModule, - EnvironmentModule, - ObjectMetadataModule, - WorkspaceSyncMetadataModule, - ], - exports: [], - providers: [AISQLQueryResolver, AISQLQueryService], -}) -export class AISQLQueryModule {} diff --git a/packages/twenty-server/src/engine/core-modules/ai-sql-query/ai-sql-query.prompt-templates.ts b/packages/twenty-server/src/engine/core-modules/ai-sql-query/ai-sql-query.prompt-templates.ts deleted file mode 100644 index 3b100c0b7711..000000000000 --- a/packages/twenty-server/src/engine/core-modules/ai-sql-query/ai-sql-query.prompt-templates.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PromptTemplate } from '@langchain/core/prompts'; - -export const sqlGenerationPromptTemplate = PromptTemplate.fromTemplate<{ - llmOutputJsonSchema: string; - sqlCreateTableStatements: string; - userQuestion: string; -}>(`Always respond following this JSON Schema: {llmOutputJsonSchema} - -Based on the table schema below, write a PostgreSQL query that would answer the user's question. All column names must be enclosed in double quotes. - -{sqlCreateTableStatements} - -Question: {userQuestion} -SQL Query:`); diff --git a/packages/twenty-server/src/engine/core-modules/ai-sql-query/ai-sql-query.resolver.ts b/packages/twenty-server/src/engine/core-modules/ai-sql-query/ai-sql-query.resolver.ts deleted file mode 100644 index c739c7e3ba06..000000000000 --- a/packages/twenty-server/src/engine/core-modules/ai-sql-query/ai-sql-query.resolver.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { ForbiddenException, UseGuards } from '@nestjs/common'; -import { Args, ArgsType, Field, Query, Resolver } from '@nestjs/graphql'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { Repository } from 'typeorm'; - -import { AISQLQueryService } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.service'; -import { AISQLQueryResult } from 'src/engine/core-modules/ai-sql-query/dtos/ai-sql-query-result.dto'; -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 { User } from 'src/engine/core-modules/user/user.entity'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; -import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; -import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; -import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; - -@ArgsType() -class GetAISQLQueryArgs { - @Field(() => String) - text: string; -} - -@UseGuards(WorkspaceAuthGuard, UserAuthGuard) -@Resolver(() => AISQLQueryResult) -export class AISQLQueryResolver { - constructor( - private readonly aiSqlQueryService: AISQLQueryService, - @InjectRepository(FeatureFlagEntity, 'core') - private readonly featureFlagRepository: Repository, - ) {} - - @Query(() => AISQLQueryResult) - async getAISQLQuery( - @AuthWorkspace() { id: workspaceId }: Workspace, - @AuthUser() user: User, - @Args() { text }: GetAISQLQueryArgs, - ) { - const isCopilotEnabledFeatureFlag = - await this.featureFlagRepository.findOneBy({ - workspaceId, - key: FeatureFlagKey.IsCopilotEnabled, - value: true, - }); - - if (!isCopilotEnabledFeatureFlag?.value) { - throw new ForbiddenException( - `${FeatureFlagKey.IsCopilotEnabled} feature flag is disabled`, - ); - } - - const traceMetadata = { - userId: user.id, - userEmail: user.email, - }; - - return this.aiSqlQueryService.generateAndExecute( - workspaceId, - text, - traceMetadata, - ); - } -} diff --git a/packages/twenty-server/src/engine/core-modules/ai-sql-query/ai-sql-query.service.ts b/packages/twenty-server/src/engine/core-modules/ai-sql-query/ai-sql-query.service.ts deleted file mode 100644 index 81cd32ec4a35..000000000000 --- a/packages/twenty-server/src/engine/core-modules/ai-sql-query/ai-sql-query.service.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { StructuredOutputParser } from '@langchain/core/output_parsers'; -import { RunnableSequence } from '@langchain/core/runnables'; -import groupBy from 'lodash.groupby'; -import { DataSource, QueryFailedError } from 'typeorm'; -import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; -import { z } from 'zod'; -import { zodToJsonSchema } from 'zod-to-json-schema'; - -import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; -import { sqlGenerationPromptTemplate } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.prompt-templates'; -import { AISQLQueryResult } from 'src/engine/core-modules/ai-sql-query/dtos/ai-sql-query-result.dto'; -import { LLMChatModelService } from 'src/engine/core-modules/llm-chat-model/llm-chat-model.service'; -import { LLMTracingService } from 'src/engine/core-modules/llm-tracing/llm-tracing.service'; -import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; -import { StandardObjectFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-object.factory'; - -@Injectable() -export class AISQLQueryService { - private readonly logger = new Logger(AISQLQueryService.name); - constructor( - private readonly workspaceDataSourceService: WorkspaceDataSourceService, - private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService, - private readonly llmChatModelService: LLMChatModelService, - private readonly llmTracingService: LLMTracingService, - private readonly standardObjectFactory: StandardObjectFactory, - ) {} - - private getLabelIdentifierName( - objectMetadata: ObjectMetadataEntity, - _dataSourceId, - _workspaceId, - _workspaceFeatureFlagsMap, - ): string | undefined { - const customObjectLabelIdentifierFieldMetadata = objectMetadata.fields.find( - (fieldMetadata) => - fieldMetadata.id === objectMetadata.labelIdentifierFieldMetadataId, - ); - - /* const standardObjectMetadataCollection = this.standardObjectFactory.create( - standardObjectMetadataDefinitions, - { workspaceId, dataSourceId }, - workspaceFeatureFlagsMap, - ); - - const standardObjectLabelIdentifierFieldMetadata = - standardObjectMetadataCollection - .find( - (standardObjectMetadata) => - standardObjectMetadata.nameSingular === objectMetadata.nameSingular, - ) - ?.fields.find( - (field: PartialFieldMetadata) => - field.name === DEFAULT_LABEL_IDENTIFIER_FIELD_NAME, - ) as PartialFieldMetadata; */ - - const labelIdentifierFieldMetadata = - customObjectLabelIdentifierFieldMetadata; /*?? - standardObjectLabelIdentifierFieldMetadata*/ - - return ( - labelIdentifierFieldMetadata?.name ?? DEFAULT_LABEL_IDENTIFIER_FIELD_NAME - ); - } - - private async getColInfosByTableName(dataSource: DataSource) { - const { schema } = dataSource.options as PostgresConnectionOptions; - - // From LangChain sql_utils.ts - const sqlQuery = `SELECT - t.table_name, - c.* - FROM - information_schema.tables t - JOIN information_schema.columns c - ON t.table_name = c.table_name - WHERE - t.table_schema = '${schema}' - AND c.table_schema = '${schema}' - ORDER BY - t.table_name, - c.ordinal_position;`; - const colInfos = await dataSource.query< - { - table_name: string; - column_name: string; - data_type: string | undefined; - is_nullable: 'YES' | 'NO'; - }[] - >(sqlQuery); - - return groupBy(colInfos, (colInfo) => colInfo.table_name); - } - - private getCreateTableStatement(tableName: string, colInfos: any[]) { - return `CREATE TABLE ${tableName} (\n ${colInfos - .map( - (colInfo) => - `${colInfo.column_name} ${colInfo.data_type} ${ - colInfo.is_nullable === 'YES' ? '' : 'NOT NULL' - }`, - ) - .join(', ')});`; - } - - private getRelationDescriptions() { - // TODO - Construct sentences like the following: - // investorId: a foreign key referencing the person table, indicating the investor who owns this portfolio company. - return ''; - } - - private getTableDescription(tableName: string, colInfos: any[]) { - return [ - this.getCreateTableStatement(tableName, colInfos), - this.getRelationDescriptions(), - ].join('\n'); - } - - private async getWorkspaceSchemaDescription( - dataSource: DataSource, - ): Promise { - const colInfoByTableName = await this.getColInfosByTableName(dataSource); - - return Object.entries(colInfoByTableName) - .map(([tableName, colInfos]) => - this.getTableDescription(tableName, colInfos), - ) - .join('\n\n'); - } - - private async generateWithDataSource( - workspaceId: string, - workspaceDataSource: DataSource, - userQuestion: string, - traceMetadata: Record = {}, - ) { - const workspaceSchemaName = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - workspaceDataSource.setOptions({ - schema: workspaceSchemaName, - }); - - const workspaceSchemaDescription = - await this.getWorkspaceSchemaDescription(workspaceDataSource); - - const llmOutputSchema = z.object({ - sqlQuery: z.string(), - }); - - const llmOutputJsonSchema = JSON.stringify( - zodToJsonSchema(llmOutputSchema), - ); - - const structuredOutputParser = - StructuredOutputParser.fromZodSchema(llmOutputSchema); - - const sqlQueryGeneratorChain = RunnableSequence.from([ - sqlGenerationPromptTemplate, - this.llmChatModelService.getJSONChatModel(), - structuredOutputParser, - ]); - - const metadata = { - workspaceId, - ...traceMetadata, - }; - const tracingCallbackHandler = - this.llmTracingService.getCallbackHandler(metadata); - - const { sqlQuery } = await sqlQueryGeneratorChain.invoke( - { - llmOutputJsonSchema, - sqlCreateTableStatements: workspaceSchemaDescription, - userQuestion, - }, - { - callbacks: [tracingCallbackHandler], - }, - ); - - return sqlQuery; - } - - async generate( - workspaceId: string, - userQuestion: string, - traceMetadata: Record = {}, - ) { - const workspaceDataSource = - await this.workspaceDataSourceService.connectToWorkspaceDataSource( - workspaceId, - ); - - return this.generateWithDataSource( - workspaceId, - workspaceDataSource, - userQuestion, - traceMetadata, - ); - } - - async generateAndExecute( - workspaceId: string, - userQuestion: string, - traceMetadata: Record = {}, - ): Promise { - const workspaceDataSource = - await this.workspaceDataSourceService.connectToWorkspaceDataSource( - workspaceId, - ); - - const sqlQuery = await this.generateWithDataSource( - workspaceId, - workspaceDataSource, - userQuestion, - traceMetadata, - ); - - try { - const sqlQueryResult: Record[] = - await this.workspaceQueryRunnerService.executeSQL( - workspaceDataSource, - workspaceId, - sqlQuery, - ); - - return { - sqlQuery, - sqlQueryResult: JSON.stringify(sqlQueryResult), - }; - } catch (error) { - if (error instanceof QueryFailedError) { - return { - sqlQuery, - queryFailedErrorMessage: error.message, - }; - } - - this.logger.error(error.message, error.stack); - - return { - sqlQuery, - }; - } - } -} diff --git a/packages/twenty-server/src/engine/core-modules/ai-sql-query/dtos/ai-sql-query-result.dto.ts b/packages/twenty-server/src/engine/core-modules/ai-sql-query/dtos/ai-sql-query-result.dto.ts deleted file mode 100644 index 1046631f3253..000000000000 --- a/packages/twenty-server/src/engine/core-modules/ai-sql-query/dtos/ai-sql-query-result.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Field, ObjectType } from '@nestjs/graphql'; - -import { IsOptional } from 'class-validator'; - -@ObjectType('AISQLQueryResult') -export class AISQLQueryResult { - @Field(() => String) - sqlQuery: string; - - @Field(() => String, { nullable: true }) - @IsOptional() - sqlQueryResult?: string; - - @Field(() => String, { nullable: true }) - @IsOptional() - queryFailedErrorMessage?: string; -} diff --git a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts index 2e2df06c4d8e..af651c18c5d2 100644 --- a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts +++ b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts @@ -3,7 +3,6 @@ import { HttpAdapterHost } from '@nestjs/core'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { ActorModule } from 'src/engine/core-modules/actor/actor.module'; -import { AISQLQueryModule } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.module'; import { AppTokenModule } from 'src/engine/core-modules/app-token/app-token.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; @@ -62,7 +61,6 @@ import { FileModule } from './file/file.module'; UserModule, WorkspaceModule, WorkspaceInvitationModule, - AISQLQueryModule, PostgresCredentialsModule, WorkflowTriggerApiModule, WorkspaceEventEmitterModule, diff --git a/packages/twenty-server/src/engine/core-modules/duplicate/duplicate.module.ts b/packages/twenty-server/src/engine/core-modules/duplicate/duplicate.module.ts deleted file mode 100644 index c32a4fa598cf..000000000000 --- a/packages/twenty-server/src/engine/core-modules/duplicate/duplicate.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { DuplicateService } from 'src/engine/core-modules/duplicate/duplicate.service'; -import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; - -@Module({ - imports: [WorkspaceDataSourceModule], - exports: [DuplicateService], - providers: [DuplicateService], -}) -export class DuplicateModule {} diff --git a/packages/twenty-server/src/engine/core-modules/duplicate/duplicate.service.ts b/packages/twenty-server/src/engine/core-modules/duplicate/duplicate.service.ts deleted file mode 100644 index 48d021b5e689..000000000000 --- a/packages/twenty-server/src/engine/core-modules/duplicate/duplicate.service.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { - Record as IRecord, - Record, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; - -import { settings } from 'src/engine/constants/settings'; -import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; - -@Injectable() -export class DuplicateService { - constructor( - private readonly workspaceDataSourceService: WorkspaceDataSourceService, - ) {} - - async findExistingRecords( - recordIds: (string | number)[], - objectMetadata: ObjectMetadataInterface, - workspaceId: string, - ) { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const results = await this.workspaceDataSourceService.executeRawQuery( - ` - SELECT - * - FROM - ${dataSourceSchema}."${computeObjectTargetTable( - objectMetadata, - )}" p - WHERE - p."id" IN (${recordIds - .map((_, index) => `$${index + 1}`) - .join(', ')}) - `, - recordIds, - workspaceId, - ); - - return results as IRecord[]; - } - - buildDuplicateConditionForGraphQL( - objectMetadata: ObjectMetadataInterface, - argsData?: Partial, - filteringByExistingRecordId?: string, - ) { - if (!argsData) { - return; - } - - const criteriaCollection = - this.getApplicableDuplicateCriteriaCollection(objectMetadata); - - const criteriaWithMatchingArgs = criteriaCollection.filter((criteria) => - criteria.columnNames.every((columnName) => { - const value = argsData[columnName] as string | undefined; - - return ( - !!value && value.length >= settings.minLengthOfStringForDuplicateCheck - ); - }), - ); - - const filterCriteria = criteriaWithMatchingArgs.map((criteria) => - Object.fromEntries( - criteria.columnNames.map((columnName) => [ - columnName, - { eq: argsData[columnName] }, - ]), - ), - ); - - return { - // when filtering by an existing record, we need to filter that explicit record out - ...(filteringByExistingRecordId && { - id: { neq: filteringByExistingRecordId }, - }), - // keep condition as "or" to get results by more duplicate criteria - or: filterCriteria, - }; - } - - private getApplicableDuplicateCriteriaCollection( - objectMetadataItem: ObjectMetadataInterface, - ) { - return DUPLICATE_CRITERIA_COLLECTION.filter( - (duplicateCriteria) => - duplicateCriteria.objectName === objectMetadataItem.nameSingular, - ); - } -} 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/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..1d75cbd7ad3f 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 @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { isDefined } from 'class-validator'; -import { Repository } from 'typeorm'; +import { InsertResult, Repository } from 'typeorm'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { @@ -45,32 +45,37 @@ export class IndexMetadataService { const indexName = `IDX_${generateDeterministicIndexName([tableName, ...columnNames])}`; - let savedIndexMetadata: IndexMetadataEntity; + let result: InsertResult; try { - savedIndexMetadata = await this.indexMetadataRepository.save({ - name: indexName, - tableName, - indexFieldMetadatas: fieldMetadataToIndex.map( - (fieldMetadata, index) => { - return { - fieldMetadataId: fieldMetadata.id, - order: index, - }; - }, - ), - workspaceId, - objectMetadataId: objectMetadata.id, - ...(isDefined(indexType) ? { indexType: indexType } : {}), - isCustom: isCustom, - }); + result = await this.indexMetadataRepository.upsert( + { + name: indexName, + indexFieldMetadatas: fieldMetadataToIndex.map( + (fieldMetadata, index) => { + return { + fieldMetadataId: fieldMetadata.id, + order: index, + }; + }, + ), + workspaceId, + objectMetadataId: objectMetadata.id, + ...(isDefined(indexType) ? { indexType: indexType } : {}), + isCustom: isCustom, + }, + { + conflictPaths: ['workspaceId', 'name', 'objectMetadataId'], + skipUpdateIfNoValuesChanged: true, + }, + ); } catch (error) { throw new Error( `Failed to create index ${indexName} on object metadata ${objectMetadata.nameSingular}`, ); } - if (!savedIndexMetadata) { + if (!result.identifiers.length) { throw new Error( `Failed to return saved index ${indexName} on object metadata ${objectMetadata.nameSingular}`, ); 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/search/search.module.ts b/packages/twenty-server/src/engine/metadata-modules/search/search.module.ts new file mode 100644 index 000000000000..2260d72181f5 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/search/search.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; + +import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; + +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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { SearchService } from 'src/engine/metadata-modules/search/search.service'; +import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; + +@Module({ + imports: [ + NestjsQueryTypeOrmModule.forFeature( + [ObjectMetadataEntity, FieldMetadataEntity], + 'metadata', + ), + IndexMetadataModule, + WorkspaceMigrationModule, + ], + providers: [SearchService], + exports: [SearchService], +}) +export class SearchModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/search/search.service.ts b/packages/twenty-server/src/engine/metadata-modules/search/search.service.ts new file mode 100644 index 000000000000..b24ad3816a99 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/search/search.service.ts @@ -0,0 +1,169 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + +import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; +import { + FieldMetadataEntity, + FieldMetadataType, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +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 { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; +import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +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, + WorkspaceMigrationTableActionType, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; +import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; +import { computeTableName } from 'src/engine/utils/compute-table-name.util'; +import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { + FieldTypeAndNameMetadata, + getTsVectorColumnExpressionFromFields, +} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; +import { SearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util'; +import { isDefined } from 'src/utils/is-defined'; + +@Injectable() +export class SearchService { + constructor( + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + 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.createIndex( + 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.createIndex( + workspaceId, + objectMetadata, + [existingSearchVectorFieldMetadata], + false, + false, + IndexType.GIN, + ); + } +} 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-manager.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts index 5532ef5ed193..7db0e69aac0b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts @@ -39,8 +39,6 @@ export class WorkspaceManagerService { schemaName, ); - await this.setWorkspaceMaxRow(workspaceId, schemaName); - await this.workspaceSyncMetadataService.synchronize({ workspaceId, dataSourceId: dataSourceMetadata.id, @@ -69,8 +67,6 @@ export class WorkspaceManagerService { schemaName, ); - await this.setWorkspaceMaxRow(workspaceId, schemaName); - await this.workspaceSyncMetadataService.synchronize({ workspaceId, dataSourceId: dataSourceMetadata.id, @@ -79,24 +75,6 @@ export class WorkspaceManagerService { await this.prefillWorkspaceWithDemoObjects(dataSourceMetadata, workspaceId); } - /** - * - * We are updating the pg_graphql max_rows from 30 (default value) to 60 - * - * @params workspaceId, schemaName - * @param workspaceId - */ - private async setWorkspaceMaxRow(workspaceId, schemaName) { - const workspaceDataSource = - await this.workspaceDataSourceService.connectToWorkspaceDataSource( - workspaceId, - ); - - await workspaceDataSource.query( - `comment on schema ${schemaName} is e'@graphql({"max_rows": 60})'`, - ); - } - /** * * We are prefilling a few standard objects with data to make it easier for the user to get started. 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 df85336609db..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 @@ -271,11 +271,6 @@ export class WorkspaceMigrationRunnerService { columns, ); } - - // Enable totalCount for the table - await queryRunner.query(` - COMMENT ON TABLE "${schemaName}"."${tableName}" IS '@graphql({"totalCount": {"enabled": true}})'; - `); } /** @@ -407,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, }), @@ -474,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/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 32d9dd50f531..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 @@ -27,7 +27,10 @@ import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace- 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'; @@ -43,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', @@ -298,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-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..283aba5a9614 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 ``` 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