From 1d351a29b853e6f5f72046a2b5b3f9558a2c3ae5 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Fri, 29 Mar 2024 18:23:58 +0100 Subject: [PATCH] Sync remote object (#4713) * Sync objects * Generate data for isRemote * Add cache version update * Add label identifier + fix field metadata input --------- Co-authored-by: Thomas Trompette --- .../src/generated-metadata/gql.ts | 4 +- .../src/generated-metadata/graphql.ts | 21 +- .../object-metadata/graphql/queries.ts | 1 + .../useFindManyObjectMetadataItems.ts | 1 + .../utils/getObjectMetadataItemsMock.ts | 7 + .../objectMetadataItemSchema.ts | 1 + .../hooks/useLoadRecordIndexTable.ts | 4 + .../src/testing/mock-data/metadata.ts | 2 + .../testing/mock-data/objectMetadataItems.ts | 1 + .../field-metadata/dtos/create-field.input.ts | 6 +- .../field-metadata/field-metadata.service.ts | 39 +- .../field-metadata/utils/generate-nullable.ts | 5 + .../dtos/create-object.input.ts | 13 +- .../object-metadata.service.ts | 578 +++++++++--------- .../remote-table/dtos/remote-table-input.ts | 21 + .../remote-postgres-table.module.ts | 11 + .../remote-postgres-table.service.ts | 106 ++++ .../utils/remote-postgres-table.util.ts | 65 ++ .../remote-table/remote-table.module.ts | 12 + .../remote-table/remote-table.resolver.ts | 14 +- .../remote-table/remote-table.service.ts | 219 +++++-- .../utils/remote-table-postgres.util.ts | 25 - 22 files changed, 777 insertions(+), 379 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table-input.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.module.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.service.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/utils/remote-postgres-table.util.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/remote-table-postgres.util.ts diff --git a/packages/twenty-front/src/generated-metadata/gql.ts b/packages/twenty-front/src/generated-metadata/gql.ts index 0b4cb3824..9055155f5 100644 --- a/packages/twenty-front/src/generated-metadata/gql.ts +++ b/packages/twenty-front/src/generated-metadata/gql.ts @@ -20,7 +20,7 @@ const documents = { "\n mutation UpdateOneObjectMetadataItem(\n $idToUpdate: ID!\n $updatePayload: UpdateObjectInput!\n ) {\n updateOneObject(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n }\n }\n": types.UpdateOneObjectMetadataItemDocument, "\n mutation DeleteOneObjectMetadataItem($idToDelete: ID!) {\n deleteOneObject(input: { id: $idToDelete }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n }\n }\n": types.DeleteOneObjectMetadataItemDocument, "\n mutation DeleteOneFieldMetadataItem($idToDelete: ID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n }\n }\n": types.DeleteOneFieldMetadataItemDocument, - "\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n createdAt\n updatedAt\n fromRelationMetadata {\n id\n relationType\n toObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n }\n toFieldMetadataId\n }\n toRelationMetadata {\n id\n relationType\n fromObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n }\n fromFieldMetadataId\n }\n defaultValue\n options\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument, + "\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n createdAt\n updatedAt\n fromRelationMetadata {\n id\n relationType\n toObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n }\n toFieldMetadataId\n }\n toRelationMetadata {\n id\n relationType\n fromObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n }\n fromFieldMetadataId\n }\n defaultValue\n options\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument, }; /** @@ -68,7 +68,7 @@ export function graphql(source: "\n mutation DeleteOneFieldMetadataItem($idToDe /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n createdAt\n updatedAt\n fromRelationMetadata {\n id\n relationType\n toObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n }\n toFieldMetadataId\n }\n toRelationMetadata {\n id\n relationType\n fromObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n }\n fromFieldMetadataId\n }\n defaultValue\n options\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n createdAt\n updatedAt\n fromRelationMetadata {\n id\n relationType\n toObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n }\n toFieldMetadataId\n }\n toRelationMetadata {\n id\n relationType\n fromObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n }\n fromFieldMetadataId\n }\n defaultValue\n options\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"]; +export function graphql(source: "\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n createdAt\n updatedAt\n fromRelationMetadata {\n id\n relationType\n toObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n }\n toFieldMetadataId\n }\n toRelationMetadata {\n id\n relationType\n fromObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n }\n fromFieldMetadataId\n }\n defaultValue\n options\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n createdAt\n updatedAt\n fromRelationMetadata {\n id\n relationType\n toObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n }\n toFieldMetadataId\n }\n toRelationMetadata {\n id\n relationType\n fromObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n }\n fromFieldMetadataId\n }\n defaultValue\n options\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"]; export function graphql(source: string) { return (documents as any)[source] ?? {}; diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index facd710fc..cefdf18e9 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -117,6 +117,7 @@ export type CreateFieldInput = { isActive?: InputMaybe; isCustom?: InputMaybe; isNullable?: InputMaybe; + isRemoteCreation?: InputMaybe; isSystem?: InputMaybe; label: Scalars['String']['input']; name: Scalars['String']['input']; @@ -129,6 +130,7 @@ export type CreateObjectInput = { description?: InputMaybe; icon?: InputMaybe; imageIdentifierFieldMetadataId?: InputMaybe; + isRemote?: InputMaybe; labelIdentifierFieldMetadataId?: InputMaybe; labelPlural: Scalars['String']['input']; labelSingular: Scalars['String']['input']; @@ -361,6 +363,7 @@ export type Mutation = { updateOneField: Field; updateOneObject: Object; updatePasswordViaResetToken: InvalidatePassword; + updateRemoteTableSyncStatus: RemoteTable; updateWorkspace: Workspace; uploadFile: Scalars['String']['output']; uploadImage: Scalars['String']['output']; @@ -487,6 +490,11 @@ export type MutationUpdatePasswordViaResetTokenArgs = { }; +export type MutationUpdateRemoteTableSyncStatusArgs = { + input: RemoteTableInput; +}; + + export type MutationUpdateWorkspaceArgs = { data: UpdateWorkspaceInput; }; @@ -777,6 +785,13 @@ export type RemoteTable = { status: RemoteTableStatus; }; +export type RemoteTableInput = { + name: Scalars['String']['input']; + remoteServerId: Scalars['ID']['input']; + schema: Scalars['String']['input']; + status: RemoteTableStatus; +}; + /** Status of the table */ export enum RemoteTableStatus { NotSynced = 'NOT_SYNCED', @@ -1106,6 +1121,7 @@ export type Object = { imageIdentifierFieldMetadataId?: Maybe; isActive: Scalars['Boolean']['output']; isCustom: Scalars['Boolean']['output']; + isRemote: Scalars['Boolean']['output']; isSystem: Scalars['Boolean']['output']; labelIdentifierFieldMetadataId?: Maybe; labelPlural: Scalars['String']['output']; @@ -1134,6 +1150,7 @@ export type ObjectFilter = { id?: InputMaybe; isActive?: InputMaybe; isCustom?: InputMaybe; + isRemote?: InputMaybe; isSystem?: InputMaybe; or?: InputMaybe>; }; @@ -1217,7 +1234,7 @@ export type ObjectMetadataItemsQueryVariables = Exact<{ }>; -export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', edges: Array<{ __typename?: 'objectEdge', node: { __typename?: 'object', id: string, dataSourceId: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isActive: boolean, isSystem: boolean, createdAt: any, updatedAt: any, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, fields: { __typename?: 'ObjectFieldsConnection', edges: Array<{ __typename?: 'fieldEdge', node: { __typename?: 'field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isNullable?: boolean | null, createdAt: any, updatedAt: any, defaultValue?: any | null, options?: any | null, fromRelationMetadata?: { __typename?: 'relation', id: string, relationType: RelationMetadataType, toFieldMetadataId: string, toObjectMetadata: { __typename?: 'object', id: string, dataSourceId: string, nameSingular: string, namePlural: string, isSystem: boolean } } | null, toRelationMetadata?: { __typename?: 'relation', id: string, relationType: RelationMetadataType, fromFieldMetadataId: string, fromObjectMetadata: { __typename?: 'object', id: string, dataSourceId: string, nameSingular: string, namePlural: string, isSystem: boolean } } | null } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } }; +export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', edges: Array<{ __typename?: 'objectEdge', node: { __typename?: 'object', id: string, dataSourceId: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, createdAt: any, updatedAt: any, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, fields: { __typename?: 'ObjectFieldsConnection', edges: Array<{ __typename?: 'fieldEdge', node: { __typename?: 'field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isNullable?: boolean | null, createdAt: any, updatedAt: any, defaultValue?: any | null, options?: any | null, fromRelationMetadata?: { __typename?: 'relation', id: string, relationType: RelationMetadataType, toFieldMetadataId: string, toObjectMetadata: { __typename?: 'object', id: string, dataSourceId: string, nameSingular: string, namePlural: string, isSystem: boolean } } | null, toRelationMetadata?: { __typename?: 'relation', id: string, relationType: RelationMetadataType, fromFieldMetadataId: string, fromObjectMetadata: { __typename?: 'object', id: string, dataSourceId: string, nameSingular: string, namePlural: string, isSystem: boolean } } | null } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } }; export const CreateOneObjectMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneObjectMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOneObjectInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneObject"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}}]}}]}}]} as unknown as DocumentNode; @@ -1227,4 +1244,4 @@ export const UpdateOneFieldMetadataItemDocument = {"kind":"Document","definition export const UpdateOneObjectMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateOneObjectMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToUpdate"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"updatePayload"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateObjectInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneObject"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToUpdate"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"update"},"value":{"kind":"Variable","name":{"kind":"Name","value":"updatePayload"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}}]}}]}}]} as unknown as DocumentNode; export const DeleteOneObjectMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneObjectMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneObject"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}}]}}]}}]} as unknown as DocumentNode; export const DeleteOneFieldMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneFieldMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneField"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; -export const ObjectMetadataItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ObjectMetadataItems"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"objectFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"fromRelationMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"relationType"}},{"kind":"Field","name":{"kind":"Name","value":"toObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}}]}},{"kind":"Field","name":{"kind":"Name","value":"toFieldMetadataId"}}]}},{"kind":"Field","name":{"kind":"Name","value":"toRelationMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"relationType"}},{"kind":"Field","name":{"kind":"Name","value":"fromObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fromFieldMetadataId"}}]}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const ObjectMetadataItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ObjectMetadataItems"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"objectFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isRemote"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"fromRelationMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"relationType"}},{"kind":"Field","name":{"kind":"Name","value":"toObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}}]}},{"kind":"Field","name":{"kind":"Name","value":"toFieldMetadataId"}}]}},{"kind":"Field","name":{"kind":"Name","value":"toRelationMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"relationType"}},{"kind":"Field","name":{"kind":"Name","value":"fromObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fromFieldMetadataId"}}]}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts b/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts index 6e7805048..c27cc5822 100644 --- a/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts +++ b/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts @@ -17,6 +17,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql` description icon isCustom + isRemote isActive isSystem createdAt diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFindManyObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFindManyObjectMetadataItems.ts index 383910daf..951b2742f 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFindManyObjectMetadataItems.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFindManyObjectMetadataItems.ts @@ -17,6 +17,7 @@ export const query = gql` description icon isCustom + isRemote isActive isSystem createdAt diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts index e9549cf70..3edc7af06 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts @@ -13,6 +13,7 @@ export const getObjectMetadataItemsMock = () => { description: 'A webhook', icon: 'IconRobot', isCustom: false, + isRemote: false, isActive: true, isSystem: true, createdAt: '2023-11-30T11:13:15.206Z', @@ -30,6 +31,7 @@ export const getObjectMetadataItemsMock = () => { description: 'An api key', icon: 'IconRobot', isCustom: false, + isRemote: false, isActive: true, isSystem: true, createdAt: '2023-11-30T11:13:15.206Z', @@ -150,6 +152,7 @@ export const getObjectMetadataItemsMock = () => { description: '(System) View Sorts', icon: 'IconArrowsSort', isCustom: false, + isRemote: false, isActive: true, isSystem: true, createdAt: '2023-11-30T11:13:15.206Z', @@ -282,6 +285,7 @@ export const getObjectMetadataItemsMock = () => { description: 'A calendar event', icon: 'IconCalendarEvent', isCustom: false, + isRemote: false, isActive: true, isSystem: true, createdAt: '2023-11-30T11:13:15.206Z', @@ -299,6 +303,7 @@ export const getObjectMetadataItemsMock = () => { description: 'An opportunity', icon: 'IconTargetArrow', isCustom: false, + isRemote: false, isActive: true, isSystem: false, createdAt: '2023-11-30T11:13:15.206Z', @@ -617,6 +622,7 @@ export const getObjectMetadataItemsMock = () => { description: 'A person', icon: 'IconUser', isCustom: false, + isRemote: false, isActive: true, isSystem: false, createdAt: '2023-11-30T11:13:15.206Z', @@ -1013,6 +1019,7 @@ export const getObjectMetadataItemsMock = () => { description: 'A workspace member', icon: 'IconUserCircle', isCustom: false, + isRemote: false, isActive: true, isSystem: true, createdAt: '2023-11-30T11:13:15.206Z', diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts index b925e1d2c..bbd31e87c 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts @@ -15,6 +15,7 @@ export const objectMetadataItemSchema = z.object({ imageIdentifierFieldMetadataId: z.string().uuid().nullable(), isActive: z.boolean(), isCustom: z.boolean(), + isRemote: z.boolean(), isSystem: z.boolean(), labelIdentifierFieldMetadataId: z.string().uuid().nullable(), labelPlural: z.string().trim().min(1), diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts index 4594d516d..147edd7f8 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts @@ -29,6 +29,10 @@ export const useFindManyParams = ( objectMetadataItem?.fields ?? [], ); + if (objectMetadataItem?.isRemote) { + return { objectNameSingular, filter }; + } + const orderBy = turnSortsIntoOrderBy( tableSorts, objectMetadataItem?.fields ?? [], diff --git a/packages/twenty-front/src/testing/mock-data/metadata.ts b/packages/twenty-front/src/testing/mock-data/metadata.ts index 1f92d8754..6166af8ea 100644 --- a/packages/twenty-front/src/testing/mock-data/metadata.ts +++ b/packages/twenty-front/src/testing/mock-data/metadata.ts @@ -18,6 +18,7 @@ export const mockedPeopleMetadata = { description: 'A person', icon: 'IconUser', isCustom: false, + isRemote: false, isActive: true, isSystem: false, createdAt: '2023-12-15T15:29:39.070Z', @@ -594,6 +595,7 @@ export const mockedCompaniesMetadata = { description: 'A company', icon: 'IconBuildingSkyscraper', isCustom: false, + isRemote: false, isActive: true, isSystem: false, createdAt: '2023-12-15T15:29:39.070Z', diff --git a/packages/twenty-front/src/testing/mock-data/objectMetadataItems.ts b/packages/twenty-front/src/testing/mock-data/objectMetadataItems.ts index b2688dfd1..a243e0e8e 100644 --- a/packages/twenty-front/src/testing/mock-data/objectMetadataItems.ts +++ b/packages/twenty-front/src/testing/mock-data/objectMetadataItems.ts @@ -11,6 +11,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = { description: 'A company', icon: 'IconBuildingSkyscraper', isCustom: false, + isRemote: false, isActive: true, isSystem: false, createdAt: '2023-12-19T12:15:28.459Z', diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/create-field.input.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/create-field.input.ts index 40ccd7467..c74687ae4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/create-field.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/create-field.input.ts @@ -1,6 +1,6 @@ import { Field, InputType, OmitType } from '@nestjs/graphql'; -import { IsUUID, ValidateNested } from 'class-validator'; +import { IsOptional, IsUUID, ValidateNested } from 'class-validator'; import { Type } from 'class-transformer'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; @@ -14,6 +14,10 @@ export class CreateFieldInput extends OmitType( @IsUUID() @Field() objectMetadataId: string; + + @Field(() => Boolean, { nullable: true }) + @IsOptional() + isRemoteCreation?: boolean; } @InputType() diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts index 1599d8363..933f81e6a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts @@ -122,12 +122,13 @@ export class FieldMetadataService extends TypeOrmQueryService relation.id), - ); + if (relationsToDelete.length > 0) { + await this.relationMetadataRepository.delete( + relationsToDelete.map((relation) => relation.id), + ); + } for (const relationToDelete of relationsToDelete) { const foreignKeyFieldsToDelete = await this.fieldMetadataRepository.find({ @@ -192,17 +194,19 @@ export class ObjectMetadataService extends TypeOrmQueryService ID) + remoteServerId: string; + + @Field(() => String) + name: string; + + @IsEnum(RemoteTableStatus) + @Field(() => RemoteTableStatus) + status: RemoteTableStatus; + + @Field(() => String) + schema: string; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.module.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.module.ts new file mode 100644 index 000000000..bb74fd739 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; + +import { RemotePostgresTableService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.service'; +import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; + +@Module({ + imports: [WorkspaceDataSourceModule], + providers: [RemotePostgresTableService], + exports: [RemotePostgresTableService], +}) +export class RemotePostgresTableModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.service.ts new file mode 100644 index 000000000..abf84c840 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.service.ts @@ -0,0 +1,106 @@ +import { Injectable } from '@nestjs/common'; + +import { DataSource } from 'typeorm'; + +import { + RemoteServerEntity, + RemoteServerType, +} from 'src/engine/metadata-modules/remote-server/remote-server.entity'; +import { RemoteTableStatus } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto'; +import { + buildPostgresUrl, + EXCLUDED_POSTGRES_SCHEMAS, +} from 'src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/utils/remote-postgres-table.util'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; + +@Injectable() +export class RemotePostgresTableService { + constructor( + private readonly workspaceDataSourceService: WorkspaceDataSourceService, + private readonly environmentService: EnvironmentService, + ) {} + + public async findAvailableRemotePostgresTables( + workspaceId: string, + remoteServer: RemoteServerEntity, + ) { + const remotePostgresTables = + await this.fetchTablesFromRemotePostgresSchema(remoteServer); + + const workspaceDataSource = + await this.workspaceDataSourceService.connectToWorkspaceDataSource( + workspaceId, + ); + + const currentForeignTableNames = ( + await workspaceDataSource.query( + `SELECT foreign_table_name FROM information_schema.foreign_tables`, + ) + ).map((foreignTable) => foreignTable.foreign_table_name); + + return remotePostgresTables.map((remoteTable) => ({ + name: remoteTable.table_name, + schema: remoteTable.table_schema, + status: currentForeignTableNames.includes(remoteTable.table_name) + ? RemoteTableStatus.SYNCED + : RemoteTableStatus.NOT_SYNCED, + })); + } + + public async fetchPostgresTableColumnsSchema( + remoteServer: RemoteServerEntity, + tableName: string, + tableSchema: string, + ) { + const dataSource = new DataSource({ + url: buildPostgresUrl( + this.environmentService.get('LOGIN_TOKEN_SECRET'), + remoteServer, + ), + type: 'postgres', + logging: true, + }); + + await dataSource.initialize(); + + const columns = await dataSource.query( + `SELECT column_name, data_type, udt_name FROM information_schema.columns WHERE table_name = '${tableName}' AND table_schema = '${tableSchema}'`, + ); + + await dataSource.destroy(); + + return columns; + } + + private async fetchTablesFromRemotePostgresSchema( + remoteServer: RemoteServerEntity, + ) { + const dataSource = new DataSource({ + url: buildPostgresUrl( + this.environmentService.get('LOGIN_TOKEN_SECRET'), + remoteServer, + ), + type: 'postgres', + logging: true, + }); + + await dataSource.initialize(); + + const schemaNames = await dataSource.query( + `SELECT schema_name FROM information_schema.schemata where schema_name not in ( ${EXCLUDED_POSTGRES_SCHEMAS.map( + (schema) => `'${schema}'`, + ).join(', ')} ) order by schema_name limit 1`, + ); + + const remotePostgresTables = await dataSource.query( + `SELECT table_name, table_schema FROM information_schema.tables WHERE table_schema IN (${schemaNames + .map((schemaName) => `'${schemaName.schema_name}'`) + .join(', ')})`, + ); + + await dataSource.destroy(); + + return remotePostgresTables; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/utils/remote-postgres-table.util.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/utils/remote-postgres-table.util.ts new file mode 100644 index 000000000..848ab2ba7 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/utils/remote-postgres-table.util.ts @@ -0,0 +1,65 @@ +import { Repository } from 'typeorm/repository/Repository'; + +import { decryptText } from 'src/engine/core-modules/auth/auth.util'; +import { + FeatureFlagEntity, + FeatureFlagKeys, +} from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { + RemoteServerEntity, + RemoteServerType, +} from 'src/engine/metadata-modules/remote-server/remote-server.entity'; + +export const EXCLUDED_POSTGRES_SCHEMAS = [ + 'information_schema', + 'pg_catalog', + 'pg_toast', +]; + +export const buildPostgresUrl = ( + secretKey: string, + remoteServer: RemoteServerEntity, +): string => { + const foreignDataWrapperOptions = remoteServer.foreignDataWrapperOptions; + const userMappingOptions = remoteServer.userMappingOptions; + + const password = decryptText(userMappingOptions.password, secretKey); + + const url = `postgres://${userMappingOptions.username}:${password}@${foreignDataWrapperOptions.host}:${foreignDataWrapperOptions.port}/${foreignDataWrapperOptions.dbname}`; + + return url; +}; + +export const mapUdtNameToFieldType = (udtName: string): FieldMetadataType => { + switch (udtName) { + case 'uuid': + return FieldMetadataType.UUID; + case 'varchar': + return FieldMetadataType.TEXT; + case 'bool': + return FieldMetadataType.BOOLEAN; + case 'timestamp': + case 'timestamptz': + return FieldMetadataType.DATE_TIME; + default: + return FieldMetadataType.TEXT; + } +}; + +export const isPostgreSQLIntegrationEnabled = async ( + featureFlagRepository: Repository, + workspaceId: string, +) => { + const featureFlag = await featureFlagRepository.findOneBy({ + workspaceId, + key: FeatureFlagKeys.IsPostgreSQLIntegrationEnabled, + value: true, + }); + + const featureFlagEnabled = featureFlag && featureFlag.value; + + if (!featureFlagEnabled) { + throw new Error('PostgreSQL integration is not enabled'); + } +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.module.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.module.ts index ea99ba001..887d05f47 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.module.ts @@ -1,15 +1,27 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; +import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { RemoteServerEntity } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; +import { RemotePostgresTableModule } from 'src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.module'; import { RemoteTableResolver } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.resolver'; import { RemoteTableService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.service'; +import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; @Module({ imports: [ TypeOrmModule.forFeature([RemoteServerEntity], 'metadata'), + TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), WorkspaceDataSourceModule, + DataSourceModule, + ObjectMetadataModule, + FieldMetadataModule, + RemotePostgresTableModule, + WorkspaceCacheVersionModule, ], providers: [RemoteTableService, RemoteTableResolver], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.resolver.ts index 631e8e44a..433dd5fd9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.resolver.ts @@ -1,10 +1,11 @@ import { UseGuards } from '@nestjs/common'; -import { Args, Query, Resolver } from '@nestjs/graphql'; +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { RemoteServerIdInput } from 'src/engine/metadata-modules/remote-server/dtos/remote-server-id.input'; +import { RemoteTableInput } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table-input'; import { RemoteTableDTO } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto'; import { RemoteTableService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.service'; @@ -23,4 +24,15 @@ export class RemoteTableResolver { workspaceId, ); } + + @Mutation(() => RemoteTableDTO) + async updateRemoteTableSyncStatus( + @Args('input') input: RemoteTableInput, + @AuthWorkspace() { id: workspaceId }: Workspace, + ) { + return this.remoteTableService.updateRemoteTableSyncStatus( + input, + workspaceId, + ); + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts index 3fb76711a..548ebf514 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts @@ -8,12 +8,23 @@ import { RemoteServerEntity, } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; import { RemoteTableStatus } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto'; -import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { - EXCLUDED_POSTGRES_SCHEMAS, - buildPostgresUrl, -} from 'src/engine/metadata-modules/remote-server/remote-table/utils/remote-table-postgres.util'; + isPostgreSQLIntegrationEnabled, + mapUdtNameToFieldType, +} from 'src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/utils/remote-postgres-table.util'; +import { RemoteTableInput } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table-input'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; +import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; +import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; +import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; +import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { RemotePostgresTableService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/remote-postgres-table.service'; +import { snakeCase } from 'src/utils/snake-case'; +import { capitalize } from 'src/utils/capitalize'; +import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; export class RemoteTableService { constructor( @@ -21,8 +32,14 @@ export class RemoteTableService { private readonly remoteServerRepository: Repository< RemoteServerEntity >, + @InjectRepository(FeatureFlagEntity, 'core') + private readonly featureFlagRepository: Repository, private readonly workspaceDataSourceService: WorkspaceDataSourceService, - private readonly environmentService: EnvironmentService, + private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, + private readonly dataSourceService: DataSourceService, + private readonly objectMetadataService: ObjectMetadataService, + private readonly fieldMetadataService: FieldMetadataService, + private readonly remotePostgresTableService: RemotePostgresTableService, ) {} public async findAvailableRemoteTablesByServerId( @@ -42,7 +59,12 @@ export class RemoteTableService { switch (remoteServer.foreignDataWrapperType) { case RemoteServerType.POSTGRES_FDW: - return this.findAvailableRemotePostgresTables( + await isPostgreSQLIntegrationEnabled( + this.featureFlagRepository, + workspaceId, + ); + + return this.remotePostgresTableService.findAvailableRemotePostgresTables( workspaceId, remoteServer, ); @@ -51,62 +73,171 @@ export class RemoteTableService { } } - // TODO: may be moved into a separated postgres table service once we have more use cases - private async findAvailableRemotePostgresTables( + public async updateRemoteTableSyncStatus( + input: RemoteTableInput, workspaceId: string, - remoteServer: RemoteServerEntity, ) { - const remotePostgresTables = - await this.fetchTablesFromRemotePostgresSchema(remoteServer); + const remoteServer = await this.remoteServerRepository.findOne({ + where: { + id: input.remoteServerId, + workspaceId, + }, + }); + + if (!remoteServer) { + throw new NotFoundException('Remote server does not exist'); + } + + const dataSourcesMetatada = + await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId( + workspaceId, + ); + + if (!dataSourcesMetatada) { + throw new NotFoundException('Workspace data source does not exist'); + } const workspaceDataSource = await this.workspaceDataSourceService.connectToWorkspaceDataSource( workspaceId, ); - const currentForeignTableNames = ( - await workspaceDataSource.query( - `SELECT foreign_table_name FROM information_schema.foreign_tables`, - ) - ).map((foreignTable) => foreignTable.foreign_table_name); + switch (input.status) { + case RemoteTableStatus.SYNCED: + await this.buildForeignTableAndMetadata( + input, + remoteServer, + workspaceId, + workspaceDataSource, + dataSourcesMetatada[0], + ); + break; + case RemoteTableStatus.NOT_SYNCED: + await this.removeForeignTableAndMetadata( + input, + workspaceId, + workspaceDataSource, + dataSourcesMetatada[0].schema, + ); + break; + default: + throw new Error('Unsupported remote table status'); + } - return remotePostgresTables.map((remoteTable) => ({ - name: remoteTable.table_name, - schema: remoteTable.table_schema, - status: currentForeignTableNames.includes(remoteTable.table_name) - ? RemoteTableStatus.SYNCED - : RemoteTableStatus.NOT_SYNCED, - })); + await this.workspaceCacheVersionService.incrementVersion(workspaceId); + + return input; } - private async fetchTablesFromRemotePostgresSchema( + private async buildForeignTableAndMetadata( + input: RemoteTableInput, remoteServer: RemoteServerEntity, + workspaceId: string, + workspaceDataSource: DataSource, + dataSourceMetadata: DataSourceEntity, ) { - const dataSource = new DataSource({ - url: buildPostgresUrl( - this.environmentService.get('LOGIN_TOKEN_SECRET'), - remoteServer, - ), - type: 'postgres', - logging: true, - }); + const localSchema = dataSourceMetadata.schema; - await dataSource.initialize(); - - const schemaNames = await dataSource.query( - `SELECT schema_name FROM information_schema.schemata where schema_name not in ( ${EXCLUDED_POSTGRES_SCHEMAS.map( - (schema) => `'${schema}'`, - ).join(', ')} ) order by schema_name limit 1`, + // TODO: Add strong typing for remote table columns. Will be done when we have another use case than Postgres + const remoteTableColumns = await this.fetchTableColumnsSchema( + remoteServer, + input.name, + input.schema, ); - const remotePostgresTables = await dataSource.query( - `SELECT table_name, table_schema FROM information_schema.tables WHERE table_schema IN (${schemaNames - .map((schemaName) => `'${schemaName.schema_name}'`) - .join(', ')})`, + const foreignTableColumns = remoteTableColumns + .map((column) => `"${column.column_name}" ${column.data_type}`) + .join(', '); + + await workspaceDataSource.query( + `CREATE FOREIGN TABLE ${localSchema}."${input.name}Remote" (${foreignTableColumns}) SERVER "${remoteServer.foreignDataWrapperId}" OPTIONS (schema_name '${input.schema}', table_name '${input.name}')`, + ); + await workspaceDataSource.query( + `COMMENT ON FOREIGN TABLE ${localSchema}."${input.name}Remote" IS e'@graphql({"primary_key_columns": ["id"], "totalCount": {"enabled": true}})'`, ); - await dataSource.destroy(); + // Should be done in a transaction. To be discussed + const objectMetadata = await this.objectMetadataService.createOne({ + nameSingular: `${input.name}Remote`, + namePlural: `${input.name}Remotes`, + labelSingular: `${capitalize(snakeCase(input.name)).replace( + /_/g, + ' ', + )} remote`, + labelPlural: `${capitalize(snakeCase(input.name)).replace( + /_/g, + ' ', + )} remotes`, + description: 'Remote table', + dataSourceId: dataSourceMetadata.id, + workspaceId: workspaceId, + icon: 'IconUser', + isRemote: true, + } as CreateObjectInput); - return remotePostgresTables; + for (const column of remoteTableColumns) { + const field = await this.fieldMetadataService.createOne({ + name: column.column_name, + label: capitalize(snakeCase(column.column_name)).replace(/_/g, ' '), + description: 'Field of remote', + // TODO: function should work for other types than Postgres + type: mapUdtNameToFieldType(column.udt_name), + workspaceId: workspaceId, + objectMetadataId: objectMetadata.id, + isRemoteCreation: true, + isNullable: true, + } as CreateFieldInput); + + if (column.column_name === 'id') { + await this.objectMetadataService.updateOne(objectMetadata.id, { + labelIdentifierFieldMetadataId: field.id, + }); + } + } + } + + private async removeForeignTableAndMetadata( + input: RemoteTableInput, + workspaceId: string, + workspaceDataSource: DataSource, + localSchema: string, + ) { + const objectMetadata = + await this.objectMetadataService.findOneWithinWorkspace(workspaceId, { + where: { nameSingular: `${input.name}Remote` }, + }); + + if (objectMetadata) { + await this.objectMetadataService.deleteOneObject( + { id: objectMetadata.id }, + workspaceId, + ); + } + + await workspaceDataSource.query( + `DROP FOREIGN TABLE ${localSchema}."${input.name}Remote"`, + ); + } + + private async fetchTableColumnsSchema( + remoteServer: RemoteServerEntity, + tableName: string, + tableSchema: string, + ) { + switch (remoteServer.foreignDataWrapperType) { + case RemoteServerType.POSTGRES_FDW: + await isPostgreSQLIntegrationEnabled( + this.featureFlagRepository, + remoteServer.workspaceId, + ); + + return this.remotePostgresTableService.fetchPostgresTableColumnsSchema( + remoteServer, + tableName, + tableSchema, + ); + default: + throw new Error('Unsupported foreign data wrapper type'); + } } } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/remote-table-postgres.util.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/remote-table-postgres.util.ts deleted file mode 100644 index 66c789909..000000000 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/remote-table-postgres.util.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { decryptText } from 'src/engine/core-modules/auth/auth.util'; -import { - RemoteServerEntity, - RemoteServerType, -} from 'src/engine/metadata-modules/remote-server/remote-server.entity'; - -export const EXCLUDED_POSTGRES_SCHEMAS = [ - 'information_schema', - 'pg_catalog', - 'pg_toast', -]; - -export const buildPostgresUrl = ( - secretKey: string, - remoteServer: RemoteServerEntity, -): string => { - const foreignDataWrapperOptions = remoteServer.foreignDataWrapperOptions; - const userMappingOptions = remoteServer.userMappingOptions; - - const password = decryptText(userMappingOptions.password, secretKey); - - const url = `postgres://${userMappingOptions.username}:${password}@${foreignDataWrapperOptions.host}:${foreignDataWrapperOptions.port}/${foreignDataWrapperOptions.dbname}`; - - return url; -};