diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 97705b937c..6947f8aa34 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 9e930133b3..5950c81d5c 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/modules/analytics/graphql/queries/track.ts b/packages/twenty-front/src/modules/analytics/graphql/queries/track.ts index 04a5d44872..44828ddaef 100644 --- a/packages/twenty-front/src/modules/analytics/graphql/queries/track.ts +++ b/packages/twenty-front/src/modules/analytics/graphql/queries/track.ts @@ -1,8 +1,8 @@ import { gql } from '@apollo/client'; export const TRACK = 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/apollo/services/__tests__/apollo.factory.test.ts b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts index 9136b83fcd..c50e93b46f 100644 --- a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts +++ b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts @@ -41,8 +41,8 @@ const makeRequest = async () => { 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/search/queries/getTextToSQL.ts b/packages/twenty-front/src/modules/search/queries/getTextToSQL.ts deleted file mode 100644 index 6758209824..0000000000 --- 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-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 f4be0b8fb2..0000000000 --- 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 f5ccbf9751..0000000000 --- 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 5755fe4134..0000000000 --- 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 85dac955cd..0000000000 --- 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 f24090c665..0000000000 --- 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 f4e4e948a6..0000000000 --- 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 58c97cd267..ca9778a33f 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 e50028109e..0000000000 --- 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 3b5c73d819..0000000000 --- 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 2d21b6aadc..0000000000 --- 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 f7bec67e37..0000000000 --- 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 31f13dcbaf..0000000000 --- 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 36cae905aa..0000000000 --- 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 b9e6bf5d45..0000000000 --- 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 e80697a74e..0000000000 --- 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 1403edffc0..0000000000 --- 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 0db5486c63..4dbb652bdd 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 1e166806be..8c348e726f 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 26526cf1fb..0000000000 --- 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 0e0e9997c1..0000000000 --- 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 3b100c0b77..0000000000 --- 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 c739c7e3ba..0000000000 --- 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 81cd32ec4a..0000000000 --- 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 1046631f32..0000000000 --- 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 2e2df06c4d..af651c18c5 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 c32a4fa598..0000000000 --- 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 48d021b5e6..0000000000 --- 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/workspace-manager/workspace-manager.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts index 5532ef5ed1..7db0e69aac 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 df85336609..878a25ed57 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}})'; - `); } /**