BREAKING CHANGE: Removing remote integration feature (#17001)

## Context
The feature has not been maintained for more than a year and was never
officially launched.
This PR removes its code due to the upcoming refactoring of the
sync-metadata that will break its logic if not handled properly and it
would be too costly for the time being.

TODO:
- remove isRemote from object/field metadata
This commit is contained in:
Weiko
2026-01-08 10:14:51 +01:00
committed by GitHub
parent 2e0eeda52e
commit 66b2882ac4
106 changed files with 24 additions and 5202 deletions
@@ -914,14 +914,6 @@ export type CreatePageLayoutWidgetInput = {
type: WidgetType;
};
export type CreateRemoteServerInput = {
foreignDataWrapperOptions: Scalars['JSON'];
foreignDataWrapperType: Scalars['String'];
label: Scalars['String'];
schema?: InputMaybe<Scalars['String']>;
userMappingOptions?: InputMaybe<UserMappingOptions>;
};
export type CreateRoleInput = {
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
canBeAssignedToAgents?: InputMaybe<Scalars['Boolean']>;
@@ -1232,14 +1224,6 @@ export type DestroyViewGroupInput = {
id: Scalars['UUID'];
};
/** Schema update on a table */
export enum DistantTableUpdate {
COLUMNS_ADDED = 'COLUMNS_ADDED',
COLUMNS_DELETED = 'COLUMNS_DELETED',
COLUMNS_TYPE_CHANGED = 'COLUMNS_TYPE_CHANGED',
TABLE_DELETED = 'TABLE_DELETED'
}
export type DomainRecord = {
__typename?: 'DomainRecord';
key: Scalars['String'];
@@ -1350,7 +1334,6 @@ export type FeatureFlagDto = {
};
export enum FeatureFlagKey {
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
IS_AI_ENABLED = 'IS_AI_ENABLED',
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
@@ -1358,11 +1341,9 @@ export enum FeatureFlagKey {
IS_IF_ELSE_ENABLED = 'IS_IF_ELSE_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_PAGE_LAYOUT_ENABLED = 'IS_PAGE_LAYOUT_ENABLED',
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_WORKSPACE_CREATION_V2_ENABLED = 'IS_WORKSPACE_CREATION_V2_ENABLED'
@@ -1503,13 +1484,6 @@ export type FindAvailableSsoidpOutput = {
workspace: WorkspaceNameAndId;
};
export type FindManyRemoteTablesInput = {
/** The id of the remote server. */
id: Scalars['ID'];
/** Indicates if pending schema updates status should be computed. */
shouldFetchPendingSchemaUpdates?: InputMaybe<Scalars['Boolean']>;
};
export type FullName = {
__typename?: 'FullName';
firstName: Scalars['String'];
@@ -1859,7 +1833,6 @@ export type Mutation = {
createOneDatabaseEventTrigger: DatabaseEventTrigger;
createOneField: Field;
createOneObject: Object;
createOneRemoteServer: RemoteServer;
createOneRole: Role;
createOneRouteTrigger: RouteTrigger;
createOneServerlessFunction: ServerlessFunction;
@@ -1894,7 +1867,6 @@ export type Mutation = {
deleteOneDatabaseEventTrigger: DatabaseEventTrigger;
deleteOneField: Field;
deleteOneObject: Object;
deleteOneRemoteServer: RemoteServer;
deleteOneRole: Scalars['String'];
deleteOneRouteTrigger: RouteTrigger;
deleteOneServerlessFunction: ServerlessFunction;
@@ -1969,12 +1941,9 @@ export type Mutation = {
switchBillingPlan: BillingUpdateOutput;
switchSubscriptionInterval: BillingUpdateOutput;
syncApplication: Scalars['Boolean'];
syncRemoteTable: RemoteTable;
syncRemoteTableSchemaChanges: RemoteTable;
testHttpRequest: TestHttpRequestOutput;
trackAnalytics: Analytics;
uninstallApplication: Scalars['Boolean'];
unsyncRemoteTable: RemoteTable;
updateApiKey?: Maybe<ApiKey>;
updateCoreView: CoreView;
updateCoreViewField: CoreViewField;
@@ -1990,7 +1959,6 @@ export type Mutation = {
updateOneDatabaseEventTrigger: DatabaseEventTrigger;
updateOneField: Field;
updateOneObject: Object;
updateOneRemoteServer: RemoteServer;
updateOneRole: Role;
updateOneRouteTrigger: RouteTrigger;
updateOneServerlessFunction: ServerlessFunction;
@@ -2193,11 +2161,6 @@ export type MutationCreateOneObjectArgs = {
};
export type MutationCreateOneRemoteServerArgs = {
input: CreateRemoteServerInput;
};
export type MutationCreateOneRoleArgs = {
createRoleInput: CreateRoleInput;
};
@@ -2365,11 +2328,6 @@ export type MutationDeleteOneObjectArgs = {
};
export type MutationDeleteOneRemoteServerArgs = {
input: RemoteServerIdInput;
};
export type MutationDeleteOneRoleArgs = {
roleId: Scalars['UUID'];
};
@@ -2721,16 +2679,6 @@ export type MutationSyncApplicationArgs = {
};
export type MutationSyncRemoteTableArgs = {
input: RemoteTableInput;
};
export type MutationSyncRemoteTableSchemaChangesArgs = {
input: RemoteTableInput;
};
export type MutationTestHttpRequestArgs = {
input: TestHttpRequestInput;
};
@@ -2749,11 +2697,6 @@ export type MutationUninstallApplicationArgs = {
};
export type MutationUnsyncRemoteTableArgs = {
input: RemoteTableInput;
};
export type MutationUpdateApiKeyArgs = {
input: UpdateApiKeyInput;
};
@@ -2835,11 +2778,6 @@ export type MutationUpdateOneObjectArgs = {
};
export type MutationUpdateOneRemoteServerArgs = {
input: UpdateRemoteServerInput;
};
export type MutationUpdateOneRoleArgs = {
updateRoleInput: UpdateRoleInput;
};
@@ -3368,20 +3306,17 @@ export type Query = {
currentWorkspace: Workspace;
field: Field;
fields: FieldConnection;
findDistantTablesWithStatus: Array<RemoteTable>;
findManyAgents: Array<Agent>;
findManyApplications: Array<Application>;
findManyCronTriggers: Array<CronTrigger>;
findManyDatabaseEventTriggers: Array<DatabaseEventTrigger>;
findManyPublicDomains: Array<PublicDomain>;
findManyRemoteServersByType: Array<RemoteServer>;
findManyRouteTriggers: Array<RouteTrigger>;
findManyServerlessFunctions: Array<ServerlessFunction>;
findOneAgent: Agent;
findOneApplication: Application;
findOneCronTrigger: CronTrigger;
findOneDatabaseEventTrigger: DatabaseEventTrigger;
findOneRemoteServerById: RemoteServer;
findOneRouteTrigger: RouteTrigger;
findOneServerlessFunction: ServerlessFunction;
findWorkspaceFromInviteHash: Workspace;
@@ -3494,16 +3429,6 @@ export type QueryFieldsArgs = {
};
export type QueryFindDistantTablesWithStatusArgs = {
input: FindManyRemoteTablesInput;
};
export type QueryFindManyRemoteServersByTypeArgs = {
input: RemoteServerTypeInput;
};
export type QueryFindOneAgentArgs = {
input: AgentIdInput;
};
@@ -3524,11 +3449,6 @@ export type QueryFindOneDatabaseEventTriggerArgs = {
};
export type QueryFindOneRemoteServerByIdArgs = {
input: RemoteServerIdInput;
};
export type QueryFindOneRouteTriggerArgs = {
input: RouteTriggerIdInput;
};
@@ -3879,48 +3799,6 @@ export enum RelationType {
ONE_TO_MANY = 'ONE_TO_MANY'
}
export type RemoteServer = {
__typename?: 'RemoteServer';
createdAt: Scalars['DateTime'];
foreignDataWrapperId: Scalars['UUID'];
foreignDataWrapperOptions?: Maybe<Scalars['JSON']>;
foreignDataWrapperType: Scalars['String'];
id: Scalars['UUID'];
label: Scalars['String'];
schema?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime'];
userMappingOptions?: Maybe<UserMappingOptionsUser>;
};
export type RemoteServerIdInput = {
/** The id of the record. */
id: Scalars['ID'];
};
export type RemoteServerTypeInput = {
foreignDataWrapperType: Scalars['String'];
};
export type RemoteTable = {
__typename?: 'RemoteTable';
id?: Maybe<Scalars['UUID']>;
name: Scalars['String'];
schema?: Maybe<Scalars['String']>;
schemaPendingUpdates?: Maybe<Array<DistantTableUpdate>>;
status: RemoteTableStatus;
};
export type RemoteTableInput = {
name: Scalars['String'];
remoteServerId: Scalars['UUID'];
};
/** Status of the table */
export enum RemoteTableStatus {
NOT_SYNCED = 'NOT_SYNCED',
SYNCED = 'SYNCED'
}
export type ResendEmailVerificationTokenOutput = {
__typename?: 'ResendEmailVerificationTokenOutput';
success: Scalars['Boolean'];
@@ -4611,14 +4489,6 @@ export type UpdatePageLayoutWithTabsInput = {
type: PageLayoutType;
};
export type UpdateRemoteServerInput = {
foreignDataWrapperOptions?: InputMaybe<Scalars['JSON']>;
id: Scalars['UUID'];
label?: InputMaybe<Scalars['String']>;
schema?: InputMaybe<Scalars['String']>;
userMappingOptions?: InputMaybe<UserMappingOptionsUpdateInput>;
};
export type UpdateRoleInput = {
/** The id of the role to update */
id: Scalars['UUID'];
@@ -4894,21 +4764,6 @@ export type UserLookup = {
workspaces: Array<WorkspaceInfo>;
};
export type UserMappingOptions = {
password?: InputMaybe<Scalars['String']>;
user?: InputMaybe<Scalars['String']>;
};
export type UserMappingOptionsUpdateInput = {
password?: InputMaybe<Scalars['String']>;
user?: InputMaybe<Scalars['String']>;
};
export type UserMappingOptionsUser = {
__typename?: 'UserMappingOptionsUser';
user?: Maybe<Scalars['String']>;
};
export type UserWorkspace = {
__typename?: 'UserWorkspace';
createdAt: Scalars['DateTime'];
@@ -5784,73 +5639,6 @@ export type ListPlansQueryVariables = Exact<{ [key: string]: never; }>;
export type ListPlansQuery = { __typename?: 'Query', listPlans: Array<{ __typename?: 'BillingPlanOutput', planKey: BillingPlanKey, licensedProducts: Array<{ __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array<string> | null, prices?: Array<{ __typename?: 'BillingPriceLicensed', stripePriceId: string, unitAmount: number, recurringInterval: SubscriptionInterval, priceUsageType: BillingUsageType }> | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } }>, meteredProducts: Array<{ __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array<string> | null, prices?: Array<{ __typename?: 'BillingPriceMetered', priceUsageType: BillingUsageType, recurringInterval: SubscriptionInterval, stripePriceId: string, tiers: Array<{ __typename?: 'BillingPriceTier', flatAmount?: number | null, unitAmount?: number | null, upTo?: number | null }> }> | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } }> }> };
export type RemoteServerFieldsFragment = { __typename?: 'RemoteServer', id: string, createdAt: string, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: string, schema?: string | null, label: string, userMappingOptions?: { __typename?: 'UserMappingOptionsUser', user?: string | null } | null };
export type RemoteTableFieldsFragment = { __typename?: 'RemoteTable', id?: string | null, name: string, schema?: string | null, status: RemoteTableStatus, schemaPendingUpdates?: Array<DistantTableUpdate> | null };
export type CreateServerMutationVariables = Exact<{
input: CreateRemoteServerInput;
}>;
export type CreateServerMutation = { __typename?: 'Mutation', createOneRemoteServer: { __typename?: 'RemoteServer', id: string, createdAt: string, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: string, schema?: string | null, label: string, userMappingOptions?: { __typename?: 'UserMappingOptionsUser', user?: string | null } | null } };
export type DeleteServerMutationVariables = Exact<{
input: RemoteServerIdInput;
}>;
export type DeleteServerMutation = { __typename?: 'Mutation', deleteOneRemoteServer: { __typename?: 'RemoteServer', id: string } };
export type SyncRemoteTableMutationVariables = Exact<{
input: RemoteTableInput;
}>;
export type SyncRemoteTableMutation = { __typename?: 'Mutation', syncRemoteTable: { __typename?: 'RemoteTable', id?: string | null, name: string, schema?: string | null, status: RemoteTableStatus, schemaPendingUpdates?: Array<DistantTableUpdate> | null } };
export type SyncRemoteTableSchemaChangesMutationVariables = Exact<{
input: RemoteTableInput;
}>;
export type SyncRemoteTableSchemaChangesMutation = { __typename?: 'Mutation', syncRemoteTableSchemaChanges: { __typename?: 'RemoteTable', id?: string | null, name: string, schema?: string | null, status: RemoteTableStatus, schemaPendingUpdates?: Array<DistantTableUpdate> | null } };
export type UnsyncRemoteTableMutationVariables = Exact<{
input: RemoteTableInput;
}>;
export type UnsyncRemoteTableMutation = { __typename?: 'Mutation', unsyncRemoteTable: { __typename?: 'RemoteTable', id?: string | null, name: string, schema?: string | null, status: RemoteTableStatus, schemaPendingUpdates?: Array<DistantTableUpdate> | null } };
export type UpdateServerMutationVariables = Exact<{
input: UpdateRemoteServerInput;
}>;
export type UpdateServerMutation = { __typename?: 'Mutation', updateOneRemoteServer: { __typename?: 'RemoteServer', id: string, createdAt: string, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: string, schema?: string | null, label: string, userMappingOptions?: { __typename?: 'UserMappingOptionsUser', user?: string | null } | null } };
export type GetManyDatabaseConnectionsQueryVariables = Exact<{
input: RemoteServerTypeInput;
}>;
export type GetManyDatabaseConnectionsQuery = { __typename?: 'Query', findManyRemoteServersByType: Array<{ __typename?: 'RemoteServer', id: string, createdAt: string, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: string, schema?: string | null, label: string, userMappingOptions?: { __typename?: 'UserMappingOptionsUser', user?: string | null } | null }> };
export type GetManyRemoteTablesQueryVariables = Exact<{
input: FindManyRemoteTablesInput;
}>;
export type GetManyRemoteTablesQuery = { __typename?: 'Query', findDistantTablesWithStatus: Array<{ __typename?: 'RemoteTable', id?: string | null, name: string, schema?: string | null, status: RemoteTableStatus, schemaPendingUpdates?: Array<DistantTableUpdate> | null }> };
export type GetOneDatabaseConnectionQueryVariables = Exact<{
input: RemoteServerIdInput;
}>;
export type GetOneDatabaseConnectionQuery = { __typename?: 'Query', findOneRemoteServerById: { __typename?: 'RemoteServer', id: string, createdAt: string, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: string, schema?: string | null, label: string, userMappingOptions?: { __typename?: 'UserMappingOptionsUser', user?: string | null } | null } };
export type CreateFileMutationVariables = Exact<{
file: Scalars['Upload'];
}>;
@@ -7160,30 +6948,6 @@ export const BillingPriceMeteredFragmentFragmentDoc = gql`
}
}
`;
export const RemoteServerFieldsFragmentDoc = gql`
fragment RemoteServerFields on RemoteServer {
id
createdAt
foreignDataWrapperId
foreignDataWrapperOptions
foreignDataWrapperType
userMappingOptions {
user
}
updatedAt
schema
label
}
`;
export const RemoteTableFieldsFragmentDoc = gql`
fragment RemoteTableFields on RemoteTable {
id
name
schema
status
schemaPendingUpdates
}
`;
export const ApiKeyFragmentFragmentDoc = gql`
fragment ApiKeyFragment on ApiKey {
id
@@ -10003,309 +9767,6 @@ export function useListPlansLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<
export type ListPlansQueryHookResult = ReturnType<typeof useListPlansQuery>;
export type ListPlansLazyQueryHookResult = ReturnType<typeof useListPlansLazyQuery>;
export type ListPlansQueryResult = Apollo.QueryResult<ListPlansQuery, ListPlansQueryVariables>;
export const CreateServerDocument = gql`
mutation createServer($input: CreateRemoteServerInput!) {
createOneRemoteServer(input: $input) {
...RemoteServerFields
}
}
${RemoteServerFieldsFragmentDoc}`;
export type CreateServerMutationFn = Apollo.MutationFunction<CreateServerMutation, CreateServerMutationVariables>;
/**
* __useCreateServerMutation__
*
* To run a mutation, you first call `useCreateServerMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateServerMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [createServerMutation, { data, loading, error }] = useCreateServerMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useCreateServerMutation(baseOptions?: Apollo.MutationHookOptions<CreateServerMutation, CreateServerMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateServerMutation, CreateServerMutationVariables>(CreateServerDocument, options);
}
export type CreateServerMutationHookResult = ReturnType<typeof useCreateServerMutation>;
export type CreateServerMutationResult = Apollo.MutationResult<CreateServerMutation>;
export type CreateServerMutationOptions = Apollo.BaseMutationOptions<CreateServerMutation, CreateServerMutationVariables>;
export const DeleteServerDocument = gql`
mutation deleteServer($input: RemoteServerIdInput!) {
deleteOneRemoteServer(input: $input) {
id
}
}
`;
export type DeleteServerMutationFn = Apollo.MutationFunction<DeleteServerMutation, DeleteServerMutationVariables>;
/**
* __useDeleteServerMutation__
*
* To run a mutation, you first call `useDeleteServerMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeleteServerMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [deleteServerMutation, { data, loading, error }] = useDeleteServerMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useDeleteServerMutation(baseOptions?: Apollo.MutationHookOptions<DeleteServerMutation, DeleteServerMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<DeleteServerMutation, DeleteServerMutationVariables>(DeleteServerDocument, options);
}
export type DeleteServerMutationHookResult = ReturnType<typeof useDeleteServerMutation>;
export type DeleteServerMutationResult = Apollo.MutationResult<DeleteServerMutation>;
export type DeleteServerMutationOptions = Apollo.BaseMutationOptions<DeleteServerMutation, DeleteServerMutationVariables>;
export const SyncRemoteTableDocument = gql`
mutation syncRemoteTable($input: RemoteTableInput!) {
syncRemoteTable(input: $input) {
...RemoteTableFields
}
}
${RemoteTableFieldsFragmentDoc}`;
export type SyncRemoteTableMutationFn = Apollo.MutationFunction<SyncRemoteTableMutation, SyncRemoteTableMutationVariables>;
/**
* __useSyncRemoteTableMutation__
*
* To run a mutation, you first call `useSyncRemoteTableMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useSyncRemoteTableMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [syncRemoteTableMutation, { data, loading, error }] = useSyncRemoteTableMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useSyncRemoteTableMutation(baseOptions?: Apollo.MutationHookOptions<SyncRemoteTableMutation, SyncRemoteTableMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<SyncRemoteTableMutation, SyncRemoteTableMutationVariables>(SyncRemoteTableDocument, options);
}
export type SyncRemoteTableMutationHookResult = ReturnType<typeof useSyncRemoteTableMutation>;
export type SyncRemoteTableMutationResult = Apollo.MutationResult<SyncRemoteTableMutation>;
export type SyncRemoteTableMutationOptions = Apollo.BaseMutationOptions<SyncRemoteTableMutation, SyncRemoteTableMutationVariables>;
export const SyncRemoteTableSchemaChangesDocument = gql`
mutation syncRemoteTableSchemaChanges($input: RemoteTableInput!) {
syncRemoteTableSchemaChanges(input: $input) {
...RemoteTableFields
}
}
${RemoteTableFieldsFragmentDoc}`;
export type SyncRemoteTableSchemaChangesMutationFn = Apollo.MutationFunction<SyncRemoteTableSchemaChangesMutation, SyncRemoteTableSchemaChangesMutationVariables>;
/**
* __useSyncRemoteTableSchemaChangesMutation__
*
* To run a mutation, you first call `useSyncRemoteTableSchemaChangesMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useSyncRemoteTableSchemaChangesMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [syncRemoteTableSchemaChangesMutation, { data, loading, error }] = useSyncRemoteTableSchemaChangesMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useSyncRemoteTableSchemaChangesMutation(baseOptions?: Apollo.MutationHookOptions<SyncRemoteTableSchemaChangesMutation, SyncRemoteTableSchemaChangesMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<SyncRemoteTableSchemaChangesMutation, SyncRemoteTableSchemaChangesMutationVariables>(SyncRemoteTableSchemaChangesDocument, options);
}
export type SyncRemoteTableSchemaChangesMutationHookResult = ReturnType<typeof useSyncRemoteTableSchemaChangesMutation>;
export type SyncRemoteTableSchemaChangesMutationResult = Apollo.MutationResult<SyncRemoteTableSchemaChangesMutation>;
export type SyncRemoteTableSchemaChangesMutationOptions = Apollo.BaseMutationOptions<SyncRemoteTableSchemaChangesMutation, SyncRemoteTableSchemaChangesMutationVariables>;
export const UnsyncRemoteTableDocument = gql`
mutation unsyncRemoteTable($input: RemoteTableInput!) {
unsyncRemoteTable(input: $input) {
...RemoteTableFields
}
}
${RemoteTableFieldsFragmentDoc}`;
export type UnsyncRemoteTableMutationFn = Apollo.MutationFunction<UnsyncRemoteTableMutation, UnsyncRemoteTableMutationVariables>;
/**
* __useUnsyncRemoteTableMutation__
*
* To run a mutation, you first call `useUnsyncRemoteTableMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUnsyncRemoteTableMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [unsyncRemoteTableMutation, { data, loading, error }] = useUnsyncRemoteTableMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useUnsyncRemoteTableMutation(baseOptions?: Apollo.MutationHookOptions<UnsyncRemoteTableMutation, UnsyncRemoteTableMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UnsyncRemoteTableMutation, UnsyncRemoteTableMutationVariables>(UnsyncRemoteTableDocument, options);
}
export type UnsyncRemoteTableMutationHookResult = ReturnType<typeof useUnsyncRemoteTableMutation>;
export type UnsyncRemoteTableMutationResult = Apollo.MutationResult<UnsyncRemoteTableMutation>;
export type UnsyncRemoteTableMutationOptions = Apollo.BaseMutationOptions<UnsyncRemoteTableMutation, UnsyncRemoteTableMutationVariables>;
export const UpdateServerDocument = gql`
mutation updateServer($input: UpdateRemoteServerInput!) {
updateOneRemoteServer(input: $input) {
...RemoteServerFields
}
}
${RemoteServerFieldsFragmentDoc}`;
export type UpdateServerMutationFn = Apollo.MutationFunction<UpdateServerMutation, UpdateServerMutationVariables>;
/**
* __useUpdateServerMutation__
*
* To run a mutation, you first call `useUpdateServerMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateServerMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateServerMutation, { data, loading, error }] = useUpdateServerMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useUpdateServerMutation(baseOptions?: Apollo.MutationHookOptions<UpdateServerMutation, UpdateServerMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateServerMutation, UpdateServerMutationVariables>(UpdateServerDocument, options);
}
export type UpdateServerMutationHookResult = ReturnType<typeof useUpdateServerMutation>;
export type UpdateServerMutationResult = Apollo.MutationResult<UpdateServerMutation>;
export type UpdateServerMutationOptions = Apollo.BaseMutationOptions<UpdateServerMutation, UpdateServerMutationVariables>;
export const GetManyDatabaseConnectionsDocument = gql`
query GetManyDatabaseConnections($input: RemoteServerTypeInput!) {
findManyRemoteServersByType(input: $input) {
...RemoteServerFields
}
}
${RemoteServerFieldsFragmentDoc}`;
/**
* __useGetManyDatabaseConnectionsQuery__
*
* To run a query within a React component, call `useGetManyDatabaseConnectionsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetManyDatabaseConnectionsQuery` 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 } = useGetManyDatabaseConnectionsQuery({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useGetManyDatabaseConnectionsQuery(baseOptions: Apollo.QueryHookOptions<GetManyDatabaseConnectionsQuery, GetManyDatabaseConnectionsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetManyDatabaseConnectionsQuery, GetManyDatabaseConnectionsQueryVariables>(GetManyDatabaseConnectionsDocument, options);
}
export function useGetManyDatabaseConnectionsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetManyDatabaseConnectionsQuery, GetManyDatabaseConnectionsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetManyDatabaseConnectionsQuery, GetManyDatabaseConnectionsQueryVariables>(GetManyDatabaseConnectionsDocument, options);
}
export type GetManyDatabaseConnectionsQueryHookResult = ReturnType<typeof useGetManyDatabaseConnectionsQuery>;
export type GetManyDatabaseConnectionsLazyQueryHookResult = ReturnType<typeof useGetManyDatabaseConnectionsLazyQuery>;
export type GetManyDatabaseConnectionsQueryResult = Apollo.QueryResult<GetManyDatabaseConnectionsQuery, GetManyDatabaseConnectionsQueryVariables>;
export const GetManyRemoteTablesDocument = gql`
query GetManyRemoteTables($input: FindManyRemoteTablesInput!) {
findDistantTablesWithStatus(input: $input) {
...RemoteTableFields
}
}
${RemoteTableFieldsFragmentDoc}`;
/**
* __useGetManyRemoteTablesQuery__
*
* To run a query within a React component, call `useGetManyRemoteTablesQuery` and pass it any options that fit your needs.
* When your component renders, `useGetManyRemoteTablesQuery` 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 } = useGetManyRemoteTablesQuery({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useGetManyRemoteTablesQuery(baseOptions: Apollo.QueryHookOptions<GetManyRemoteTablesQuery, GetManyRemoteTablesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetManyRemoteTablesQuery, GetManyRemoteTablesQueryVariables>(GetManyRemoteTablesDocument, options);
}
export function useGetManyRemoteTablesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetManyRemoteTablesQuery, GetManyRemoteTablesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetManyRemoteTablesQuery, GetManyRemoteTablesQueryVariables>(GetManyRemoteTablesDocument, options);
}
export type GetManyRemoteTablesQueryHookResult = ReturnType<typeof useGetManyRemoteTablesQuery>;
export type GetManyRemoteTablesLazyQueryHookResult = ReturnType<typeof useGetManyRemoteTablesLazyQuery>;
export type GetManyRemoteTablesQueryResult = Apollo.QueryResult<GetManyRemoteTablesQuery, GetManyRemoteTablesQueryVariables>;
export const GetOneDatabaseConnectionDocument = gql`
query GetOneDatabaseConnection($input: RemoteServerIdInput!) {
findOneRemoteServerById(input: $input) {
...RemoteServerFields
}
}
${RemoteServerFieldsFragmentDoc}`;
/**
* __useGetOneDatabaseConnectionQuery__
*
* To run a query within a React component, call `useGetOneDatabaseConnectionQuery` and pass it any options that fit your needs.
* When your component renders, `useGetOneDatabaseConnectionQuery` 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 } = useGetOneDatabaseConnectionQuery({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useGetOneDatabaseConnectionQuery(baseOptions: Apollo.QueryHookOptions<GetOneDatabaseConnectionQuery, GetOneDatabaseConnectionQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetOneDatabaseConnectionQuery, GetOneDatabaseConnectionQueryVariables>(GetOneDatabaseConnectionDocument, options);
}
export function useGetOneDatabaseConnectionLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetOneDatabaseConnectionQuery, GetOneDatabaseConnectionQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetOneDatabaseConnectionQuery, GetOneDatabaseConnectionQueryVariables>(GetOneDatabaseConnectionDocument, options);
}
export type GetOneDatabaseConnectionQueryHookResult = ReturnType<typeof useGetOneDatabaseConnectionQuery>;
export type GetOneDatabaseConnectionLazyQueryHookResult = ReturnType<typeof useGetOneDatabaseConnectionLazyQuery>;
export type GetOneDatabaseConnectionQueryResult = Apollo.QueryResult<GetOneDatabaseConnectionQuery, GetOneDatabaseConnectionQueryVariables>;
export const CreateFileDocument = gql`
mutation CreateFile($file: Upload!) {
createFile(file: $file) {
@@ -1207,14 +1207,6 @@ export type DestroyViewGroupInput = {
id: Scalars['UUID'];
};
/** Schema update on a table */
export enum DistantTableUpdate {
COLUMNS_ADDED = 'COLUMNS_ADDED',
COLUMNS_DELETED = 'COLUMNS_DELETED',
COLUMNS_TYPE_CHANGED = 'COLUMNS_TYPE_CHANGED',
TABLE_DELETED = 'TABLE_DELETED'
}
export type DomainRecord = {
__typename?: 'DomainRecord';
key: Scalars['String'];
@@ -1325,7 +1317,6 @@ export type FeatureFlagDto = {
};
export enum FeatureFlagKey {
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
IS_AI_ENABLED = 'IS_AI_ENABLED',
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
@@ -1333,11 +1324,9 @@ export enum FeatureFlagKey {
IS_IF_ELSE_ENABLED = 'IS_IF_ELSE_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_PAGE_LAYOUT_ENABLED = 'IS_PAGE_LAYOUT_ENABLED',
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_WORKSPACE_CREATION_V2_ENABLED = 'IS_WORKSPACE_CREATION_V2_ENABLED'
@@ -3685,34 +3674,6 @@ export enum RelationType {
ONE_TO_MANY = 'ONE_TO_MANY'
}
export type RemoteServer = {
__typename?: 'RemoteServer';
createdAt: Scalars['DateTime'];
foreignDataWrapperId: Scalars['UUID'];
foreignDataWrapperOptions?: Maybe<Scalars['JSON']>;
foreignDataWrapperType: Scalars['String'];
id: Scalars['UUID'];
label: Scalars['String'];
schema?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime'];
userMappingOptions?: Maybe<UserMappingOptionsUser>;
};
export type RemoteTable = {
__typename?: 'RemoteTable';
id?: Maybe<Scalars['UUID']>;
name: Scalars['String'];
schema?: Maybe<Scalars['String']>;
schemaPendingUpdates?: Maybe<Array<DistantTableUpdate>>;
status: RemoteTableStatus;
};
/** Status of the table */
export enum RemoteTableStatus {
NOT_SYNCED = 'NOT_SYNCED',
SYNCED = 'SYNCED'
}
export type ResendEmailVerificationTokenOutput = {
__typename?: 'ResendEmailVerificationTokenOutput';
success: Scalars['Boolean'];
@@ -4668,11 +4629,6 @@ export type UserLookup = {
workspaces: Array<WorkspaceInfo>;
};
export type UserMappingOptionsUser = {
__typename?: 'UserMappingOptionsUser';
user?: Maybe<Scalars['String']>;
};
export type UserWorkspace = {
__typename?: 'UserWorkspace';
createdAt: Scalars['DateTime'];
@@ -225,14 +225,6 @@ const SettingsBilling = lazy(() =>
})),
);
const SettingsIntegrations = lazy(() =>
import('~/pages/settings/integrations/SettingsIntegrations').then(
(module) => ({
default: module.SettingsIntegrations,
}),
),
);
const SettingsObjects = lazy(() =>
import('~/pages/settings/data-model/SettingsObjects').then((module) => ({
default: module.SettingsObjects,
@@ -572,10 +564,6 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
path={SettingsPath.WebhookDetail}
element={<SettingsDevelopersWebhookDetail />}
/>
<Route
path={SettingsPath.Integrations}
element={<SettingsIntegrations />}
/>
</Route>
<Route
@@ -1,17 +0,0 @@
import { gql } from '@apollo/client';
export const DATABASE_CONNECTION_FRAGMENT = gql`
fragment RemoteServerFields on RemoteServer {
id
createdAt
foreignDataWrapperId
foreignDataWrapperOptions
foreignDataWrapperType
userMappingOptions {
user
}
updatedAt
schema
label
}
`;
@@ -1,11 +0,0 @@
import { gql } from '@apollo/client';
export const REMOTE_TABLE_FRAGMENT = gql`
fragment RemoteTableFields on RemoteTable {
id
name
schema
status
schemaPendingUpdates
}
`;
@@ -1,12 +0,0 @@
import { gql } from '@apollo/client';
import { DATABASE_CONNECTION_FRAGMENT } from '@/databases/graphql/fragments/databaseConnectionFragment';
export const CREATE_ONE_DATABASE_CONNECTION = gql`
${DATABASE_CONNECTION_FRAGMENT}
mutation createServer($input: CreateRemoteServerInput!) {
createOneRemoteServer(input: $input) {
...RemoteServerFields
}
}
`;
@@ -1,9 +0,0 @@
import { gql } from '@apollo/client';
export const DELETE_ONE_DATABASE_CONNECTION = gql`
mutation deleteServer($input: RemoteServerIdInput!) {
deleteOneRemoteServer(input: $input) {
id
}
}
`;
@@ -1,12 +0,0 @@
import { gql } from '@apollo/client';
import { REMOTE_TABLE_FRAGMENT } from '@/databases/graphql/fragments/remoteTableFragment';
export const SYNC_REMOTE_TABLE = gql`
${REMOTE_TABLE_FRAGMENT}
mutation syncRemoteTable($input: RemoteTableInput!) {
syncRemoteTable(input: $input) {
...RemoteTableFields
}
}
`;
@@ -1,12 +0,0 @@
import { gql } from '@apollo/client';
import { REMOTE_TABLE_FRAGMENT } from '@/databases/graphql/fragments/remoteTableFragment';
export const SYNC_REMOTE_TABLE_SCHEMA_CHANGES = gql`
${REMOTE_TABLE_FRAGMENT}
mutation syncRemoteTableSchemaChanges($input: RemoteTableInput!) {
syncRemoteTableSchemaChanges(input: $input) {
...RemoteTableFields
}
}
`;
@@ -1,12 +0,0 @@
import { gql } from '@apollo/client';
import { REMOTE_TABLE_FRAGMENT } from '@/databases/graphql/fragments/remoteTableFragment';
export const UNSYNC_REMOTE_TABLE = gql`
${REMOTE_TABLE_FRAGMENT}
mutation unsyncRemoteTable($input: RemoteTableInput!) {
unsyncRemoteTable(input: $input) {
...RemoteTableFields
}
}
`;
@@ -1,12 +0,0 @@
import { gql } from '@apollo/client';
import { DATABASE_CONNECTION_FRAGMENT } from '@/databases/graphql/fragments/databaseConnectionFragment';
export const UPDATE_ONE_DATABASE_CONNECTION = gql`
${DATABASE_CONNECTION_FRAGMENT}
mutation updateServer($input: UpdateRemoteServerInput!) {
updateOneRemoteServer(input: $input) {
...RemoteServerFields
}
}
`;
@@ -1,12 +0,0 @@
import { gql } from '@apollo/client';
import { DATABASE_CONNECTION_FRAGMENT } from '@/databases/graphql/fragments/databaseConnectionFragment';
export const GET_MANY_DATABASE_CONNECTIONS = gql`
${DATABASE_CONNECTION_FRAGMENT}
query GetManyDatabaseConnections($input: RemoteServerTypeInput!) {
findManyRemoteServersByType(input: $input) {
...RemoteServerFields
}
}
`;
@@ -1,12 +0,0 @@
import { gql } from '@apollo/client';
import { REMOTE_TABLE_FRAGMENT } from '@/databases/graphql/fragments/remoteTableFragment';
export const GET_MANY_REMOTE_TABLES = gql`
${REMOTE_TABLE_FRAGMENT}
query GetManyRemoteTables($input: FindManyRemoteTablesInput!) {
findDistantTablesWithStatus(input: $input) {
...RemoteTableFields
}
}
`;
@@ -1,12 +0,0 @@
import { gql } from '@apollo/client';
import { DATABASE_CONNECTION_FRAGMENT } from '@/databases/graphql/fragments/databaseConnectionFragment';
export const GET_ONE_DATABASE_CONNECTION = gql`
${DATABASE_CONNECTION_FRAGMENT}
query GetOneDatabaseConnection($input: RemoteServerIdInput!) {
findOneRemoteServerById(input: $input) {
...RemoteServerFields
}
}
`;
@@ -1,58 +0,0 @@
import { useMutation } from '@apollo/client';
import { CREATE_ONE_DATABASE_CONNECTION } from '@/databases/graphql/mutations/createOneDatabaseConnection';
import { GET_MANY_DATABASE_CONNECTIONS } from '@/databases/graphql/queries/findManyDatabaseConnections';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import {
type CreateRemoteServerInput,
type CreateServerMutation,
type CreateServerMutationVariables,
} from '~/generated-metadata/graphql';
import { isDefined } from 'twenty-shared/utils';
export const useCreateOneDatabaseConnection = () => {
const apolloMetadataClient = useApolloCoreClient();
const [mutate] = useMutation<
CreateServerMutation,
CreateServerMutationVariables
>(CREATE_ONE_DATABASE_CONNECTION, {
client: apolloMetadataClient,
});
const createOneDatabaseConnection = async (
input: CreateRemoteServerInput,
) => {
return await mutate({
variables: {
input,
},
update: (cache, { data }) => {
const createdRemoteServer = data?.createOneRemoteServer;
if (!createdRemoteServer) return;
const getManyDatabaseConnectionsQuery = {
query: GET_MANY_DATABASE_CONNECTIONS,
variables: {
input: { foreignDataWrapperType: input.foreignDataWrapperType },
},
};
if (isDefined(cache.readQuery(getManyDatabaseConnectionsQuery))) {
cache.updateQuery(getManyDatabaseConnectionsQuery, (cachedQuery) => ({
findManyRemoteServersByType: [
...cachedQuery.findManyRemoteServersByType,
createdRemoteServer,
],
}));
return;
}
},
});
};
return {
createOneDatabaseConnection,
};
};
@@ -1,36 +0,0 @@
import { useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { DELETE_ONE_DATABASE_CONNECTION } from '@/databases/graphql/mutations/deleteOneDatabaseConnection';
import { GET_MANY_DATABASE_CONNECTIONS } from '@/databases/graphql/queries/findManyDatabaseConnections';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import {
type DeleteServerMutation,
type DeleteServerMutationVariables,
type RemoteServerIdInput,
} from '~/generated-metadata/graphql';
export const useDeleteOneDatabaseConnection = () => {
const apolloMetadataClient = useApolloCoreClient();
const [mutate] = useMutation<
DeleteServerMutation,
DeleteServerMutationVariables
>(DELETE_ONE_DATABASE_CONNECTION, {
client: apolloMetadataClient,
});
const deleteOneDatabaseConnection = async (input: RemoteServerIdInput) => {
return await mutate({
variables: {
input,
},
awaitRefetchQueries: true,
refetchQueries: [getOperationName(GET_MANY_DATABASE_CONNECTIONS) ?? ''],
});
};
return {
deleteOneDatabaseConnection,
};
};
@@ -1,52 +0,0 @@
import { useQuery, type WatchQueryFetchPolicy } from '@apollo/client';
import { GET_ONE_DATABASE_CONNECTION } from '@/databases/graphql/queries/findOneDatabaseConnection';
import { getForeignDataWrapperType } from '@/databases/utils/getForeignDataWrapperType';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import {
type GetOneDatabaseConnectionQuery,
type GetOneDatabaseConnectionQueryVariables,
} from '~/generated-metadata/graphql';
type UseGetDatabaseConnectionParams = {
databaseKey: string;
connectionId: string;
skip?: boolean;
fetchPolicy?: WatchQueryFetchPolicy;
};
export const useGetDatabaseConnection = ({
databaseKey,
connectionId,
skip,
fetchPolicy,
}: UseGetDatabaseConnectionParams) => {
const apolloMetadataClient = useApolloCoreClient();
const foreignDataWrapperType = getForeignDataWrapperType(databaseKey);
const fetchPolicyOption = fetchPolicy ? { fetchPolicy: fetchPolicy } : {};
const { data, loading } = useQuery<
GetOneDatabaseConnectionQuery,
GetOneDatabaseConnectionQueryVariables
>(GET_ONE_DATABASE_CONNECTION, {
client: apolloMetadataClient ?? undefined,
skip: skip || !apolloMetadataClient || !foreignDataWrapperType,
variables: {
input: {
id: connectionId,
},
},
...fetchPolicyOption,
});
const connection = data?.findOneRemoteServerById ?? null;
return {
connection:
connection?.foreignDataWrapperType === foreignDataWrapperType
? connection
: null,
loading,
};
};
@@ -1,46 +0,0 @@
import { useQuery, type WatchQueryFetchPolicy } from '@apollo/client';
import { GET_MANY_REMOTE_TABLES } from '@/databases/graphql/queries/findManyRemoteTables';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import {
type GetManyRemoteTablesQuery,
type GetManyRemoteTablesQueryVariables,
} from '~/generated-metadata/graphql';
type UseGetDatabaseConnectionTablesParams = {
connectionId: string;
skip?: boolean;
shouldFetchPendingSchemaUpdates?: boolean;
fetchPolicy?: WatchQueryFetchPolicy;
};
export const useGetDatabaseConnectionTables = ({
connectionId,
skip,
shouldFetchPendingSchemaUpdates,
fetchPolicy,
}: UseGetDatabaseConnectionTablesParams) => {
const apolloMetadataClient = useApolloCoreClient();
const fetchPolicyOption = fetchPolicy ? { fetchPolicy: fetchPolicy } : {};
const { data, error } = useQuery<
GetManyRemoteTablesQuery,
GetManyRemoteTablesQueryVariables
>(GET_MANY_REMOTE_TABLES, {
client: apolloMetadataClient ?? undefined,
skip: skip || !apolloMetadataClient,
variables: {
input: {
id: connectionId,
shouldFetchPendingSchemaUpdates,
},
},
...fetchPolicyOption,
});
return {
tables: data?.findDistantTablesWithStatus || [],
error,
};
};
@@ -1,39 +0,0 @@
import { useQuery } from '@apollo/client';
import { GET_MANY_DATABASE_CONNECTIONS } from '@/databases/graphql/queries/findManyDatabaseConnections';
import { getForeignDataWrapperType } from '@/databases/utils/getForeignDataWrapperType';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import {
type GetManyDatabaseConnectionsQuery,
type GetManyDatabaseConnectionsQueryVariables,
} from '~/generated-metadata/graphql';
type UseGetDatabaseConnectionsParams = {
databaseKey: string;
skip?: boolean;
};
export const useGetDatabaseConnections = ({
databaseKey,
skip,
}: UseGetDatabaseConnectionsParams) => {
const apolloMetadataClient = useApolloCoreClient();
const foreignDataWrapperType = getForeignDataWrapperType(databaseKey);
const { data } = useQuery<
GetManyDatabaseConnectionsQuery,
GetManyDatabaseConnectionsQueryVariables
>(GET_MANY_DATABASE_CONNECTIONS, {
client: apolloMetadataClient ?? undefined,
skip: skip || !apolloMetadataClient || !foreignDataWrapperType,
variables: {
input: {
foreignDataWrapperType: foreignDataWrapperType || '',
},
},
});
return {
connections: data?.findManyRemoteServersByType || [],
};
};
@@ -1,56 +0,0 @@
import { useMutation } from '@apollo/client';
import { useCallback } from 'react';
import { UNSYNC_REMOTE_TABLE } from '@/databases/graphql/mutations/unsyncRemoteTable';
import { modifyRemoteTableFromCache } from '@/databases/utils/modifyRemoteTableFromCache';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useFindManyObjectMetadataItems } from '@/object-metadata/hooks/useFindManyObjectMetadataItems';
import {
type RemoteTableInput,
type UnsyncRemoteTableMutation,
type UnsyncRemoteTableMutationVariables,
} from '~/generated-metadata/graphql';
import { isDefined } from 'twenty-shared/utils';
export const useUnsyncRemoteTable = () => {
const apolloMetadataClient = useApolloCoreClient();
const { refetch: refetchObjectMetadataItems } =
useFindManyObjectMetadataItems();
const [mutate] = useMutation<
UnsyncRemoteTableMutation,
UnsyncRemoteTableMutationVariables
>(UNSYNC_REMOTE_TABLE, {
client: apolloMetadataClient,
});
const unsyncRemoteTable = useCallback(
async (input: RemoteTableInput) => {
const remoteTable = await mutate({
variables: {
input,
},
update: (cache, { data }) => {
if (isDefined(data)) {
modifyRemoteTableFromCache({
cache: cache,
remoteTableName: input.name,
fieldModifiers: {
status: () => data.unsyncRemoteTable.status,
},
});
}
},
});
await refetchObjectMetadataItems();
return remoteTable;
},
[mutate, refetchObjectMetadataItems],
);
return {
unsyncRemoteTable,
};
};
@@ -1,34 +0,0 @@
import { useMutation } from '@apollo/client';
import { UPDATE_ONE_DATABASE_CONNECTION } from '@/databases/graphql/mutations/updateOneDatabaseConnection';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import {
type UpdateRemoteServerInput,
type UpdateServerMutation,
type UpdateServerMutationVariables,
} from '~/generated-metadata/graphql';
export const useUpdateOneDatabaseConnection = () => {
const apolloMetadataClient = useApolloCoreClient();
const [mutate] = useMutation<
UpdateServerMutation,
UpdateServerMutationVariables
>(UPDATE_ONE_DATABASE_CONNECTION, {
client: apolloMetadataClient,
});
const updateOneDatabaseConnection = async (
input: UpdateRemoteServerInput,
) => {
return await mutate({
variables: {
input,
},
});
};
return {
updateOneDatabaseConnection,
};
};
@@ -1,15 +0,0 @@
import { getForeignDataWrapperType } from '@/databases/utils/getForeignDataWrapperType';
describe('getForeignDataWrapperType', () => {
it('should handle postgres', () => {
expect(getForeignDataWrapperType('postgresql')).toBe('postgres_fdw');
});
it('should handle stripe', () => {
expect(getForeignDataWrapperType('stripe')).toBe('stripe_fdw');
});
it('should return null for unknown', () => {
expect(getForeignDataWrapperType('unknown')).toBeNull();
});
});
@@ -1,10 +0,0 @@
export const getForeignDataWrapperType = (databaseKey: string) => {
switch (databaseKey) {
case 'postgresql':
return 'postgres_fdw';
case 'stripe':
return 'stripe_fdw';
default:
return null;
}
};
@@ -1,22 +0,0 @@
import { type ApolloCache } from '@apollo/client';
import { type Modifiers } from '@apollo/client/cache';
import { type RemoteTable } from '~/generated-metadata/graphql';
export const modifyRemoteTableFromCache = ({
cache,
fieldModifiers,
remoteTableName,
}: {
cache: ApolloCache<object>;
fieldModifiers: Modifiers<RemoteTable>;
remoteTableName: string;
}) => {
const remoteTableCacheId = `RemoteTable:{"name":"${remoteTableName}"}`;
cache.modify({
id: remoteTableCacheId,
fields: fieldModifiers,
optimistic: true,
});
};
@@ -1,15 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" viewBox="0 0 512 144">
<rect width="539" height="143" x="-13.5" y=".5" fill="url(#a)" fill-opacity=".05" rx="7.5"/>
<rect width="539" height="143" x="-13.5" y=".5" fill="url(#b)" rx="7.5"/>
<rect width="539" height="143" x="-13.5" y=".5" stroke="#EBEBEB" rx="7.5"/>
<defs>
<radialGradient id="b" cx="0" cy="0" r="1" gradientTransform="matrix(270 0 0 487.572 256 72)" gradientUnits="userSpaceOnUse">
<stop stop-color="#FCFCFC" stop-opacity="0"/>
<stop offset="1" stop-color="#F8F8F8" stop-opacity=".9"/>
</radialGradient>
<pattern id="a" width=".044" height=".167" patternContentUnits="objectBoundingBox">
<use xlink:href="#c" transform="scale(.00035 .0013)"/>
</pattern>
<image xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAFaSURBVHgB7d2xDcAgDABBO/vvTETLCvfX0aKX5c47rvO8d0DfhFYAuALAFQCuAHAFgCsAXAHgCgBXALgCwBUArgBwBYArAFwB4AoAVwC4AsAVAK4AcAWAKwBcAeAKAFcAuALAFQCuAHAFgCsAXAHgCgBXALgCwBUArgBwBYArAFwB4AoAVwC4AsAVAK4AcAWAKwBcAeAKAFcAuALAFQCuAHAFgCsAXAHgCgBXALgCwBUArgBw92L2mbCaALgCwBUAbsf17j7kXzQBcAWAKwBcAeAKAFcAuALAFQCuAHAFgCsAXAHgCgBXALgCwBUArgBwBYArAFwB4AoAVwC4AsAVAK4AcAWAKwBcAeAKAFcAuALAFQCuAHAFgCsAXAHgCgBXALgCwBUArgBwBYArAFwB4AoAVwC4AsAVAK4AcAWAKwBcAeAKAFcAuALAFQCuAHAFgCsAXAHgCgD3A+UEA/3CEUcsAAAAAElFTkSuQmCC" id="c" width="128" height="128"/>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@@ -1,15 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 126 23">
<path stroke="#000" stroke-dasharray="4.71 4.71" stroke-linecap="round" stroke-width="1.768" d="M125 11H1"/>
<g filter="url(#a)">
<circle cx="63" cy="11" r="10.667" fill="#fff" style="mix-blend-mode:normal"/>
<circle cx="63" cy="11" r="10.203" stroke="#000" stroke-width=".928"/>
</g>
<path fill="#000" d="M60.38 8.425a3.758 3.758 0 0 1 5.223-.022l-.955.935a.54.54 0 0 0-.12.597c.085.205.29.337.514.337h2.968a.55.55 0 0 0 .557-.547V6.811a.546.546 0 0 0-.343-.506.565.565 0 0 0-.608.119l-.964.947c-2.032-1.97-5.304-1.963-7.323.023a5.06 5.06 0 0 0-1.225 1.908.725.725 0 0 0 .453.929.746.746 0 0 0 .946-.444 3.6 3.6 0 0 1 .876-1.362Zm-2.944 3.85v2.915c0 .221.135.422.344.506a.565.565 0 0 0 .607-.118l.965-.948c2.031 1.97 5.303 1.963 7.323-.023a5.077 5.077 0 0 0 1.227-1.905.725.725 0 0 0-.453-.93.746.746 0 0 0-.946.445 3.598 3.598 0 0 1-.876 1.361 3.758 3.758 0 0 1-5.225.023l.953-.938a.54.54 0 0 0 .12-.597.556.556 0 0 0-.514-.337h-2.968a.55.55 0 0 0-.556.547Z"/>
<defs>
<filter id="a" width="22.261" height="22.261" x="51.406" y=".333" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
<feOffset dx="-.93" dy=".928"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
<feBlend in="SourceGraphic" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,129 +0,0 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { type SettingsIntegration } from '@/settings/integrations/types/SettingsIntegration';
import { t } from '@lingui/core/macro';
import { Link } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
import { Pill } from 'twenty-ui/components';
import {
IconArrowUpRight,
IconBolt,
IconCopy,
IconPlus,
Status,
} from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
interface SettingsIntegrationComponentProps {
integration: SettingsIntegration;
}
const StyledContainer = styled.div<{ to?: string }>`
align-items: center;
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
font-size: ${({ theme }) => theme.font.size.md};
display: flex;
flex-direction: row;
justify-content: space-between;
padding: ${({ theme }) => theme.spacing(3)};
text-decoration: none;
color: ${({ theme }) => theme.font.color.primary};
${({ to }) =>
isDefined(to) &&
css`
cursor: pointer;
`}
`;
const StyledSection = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(3)};
`;
const StyledIntegrationLogo = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
color: ${({ theme }) => theme.border.color.strong};
`;
const StyledSoonPill = styled(Pill)`
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
`;
const StyledLogo = styled.img`
height: 24px;
width: 24px;
`;
export const SettingsIntegrationComponent = ({
integration,
}: SettingsIntegrationComponentProps) => {
const { copyToClipboard } = useCopyToClipboard();
return (
<StyledContainer
to={integration.type === 'Active' ? integration.link : undefined}
as={integration.type === 'Active' ? Link : 'div'}
>
<StyledSection>
<StyledIntegrationLogo>
<StyledLogo src={integration.from.image} alt={integration.from.key} />
{isDefined(integration.to) && (
<>
<div></div>
<StyledLogo src={integration.to.image} alt={integration.to.key} />
</>
)}
</StyledIntegrationLogo>
{integration.text}
</StyledSection>
{integration.type === 'Soon' ? (
<StyledSoonPill label={t`Soon`} />
) : integration.type === 'Active' ? (
<Status color="green" text={t`Active`} />
) : integration.type === 'Add' ? (
<Button
to={integration.link}
Icon={IconPlus}
title={t`Add`}
size="small"
/>
) : integration.type === 'Use' ? (
<Button
to={integration.link}
target="_blank"
Icon={IconBolt}
title={t`Use`}
size="small"
/>
) : integration.type === 'Copy' ? (
<Button
onClick={() => {
if (isDefined(integration.content)) {
copyToClipboard(integration.content);
}
}}
Icon={IconCopy}
title={integration.linkText}
size="small"
/>
) : (
<Button
to={integration.link}
target="_blank"
Icon={IconArrowUpRight}
title={integration.linkText}
size="small"
/>
)}
</StyledContainer>
);
};
@@ -1,63 +0,0 @@
import styled from '@emotion/styled';
import { Link } from 'react-router-dom';
import { SettingsIntegrationComponent } from '@/settings/integrations/components/SettingsIntegrationComponent';
import { type SettingsIntegrationCategory } from '@/settings/integrations/types/SettingsIntegrationCategory';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
interface SettingsIntegrationGroupProps {
integrationGroup: SettingsIntegrationCategory;
}
const StyledIntegrationGroupHeader = styled.div`
align-items: start;
display: flex;
flex-direction: row;
justify-content: space-between;
`;
const StyledGroupLink = styled(Link)`
align-items: start;
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.md};
gap: ${({ theme }) => theme.spacing(1)};
cursor: pointer;
text-decoration: none;
color: ${({ theme }) => theme.font.color.primary};
`;
const StyledIntegrationsSection = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
`;
export const SettingsIntegrationGroup = ({
integrationGroup,
}: SettingsIntegrationGroupProps) => (
<Section>
<StyledIntegrationGroupHeader>
<H2Title title={integrationGroup.title} />
{integrationGroup.hyperlink && (
<StyledGroupLink target="_blank" to={integrationGroup.hyperlink ?? ''}>
<div>{integrationGroup.hyperlinkText}</div>
<div></div>
</StyledGroupLink>
)}
</StyledIntegrationGroupHeader>
<StyledIntegrationsSection>
{integrationGroup.integrations.map((integration) => (
<SettingsIntegrationComponent
key={[
integrationGroup.key,
integration.from.key,
integration.to?.key,
].join('-')}
integration={integration}
/>
))}
</StyledIntegrationsSection>
</Section>
);
@@ -1,76 +0,0 @@
import { t } from '@lingui/core/macro';
import styled from '@emotion/styled';
import PreviewBackgroundImage from '@/settings/integrations/assets/preview-background.svg';
import SyncImage from '@/settings/integrations/assets/sync.svg?react';
import { Card, CardContent } from 'twenty-ui/layout';
type SettingsIntegrationPreviewProps = {
integrationLogoUrl: string;
};
const StyledCard = styled(Card)`
border: 0;
`;
const StyledCardContent = styled(CardContent)`
background-image: url(${PreviewBackgroundImage});
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ theme }) => theme.spacing(28)};
`;
const StyledLogosContainer = styled.div`
align-items: center;
display: flex;
justify-content: center;
gap: ${({ theme }) => theme.spacing(4)};
`;
const StyledIntegrationLogoContainer = styled.div`
align-items: center;
display: flex;
height: ${({ theme }) => theme.spacing(16)};
justify-content: center;
width: ${({ theme }) => theme.spacing(16)};
`;
const StyledIntegrationLogo = styled.img`
height: 100%;
`;
const StyledTwentyLogo = styled.img`
height: ${({ theme }) => theme.spacing(12)};
padding: ${({ theme }) => theme.spacing(2)};
`;
const StyledSyncImage = styled(SyncImage)`
width: ${({ theme }) => theme.spacing(31)};
`;
const StyledLabel = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
line-height: ${({ theme }) => theme.spacing(6)};
`;
export const SettingsIntegrationPreview = ({
integrationLogoUrl,
}: SettingsIntegrationPreviewProps) => (
<StyledCard>
<StyledCardContent>
<StyledLogosContainer>
<StyledIntegrationLogoContainer>
<StyledIntegrationLogo alt="" src={integrationLogoUrl} />
</StyledIntegrationLogoContainer>
<StyledSyncImage />
<StyledTwentyLogo alt="" src="/images/integrations/twenty-logo.svg" />
</StyledLogosContainer>
<StyledLabel>{t`Import your tables as remote objects`}</StyledLabel>
</StyledCardContent>
</StyledCard>
);
@@ -1,38 +0,0 @@
import { type FetchResult } from '@apollo/client';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { IconReload } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { type SyncRemoteTableSchemaChangesMutation } from '~/generated-metadata/graphql';
const StyledText = styled.h3`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin: 0;
`;
type SettingsIntegrationRemoteTableSchemaUpdateProps = {
updatesText: string;
onUpdate: () => Promise<FetchResult<SyncRemoteTableSchemaChangesMutation>>;
};
export const SettingsIntegrationRemoteTableSchemaUpdate = ({
updatesText,
onUpdate,
}: SettingsIntegrationRemoteTableSchemaUpdateProps) => {
return (
<>
{updatesText && <StyledText>{updatesText}</StyledText>}
{updatesText && (
<Button
Icon={IconReload}
title={t`Update`}
size="small"
onClick={onUpdate}
/>
)}
</>
);
};
@@ -1,31 +0,0 @@
import { useState } from 'react';
import { RemoteTableStatus } from '~/generated-metadata/graphql';
import { Toggle } from 'twenty-ui/input';
export const SettingsIntegrationRemoteTableSyncStatusToggle = ({
tableName,
tableStatus,
onSyncUpdate,
}: {
tableName: string;
tableStatus: RemoteTableStatus;
onSyncUpdate: (value: boolean, tableName: string) => Promise<void>;
}) => {
const [isToggleLoading, setIsToggleLoading] = useState(false);
const onChange = async (newValue: boolean) => {
if (isToggleLoading) return;
setIsToggleLoading(true);
await onSyncUpdate(newValue, tableName);
setIsToggleLoading(false);
};
return (
<Toggle
value={tableStatus === RemoteTableStatus.SYNCED}
disabled={isToggleLoading}
onChange={onChange}
/>
);
};
@@ -1,14 +0,0 @@
export const MOCK_REMOTE_DATABASES = [
{
name: 'airtable',
isActive: false,
},
{
name: 'postgresql',
isActive: true,
},
{
name: 'stripe',
isActive: true,
},
];
@@ -1,18 +0,0 @@
import { type SettingsIntegrationCategory } from '@/settings/integrations/types/SettingsIntegrationCategory';
export const SETTINGS_INTEGRATION_REQUEST_CATEGORY: SettingsIntegrationCategory =
{
key: 'request',
title: 'Request an integration',
hyperlink: null,
integrations: [
{
from: { key: 'github', image: '/images/integrations/github-logo.png' },
to: null,
type: 'Goto',
text: 'Request an integration on Github conversations',
link: 'https://github.com/twentyhq/twenty/discussions/categories/ideas',
linkText: 'Go to GitHub',
},
],
};
@@ -1,42 +0,0 @@
import { type SettingsIntegrationCategory } from '@/settings/integrations/types/SettingsIntegrationCategory';
export const SETTINGS_INTEGRATION_ZAPIER_CATEGORY: SettingsIntegrationCategory =
{
key: 'zapier',
title: 'With Zapier',
hyperlinkText: 'See all zaps',
hyperlink: 'https://zapier.com/apps/twenty/integrations',
integrations: [
{
from: { key: 'twenty', image: '/images/integrations/twenty-logo.svg' },
to: { key: 'slack', image: '/images/integrations/slack-logo.png' },
type: 'Use',
text: 'Post to Slack when a company is updated',
link: 'https://zapier.com/apps/twenty/integrations/slack',
},
{
from: { key: 'cal', image: '/images/integrations/cal-logo.png' },
to: { key: 'twenty', image: '/images/integrations/twenty-logo.svg' },
type: 'Use',
text: 'Create a person when Cal.com event is created',
link: 'https://zapier.com/apps/twenty/integrations/calcom',
},
{
from: {
key: 'mailchimp',
image: '/images/integrations/mailchimp-logo.png',
},
to: { key: 'twenty', image: '/images/integrations/twenty-logo.svg' },
type: 'Use',
text: 'Create a person when a MailChimp sub is created',
link: 'https://zapier.com/apps/twenty/integrations/mailchimp',
},
{
from: { key: 'tally', image: '/images/integrations/tally-logo.png' },
to: { key: 'twenty', image: '/images/integrations/twenty-logo.svg' },
type: 'Use',
text: 'Create a company when a Tally form is sent',
link: 'https://zapier.com/apps/twenty/integrations/tally',
},
],
};
@@ -1,22 +0,0 @@
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
const getFeatureKey = (databaseKey: string): FeatureFlagKey | null => {
switch (databaseKey) {
case 'airtable':
return FeatureFlagKey.IS_AIRTABLE_INTEGRATION_ENABLED;
case 'postgresql':
return FeatureFlagKey.IS_POSTGRESQL_INTEGRATION_ENABLED;
case 'stripe':
return FeatureFlagKey.IS_STRIPE_INTEGRATION_ENABLED;
default:
return null;
}
};
export const useIsSettingsIntegrationEnabled = (
databaseKey: string,
): boolean => {
const featureKey = getFeatureKey(databaseKey);
return useIsFeatureEnabled(featureKey);
};
@@ -1,11 +0,0 @@
import { SETTINGS_INTEGRATION_REQUEST_CATEGORY } from '@/settings/integrations/constants/SettingsIntegrationRequest';
import { SETTINGS_INTEGRATION_ZAPIER_CATEGORY } from '@/settings/integrations/constants/SettingsIntegrationZapier';
import { type SettingsIntegrationCategory } from '@/settings/integrations/types/SettingsIntegrationCategory';
export const useSettingsIntegrationCategories =
(): SettingsIntegrationCategory[] => {
return [
SETTINGS_INTEGRATION_ZAPIER_CATEGORY,
SETTINGS_INTEGRATION_REQUEST_CATEGORY,
];
};
@@ -1,17 +0,0 @@
export type SettingsIntegrationType =
| 'Active'
| 'Add'
| 'Goto'
| 'Soon'
| 'Use'
| 'Copy';
export type SettingsIntegration = {
from: { key: string; image: string };
to?: { key: string; image: string } | null;
type: SettingsIntegrationType;
linkText?: string;
content?: string;
link: string;
text: string;
};
@@ -1,9 +0,0 @@
import { type SettingsIntegration } from '@/settings/integrations/types/SettingsIntegration';
export type SettingsIntegrationCategory = {
key: string;
title: string;
hyperlinkText?: string;
hyperlink?: string | null;
integrations: SettingsIntegration[];
};
@@ -1,31 +0,0 @@
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsIntegrationGroup } from '@/settings/integrations/components/SettingsIntegrationGroup';
import { useSettingsIntegrationCategories } from '@/settings/integrations/hooks/useSettingsIntegrationCategories';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { Trans, useLingui } from '@lingui/react/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
export const SettingsIntegrations = () => {
const { t } = useLingui();
const integrationCategories = useSettingsIntegrationCategories();
return (
<SubMenuTopBarContainer
title={t`Integrations`}
links={[
{
children: <Trans>Workspace</Trans>,
href: getSettingsPath(SettingsPath.Workspace),
},
{ children: <Trans>Integrations</Trans> },
]}
>
<SettingsPageContainer>
{integrationCategories.map((group) => (
<SettingsIntegrationGroup key={group.key} integrationGroup={group} />
))}
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};
@@ -1,37 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { SettingsIntegrations } from '~/pages/settings/integrations/SettingsIntegrations';
import {
PageDecorator,
type PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { sleep } from '~/utils/sleep';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/Integrations/SettingsIntegrations',
component: SettingsIntegrations,
decorators: [PageDecorator],
args: { routePath: getSettingsPath(SettingsPath.Integrations) },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsIntegrations>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await sleep(1000);
await canvas.findByText('Go to GitHub');
},
};
@@ -16,7 +16,6 @@ import { mockedFavoriteFoldersData } from '~/testing/mock-data/favorite-folders'
import { mockedNotes } from '~/testing/mock-data/notes';
import { getPeopleRecordConnectionMock } from '~/testing/mock-data/people';
import { mockedPublicWorkspaceDataBySubdomain } from '~/testing/mock-data/publicWorkspaceDataBySubdomain';
import { mockedRemoteTables } from '~/testing/mock-data/remote-tables';
import { mockedUserData } from '~/testing/mock-data/users';
import { mockedViewsData } from '~/testing/mock-data/views';
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
@@ -35,7 +34,6 @@ import {
workflowQueryResult,
} from '~/testing/mock-data/workflow';
import { oneSucceededWorkflowRunQueryResult } from '~/testing/mock-data/workflow-run';
import { mockedRemoteServers } from './mock-data/remote-servers';
import { mockedViewFieldsData } from './mock-data/view-fields';
const peopleMock = getPeopleRecordConnectionMock();
@@ -630,20 +628,6 @@ export const graphqlMocks = {
},
});
}),
graphql.query('GetOneDatabaseConnection', () => {
return HttpResponse.json({
data: {
findOneRemoteServerById: mockedRemoteServers[0],
},
});
}),
graphql.query('GetManyRemoteTables', () => {
return HttpResponse.json({
data: {
findDistantTablesWithStatus: mockedRemoteTables,
},
});
}),
graphql.query<GraphQLQuery, { objectRecordId: string }>(
'FindOnePerson',
({ variables: { objectRecordId } }) => {
@@ -1,33 +0,0 @@
export const mockedRemoteServers = [
{
__typename: 'RemoteServer',
id: '67cbfd35-8dd4-4591-b9d4-c1906281a5da',
createdAt: '2024-04-30T13:41:25.584Z',
foreignDataWrapperId: 'b306b641-2142-4b4e-8fba-976afbc3b2bc',
foreignDataWrapperOptions: {
host: 'localhost',
port: 5432,
dbname: 'dbname_test',
},
foreignDataWrapperType: 'postgres_fdw',
userMappingOptions: {
__typename: 'UserMappingOptionsDTO',
user: 'twenty',
},
updatedAt: '2024-04-30T13:41:25.858Z',
schema: 'public',
label: 'postgres DB',
},
{
__typename: 'RemoteServer',
id: 'ddc3b641-2142-4b4e-8fba-976afbc3b2bc',
createdAt: '2024-04-30T13:41:25.584Z',
foreignDataWrapperId: 'dqfdsqf-2142-4b4e-8fba-976afbc3b2bc',
foreignDataWrapperOptions: {
api_key: 'sk_test_51',
},
foreignDataWrapperType: 'stripe_fdw',
updatedAt: '2024-04-30T13:41:25.858Z',
label: 'stripe DB',
},
];
@@ -1,16 +0,0 @@
export const mockedRemoteTables = [
{
__typename: 'RemoteTable',
id: 'bb06a39e-a792-424e-ac29-c54a395fdb4f',
name: 'Cars',
schema: 'public',
status: 'SYNCED',
},
{
__typename: 'RemoteTable',
id: '994b68c7-148d-4ee0-b557-c688c3f53f73',
name: 'Flowers',
schema: 'public',
status: 'NOT_SYNCED',
},
];
@@ -1,26 +0,0 @@
import { RemoteTableStatus } from '~/generated-metadata/graphql';
export const mockedRemoteObjectIntegrations = [
{
id: '5b717911-dc75-4876-bf33-dfdc994c88cd',
key: 'postgresql',
connections: [
{
id: '67cbfd35-8dd4-4591-b9d4-c1906281a5da',
name: 'Twenty_postgres',
tables: [
{ name: 'Invoices', status: RemoteTableStatus.NOT_SYNCED },
{ name: 'Quotes', status: RemoteTableStatus.SYNCED },
{ name: 'Customers', status: RemoteTableStatus.NOT_SYNCED },
{ name: 'Subscriptions', status: RemoteTableStatus.SYNCED },
{ name: 'Payments', status: RemoteTableStatus.NOT_SYNCED },
],
},
{
id: '3740cd85-7a1e-45b5-8b0d-47e1921d01f3',
name: 'Image_postgres',
tables: [],
},
],
},
];
@@ -4,7 +4,6 @@ import { type CurrentUserWorkspace } from '@/auth/states/currentUserWorkspaceSta
import { CUSTOM_WORKSPACE_APPLICATION_MOCK } from '@/object-metadata/hooks/__tests__/constants/CustomWorkspaceApplicationMock.test.constant';
import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import {
FeatureFlagKey,
OnboardingStatus,
PermissionFlagType,
SubscriptionInterval,
@@ -80,16 +79,7 @@ export const mockCurrentWorkspace = {
customUrl: undefined,
subdomainUrl: 'twenty.twenty.com',
},
featureFlags: [
{
key: FeatureFlagKey.IS_AIRTABLE_INTEGRATION_ENABLED,
value: true,
},
{
key: FeatureFlagKey.IS_POSTGRESQL_INTEGRATION_ENABLED,
value: true,
},
],
featureFlags: [],
createdAt: '2023-04-26T10:23:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
metadataVersion: 1,
@@ -36,8 +36,6 @@ import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RemoteServerEntity } from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import { RemoteTableEntity } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
@@ -84,7 +82,6 @@ const WORKSPACE_RELATED_ENTITIES: EntityTarget<ObjectLiteral>[] = [
ViewEntity,
IndexMetadataEntity,
PageLayoutTabEntity,
RemoteTableEntity,
RouteTriggerEntity,
CronTriggerEntity,
DatabaseEventTriggerEntity,
@@ -99,7 +96,6 @@ const WORKSPACE_RELATED_ENTITIES: EntityTarget<ObjectLiteral>[] = [
ObjectMetadataEntity,
RoleEntity,
AgentEntity,
RemoteServerEntity,
UserWorkspaceEntity,
ApiKeyEntity,
ServerlessFunctionLayerEntity,
@@ -0,0 +1,19 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class RemoteRemoteTables1767812158000 implements MigrationInterface {
name = 'RemoteRemoteTables1767812158000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "core"."remoteTable"`);
await queryRunner.query(`DROP TABLE "core"."remoteServer"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "core"."remoteTable" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "distantTableName" character varying NOT NULL, "localTableName" character varying NOT NULL, "workspaceId" uuid NOT NULL, "remoteServerId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_632b3858de52c8c2eb00c709b52" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "core"."remoteServer" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "foreignDataWrapperId" uuid NOT NULL DEFAULT uuid_generate_v4(), "foreignDataWrapperType" text, "label" text, "foreignDataWrapperOptions" jsonb, "userMappingOptions" jsonb, "schema" text, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_8e5d208498fa2c9710bb934023a" PRIMARY KEY ("id"))`,
);
}
}
@@ -1,5 +0,0 @@
import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory';
export const workspaceQueryBuilderFactories = [
ForeignDataWrapperServerQueryFactory,
];
@@ -1,69 +0,0 @@
import { Injectable } from '@nestjs/common';
import {
type ForeignDataWrapperOptions,
type RemoteServerType,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import { type UserMappingOptions } from 'src/engine/metadata-modules/remote-server/types/user-mapping-options';
@Injectable()
export class ForeignDataWrapperServerQueryFactory {
createForeignDataWrapperServer(
foreignDataWrapperId: string,
foreignDataWrapperType: RemoteServerType,
foreignDataWrapperOptions: ForeignDataWrapperOptions<RemoteServerType>,
) {
const options = this.buildQueryOptions(foreignDataWrapperOptions, false);
return `CREATE SERVER "${foreignDataWrapperId}" FOREIGN DATA WRAPPER ${foreignDataWrapperType} OPTIONS (${options})`;
}
updateForeignDataWrapperServer({
foreignDataWrapperId,
foreignDataWrapperOptions,
}: {
foreignDataWrapperId: string;
foreignDataWrapperOptions: Partial<
ForeignDataWrapperOptions<RemoteServerType>
>;
}) {
const options = this.buildQueryOptions(foreignDataWrapperOptions, true);
return `ALTER SERVER "${foreignDataWrapperId}" OPTIONS (${options})`;
}
createUserMapping(
foreignDataWrapperId: string,
userMappingOptions: UserMappingOptions,
) {
const options = this.buildQueryOptions(userMappingOptions, false);
// CURRENT_USER works for now since we are using only one user. But if we switch to a user per workspace, we need to change this.
return `CREATE USER MAPPING IF NOT EXISTS FOR CURRENT_USER SERVER "${foreignDataWrapperId}" OPTIONS (${options})`;
}
updateUserMapping(
foreignDataWrapperId: string,
userMappingOptions: Partial<UserMappingOptions>,
) {
const options = this.buildQueryOptions(userMappingOptions, true);
// CURRENT_USER works for now since we are using only one user. But if we switch to a user per workspace, we need to change this.
return `ALTER USER MAPPING FOR CURRENT_USER SERVER "${foreignDataWrapperId}" OPTIONS (${options})`;
}
private buildQueryOptions(
options:
| ForeignDataWrapperOptions<RemoteServerType>
| Partial<ForeignDataWrapperOptions<RemoteServerType>>
| UserMappingOptions
| Partial<UserMappingOptions>,
isUpdate: boolean,
) {
const prefix = isUpdate ? 'SET ' : '';
return Object.entries(options)
.map(([key, value]) => `${prefix}${key} '${value}'`)
.join(', ');
}
}
@@ -1,12 +0,0 @@
import { Module } from '@nestjs/common';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { workspaceQueryBuilderFactories } from './factories/factories';
@Module({
imports: [ObjectMetadataModule],
providers: [...workspaceQueryBuilderFactories],
exports: [],
})
export class WorkspaceQueryBuilderModule {}
@@ -1,7 +1,6 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceQueryBuilderModule } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module';
import { TelemetryListener } from 'src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener';
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
@@ -19,7 +18,6 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
@Module({
imports: [
AuthModule,
WorkspaceQueryBuilderModule,
WorkspaceDataSourceModule,
WorkspaceQueryHookModule,
TypeOrmModule.forFeature([FeatureFlagEntity]),
@@ -110,10 +110,6 @@ export type PostgresProxyTokenJwtPayload = CommonPropertiesJwtPayload & {
type: JwtTokenTypeEnum.POSTGRES_PROXY;
};
export type RemoteServerTokenJwtPayload = CommonPropertiesJwtPayload & {
type: JwtTokenTypeEnum.REMOTE_SERVER;
};
export type JwtPayload =
| AccessTokenJwtPayload
| ApiKeyTokenJwtPayload
@@ -123,5 +119,4 @@ export type JwtPayload =
| TransientTokenJwtPayload
| RefreshTokenJwtPayload
| FileTokenJwtPayload
| PostgresProxyTokenJwtPayload
| RemoteServerTokenJwtPayload;
| PostgresProxyTokenJwtPayload;
@@ -1,7 +1,4 @@
export enum FeatureFlagKey {
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_AI_ENABLED = 'IS_AI_ENABLED',
@@ -9,7 +9,6 @@ import { DatabaseEventTriggerModule } from 'src/engine/metadata-modules/database
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 { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RemoteServerModule } from 'src/engine/metadata-modules/remote-server/remote-server.module';
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
import { RouteTriggerModule } from 'src/engine/metadata-modules/route-trigger/route-trigger.module';
import { SearchFieldMetadataModule } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.module';
@@ -35,7 +34,6 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-
ViewModule,
WorkspaceMetadataVersionModule,
WorkspaceMigrationModule,
RemoteServerModule,
RoleModule,
PermissionsModule,
RouteTriggerModule,
@@ -53,7 +51,6 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-
AiAgentModule,
AiChatModule,
ViewModule,
RemoteServerModule,
RoleModule,
PermissionsModule,
],
@@ -28,7 +28,6 @@ import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metada
import { ObjectMetadataToolsFactory } from 'src/engine/metadata-modules/object-metadata/tools/object-metadata-tools.factory';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { RemoteTableRelationsModule } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.module';
import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-field.module';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
@@ -56,7 +55,6 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
WorkspaceMigrationModule,
WorkspaceMigrationRunnerModule,
WorkspaceMetadataVersionModule,
RemoteTableRelationsModule,
IndexMetadataModule,
PermissionsModule,
WorkspaceCacheStorageModule,
@@ -1,30 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsOptional } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import {
ForeignDataWrapperOptions,
type RemoteServerType,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import { UserMappingOptions } from 'src/engine/metadata-modules/remote-server/types/user-mapping-options';
@InputType()
export class CreateRemoteServerInput<T extends RemoteServerType> {
@Field(() => String)
foreignDataWrapperType: T;
@Field(() => GraphQLJSON)
foreignDataWrapperOptions: ForeignDataWrapperOptions<T>;
@Field(() => String)
label: string;
@IsOptional()
@Field(() => UserMappingOptions, { nullable: true })
userMappingOptions?: UserMappingOptions;
@IsOptional()
@Field(() => String, { nullable: true })
schema?: string;
}
@@ -1,9 +0,0 @@
import { InputType, ID } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
@InputType()
export class RemoteServerIdInput {
@IDField(() => ID, { description: 'The id of the record.' })
id!: string;
}
@@ -1,12 +0,0 @@
import { InputType, Field } from '@nestjs/graphql';
import { IsString } from 'class-validator';
import { type RemoteServerType } from 'src/engine/metadata-modules/remote-server/remote-server.entity';
@InputType()
export class RemoteServerTypeInput<T extends RemoteServerType> {
@Field(() => String)
@IsString()
foreignDataWrapperType!: T;
}
@@ -1,47 +0,0 @@
import { Field, HideField, ObjectType } from '@nestjs/graphql';
import { IsOptional } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { UserMappingOptionsDTO } from 'src/engine/metadata-modules/remote-server/dtos/user-mapping-dto';
import {
ForeignDataWrapperOptions,
type RemoteServerType,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';
@ObjectType('RemoteServer')
export class RemoteServerDTO<T extends RemoteServerType> {
@Field(() => UUIDScalarType)
id: string;
@Field(() => UUIDScalarType)
foreignDataWrapperId: string;
@Field(() => String)
foreignDataWrapperType: T;
@Field(() => String)
label: string;
@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
foreignDataWrapperOptions?: ForeignDataWrapperOptions<T>;
@IsOptional()
@Field(() => UserMappingOptionsDTO, { nullable: true })
userMappingOptions?: UserMappingOptionsDTO;
@IsOptional()
@Field(() => String, { nullable: true })
schema?: string;
@HideField()
workspaceId: string;
@Field()
createdAt: Date;
@Field()
updatedAt: Date;
}
@@ -1,36 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsOptional } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import {
type ForeignDataWrapperOptions,
type RemoteServerType,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import {
type UserMappingOptions,
UserMappingOptionsUpdateInput,
} from 'src/engine/metadata-modules/remote-server/types/user-mapping-options';
@InputType()
export class UpdateRemoteServerInput<T extends RemoteServerType> {
@Field(() => UUIDScalarType)
id: string;
@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
foreignDataWrapperOptions?: Partial<ForeignDataWrapperOptions<T>>;
@IsOptional()
@Field(() => String, { nullable: true })
label?: string;
@IsOptional()
@Field(() => UserMappingOptionsUpdateInput, { nullable: true })
userMappingOptions?: Partial<UserMappingOptions>;
@IsOptional()
@Field(() => String, { nullable: true })
schema?: string;
}
@@ -1,10 +0,0 @@
import { ObjectType, Field } from '@nestjs/graphql';
import { IsOptional } from 'class-validator';
@ObjectType('UserMappingOptionsUser')
export class UserMappingOptionsDTO {
@IsOptional()
@Field(() => String, { nullable: true })
user: string;
}
@@ -1,74 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
Generated,
OneToMany,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { RemoteTableEntity } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.entity';
import { UserMappingOptions } from 'src/engine/metadata-modules/remote-server/types/user-mapping-options';
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/workspace-sync/types/workspace-related-entity';
export enum RemoteServerType {
POSTGRES_FDW = 'postgres_fdw',
STRIPE_FDW = 'stripe_fdw',
}
type PostgresForeignDataWrapperOptions = {
host: string;
port: number;
dbname: string;
};
type StripeForeignDataWrapperOptions = {
api_key: string;
};
export type ForeignDataWrapperOptions<T extends RemoteServerType> =
T extends RemoteServerType.POSTGRES_FDW
? PostgresForeignDataWrapperOptions
: T extends RemoteServerType.STRIPE_FDW
? StripeForeignDataWrapperOptions
: never;
@Entity('remoteServer')
export class RemoteServerEntity<
T extends RemoteServerType,
> extends WorkspaceRelatedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
@Generated('uuid')
foreignDataWrapperId: string;
@Column({ type: 'text', nullable: true })
foreignDataWrapperType: T;
@Column({ type: 'text', nullable: true })
label: string;
@Column({ nullable: true, type: 'jsonb' })
foreignDataWrapperOptions: ForeignDataWrapperOptions<T>;
@Column({ nullable: true, type: 'jsonb' })
userMappingOptions: UserMappingOptions;
@Column({ type: 'text', nullable: true })
schema: string;
@OneToMany(() => RemoteTableEntity, (table) => table.server, {
cascade: true,
})
syncedTables: Relation<RemoteTableEntity[]>;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}
@@ -1,46 +0,0 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export enum RemoteServerExceptionCode {
REMOTE_SERVER_NOT_FOUND = 'REMOTE_SERVER_NOT_FOUND',
REMOTE_SERVER_ALREADY_EXISTS = 'REMOTE_SERVER_ALREADY_EXISTS',
REMOTE_SERVER_MUTATION_NOT_ALLOWED = 'REMOTE_SERVER_MUTATION_NOT_ALLOWED',
REMOTE_SERVER_CONNECTION_ERROR = 'REMOTE_SERVER_CONNECTION_ERROR',
INVALID_REMOTE_SERVER_INPUT = 'INVALID_REMOTE_SERVER_INPUT',
}
const getRemoteServerExceptionUserFriendlyMessage = (
code: RemoteServerExceptionCode,
) => {
switch (code) {
case RemoteServerExceptionCode.REMOTE_SERVER_NOT_FOUND:
return msg`Remote server not found.`;
case RemoteServerExceptionCode.REMOTE_SERVER_ALREADY_EXISTS:
return msg`Remote server already exists.`;
case RemoteServerExceptionCode.REMOTE_SERVER_MUTATION_NOT_ALLOWED:
return msg`This remote server cannot be modified.`;
case RemoteServerExceptionCode.REMOTE_SERVER_CONNECTION_ERROR:
return msg`Failed to connect to remote server.`;
case RemoteServerExceptionCode.INVALID_REMOTE_SERVER_INPUT:
return msg`Invalid remote server input.`;
default:
assertUnreachable(code);
}
};
export class RemoteServerException extends CustomException<RemoteServerExceptionCode> {
constructor(
message: string,
code: RemoteServerExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ??
getRemoteServerExceptionUserFriendlyMessage(code),
});
}
}
@@ -1,29 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RemoteServerEntity } from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import { RemoteServerResolver } from 'src/engine/metadata-modules/remote-server/remote-server.resolver';
import { RemoteServerService } from 'src/engine/metadata-modules/remote-server/remote-server.service';
import { RemoteTableModule } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@Module({
imports: [
JwtModule,
PermissionsModule,
TypeOrmModule.forFeature([RemoteServerEntity]),
RemoteTableModule,
WorkspaceDataSourceModule,
TypeOrmModule.forFeature([FeatureFlagEntity]),
],
providers: [
RemoteServerService,
RemoteServerResolver,
ForeignDataWrapperServerQueryFactory,
],
})
export class RemoteServerModule {}
@@ -1,111 +0,0 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/create-remote-server.input';
import { RemoteServerIdInput } from 'src/engine/metadata-modules/remote-server/dtos/remote-server-id.input';
import { RemoteServerTypeInput } from 'src/engine/metadata-modules/remote-server/dtos/remote-server-type.input';
import { RemoteServerDTO } from 'src/engine/metadata-modules/remote-server/dtos/remote-server.dto';
import { UpdateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/update-remote-server.input';
import { type RemoteServerType } from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import { RemoteServerService } from 'src/engine/metadata-modules/remote-server/remote-server.service';
import { remoteServerGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/remote-server/utils/remote-server-graphql-api-exception-handler.util';
@UseGuards(
WorkspaceAuthGuard,
SettingsPermissionGuard(PermissionFlagType.DATA_MODEL),
)
@UsePipes(ResolverValidationPipe)
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
@Resolver()
export class RemoteServerResolver {
constructor(
private readonly remoteServerService: RemoteServerService<RemoteServerType>,
) {}
@Mutation(() => RemoteServerDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL))
async createOneRemoteServer(
@Args('input') input: CreateRemoteServerInput<RemoteServerType>,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
try {
return await this.remoteServerService.createOneRemoteServer(
input,
workspaceId,
);
} catch (error) {
remoteServerGraphqlApiExceptionHandler(error);
}
}
@Mutation(() => RemoteServerDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL))
async updateOneRemoteServer(
@Args('input') input: UpdateRemoteServerInput<RemoteServerType>,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
try {
return await this.remoteServerService.updateOneRemoteServer(
input,
workspaceId,
);
} catch (error) {
remoteServerGraphqlApiExceptionHandler(error);
}
}
@Mutation(() => RemoteServerDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL))
async deleteOneRemoteServer(
@Args('input') { id }: RemoteServerIdInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
try {
return await this.remoteServerService.deleteOneRemoteServer(
id,
workspaceId,
);
} catch (error) {
remoteServerGraphqlApiExceptionHandler(error);
}
}
@Query(() => RemoteServerDTO)
async findOneRemoteServerById(
@Args('input') { id }: RemoteServerIdInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
try {
return await this.remoteServerService.findOneByIdWithinWorkspace(
id,
workspaceId,
);
} catch (error) {
remoteServerGraphqlApiExceptionHandler(error);
}
}
@Query(() => [RemoteServerDTO])
async findManyRemoteServersByType(
@Args('input')
{ foreignDataWrapperType }: RemoteServerTypeInput<RemoteServerType>,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
try {
return await this.remoteServerService.findManyByTypeWithinWorkspace(
foreignDataWrapperType,
workspaceId,
);
} catch (error) {
remoteServerGraphqlApiExceptionHandler(error);
}
}
}
@@ -1,299 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import isEmpty from 'lodash.isempty';
import { DataSource, type EntityManager, Repository } from 'typeorm';
import { type DeepPartial } from 'typeorm/common/DeepPartial';
import { v4 } from 'uuid';
import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory';
import { encryptText } from 'src/engine/core-modules/auth/auth.util';
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
import { type CreateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/create-remote-server.input';
import { type UpdateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/update-remote-server.input';
import {
RemoteServerEntity,
type RemoteServerType,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import {
RemoteServerException,
RemoteServerExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-server.exception';
import { RemoteTableService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.service';
import { buildUpdateRemoteServerRawQuery } from 'src/engine/metadata-modules/remote-server/utils/build-update-remote-server-raw-query.utils';
import {
validateObjectAgainstInjections,
validateStringAgainstInjections,
} from 'src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.utils';
import { validateRemoteServerType } from 'src/engine/metadata-modules/remote-server/utils/validate-remote-server-type.util';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
@Injectable()
export class RemoteServerService<T extends RemoteServerType> {
constructor(
@InjectRepository(RemoteServerEntity)
private readonly remoteServerRepository: Repository<
RemoteServerEntity<RemoteServerType>
>,
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly jwtWrapperService: JwtWrapperService,
private readonly foreignDataWrapperServerQueryFactory: ForeignDataWrapperServerQueryFactory,
private readonly remoteTableService: RemoteTableService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
@InjectRepository(FeatureFlagEntity)
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
) {}
public async createOneRemoteServer(
remoteServerInput: CreateRemoteServerInput<T>,
workspaceId: string,
): Promise<RemoteServerEntity<RemoteServerType>> {
this.validateRemoteServerInputAgainstInjections(remoteServerInput);
validateRemoteServerType(
remoteServerInput.foreignDataWrapperType,
this.featureFlagRepository,
workspaceId,
);
const foreignDataWrapperId = v4();
let remoteServerToCreate = {
...remoteServerInput,
workspaceId,
foreignDataWrapperId,
};
if (remoteServerInput.userMappingOptions) {
remoteServerToCreate = {
...remoteServerToCreate,
userMappingOptions: {
...remoteServerInput.userMappingOptions,
password: this.encryptPassword(
remoteServerInput.userMappingOptions.password,
workspaceId,
),
},
};
}
return this.coreDataSource.transaction(
async (entityManager: WorkspaceEntityManager) => {
const createdRemoteServer = entityManager.create(
RemoteServerEntity,
remoteServerToCreate,
) as RemoteServerEntity<RemoteServerType>;
const foreignDataWrapperQuery =
this.foreignDataWrapperServerQueryFactory.createForeignDataWrapperServer(
createdRemoteServer.foreignDataWrapperId,
remoteServerInput.foreignDataWrapperType,
remoteServerInput.foreignDataWrapperOptions,
);
await entityManager.query(foreignDataWrapperQuery);
if (remoteServerInput.userMappingOptions) {
const userMappingQuery =
this.foreignDataWrapperServerQueryFactory.createUserMapping(
createdRemoteServer.foreignDataWrapperId,
remoteServerInput.userMappingOptions,
);
await entityManager.query(userMappingQuery);
}
await entityManager.save(RemoteServerEntity, createdRemoteServer);
return createdRemoteServer;
},
);
}
public async updateOneRemoteServer(
remoteServerInput: UpdateRemoteServerInput<T>,
workspaceId: string,
): Promise<RemoteServerEntity<RemoteServerType>> {
this.validateRemoteServerInputAgainstInjections(remoteServerInput);
const remoteServer = await this.findOneByIdWithinWorkspace(
remoteServerInput.id,
workspaceId,
);
if (!remoteServer) {
throw new RemoteServerException(
'Remote server does not exist',
RemoteServerExceptionCode.REMOTE_SERVER_NOT_FOUND,
);
}
const currentRemoteTablesForServer =
await this.remoteTableService.findRemoteTablesByServerId({
remoteServerId: remoteServer.id,
workspaceId,
});
if (currentRemoteTablesForServer.length > 0) {
throw new RemoteServerException(
'Cannot update remote server with synchronized tables',
RemoteServerExceptionCode.REMOTE_SERVER_MUTATION_NOT_ALLOWED,
);
}
const foreignDataWrapperId = remoteServer.foreignDataWrapperId;
let partialRemoteServerWithUpdates = {
...remoteServerInput,
workspaceId,
foreignDataWrapperId,
};
if (partialRemoteServerWithUpdates?.userMappingOptions?.password) {
partialRemoteServerWithUpdates = {
...partialRemoteServerWithUpdates,
userMappingOptions: {
...partialRemoteServerWithUpdates.userMappingOptions,
password: this.encryptPassword(
partialRemoteServerWithUpdates.userMappingOptions.password,
workspaceId,
),
},
};
}
return this.coreDataSource.transaction(
async (entityManager: EntityManager) => {
const updatedRemoteServer = await this.updateRemoteServer(
partialRemoteServerWithUpdates,
);
if (
!isEmpty(partialRemoteServerWithUpdates.foreignDataWrapperOptions)
) {
const foreignDataWrapperQuery =
this.foreignDataWrapperServerQueryFactory.updateForeignDataWrapperServer(
{
foreignDataWrapperId,
foreignDataWrapperOptions:
partialRemoteServerWithUpdates.foreignDataWrapperOptions,
},
);
await entityManager.query(foreignDataWrapperQuery);
}
if (!isEmpty(partialRemoteServerWithUpdates.userMappingOptions)) {
const userMappingQuery =
this.foreignDataWrapperServerQueryFactory.updateUserMapping(
foreignDataWrapperId,
partialRemoteServerWithUpdates.userMappingOptions,
);
await entityManager.query(userMappingQuery);
}
return updatedRemoteServer;
},
);
}
public async deleteOneRemoteServer(
id: string,
workspaceId: string,
): Promise<RemoteServerEntity<RemoteServerType>> {
validateStringAgainstInjections(id);
const remoteServer = await this.remoteServerRepository.findOne({
where: {
id,
workspaceId,
},
});
if (!remoteServer) {
throw new RemoteServerException(
'Remote server does not exist',
RemoteServerExceptionCode.REMOTE_SERVER_NOT_FOUND,
);
}
await this.remoteTableService.unsyncAll(workspaceId, remoteServer);
return this.coreDataSource.transaction(
async (entityManager: EntityManager) => {
await entityManager.query(
`DROP SERVER "${remoteServer.foreignDataWrapperId}" CASCADE`,
);
await entityManager.delete(RemoteServerEntity, id);
return remoteServer;
},
);
}
public async findOneByIdWithinWorkspace(id: string, workspaceId: string) {
return this.remoteServerRepository.findOne({
where: {
id,
workspaceId,
},
});
}
public async findManyByTypeWithinWorkspace<T extends RemoteServerType>(
foreignDataWrapperType: T,
workspaceId: string,
) {
return this.remoteServerRepository.find({
where: {
foreignDataWrapperType,
workspaceId,
},
});
}
private encryptPassword(password: string, workspaceId: string) {
const key = this.jwtWrapperService.generateAppSecret(
JwtTokenTypeEnum.REMOTE_SERVER,
workspaceId,
);
return encryptText(password, key);
}
private async updateRemoteServer(
remoteServerToUpdate: DeepPartial<RemoteServerEntity<RemoteServerType>> &
Pick<RemoteServerEntity<RemoteServerType>, 'workspaceId' | 'id'>,
): Promise<RemoteServerEntity<RemoteServerType>> {
const [parameters, rawQuery] =
buildUpdateRemoteServerRawQuery(remoteServerToUpdate);
// TO DO: executeRawQuery is deprecated and will throw
const updateResult = await this.workspaceDataSourceService.executeRawQuery(
rawQuery,
parameters,
remoteServerToUpdate.workspaceId,
);
return updateResult[0][0];
}
private validateRemoteServerInputAgainstInjections(
remoteServerInput: CreateRemoteServerInput<T> | UpdateRemoteServerInput<T>,
) {
if (remoteServerInput.foreignDataWrapperOptions) {
validateObjectAgainstInjections(
remoteServerInput.foreignDataWrapperOptions,
);
}
if (remoteServerInput.userMappingOptions) {
validateObjectAgainstInjections(remoteServerInput.userMappingOptions);
}
}
}
@@ -1,41 +0,0 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import {
appendCommonExceptionCode,
CustomException,
} from 'src/utils/custom-exception';
export const DistantTableExceptionCode = appendCommonExceptionCode({
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
} as const);
const getDistantTableExceptionUserFriendlyMessage = (
code: keyof typeof DistantTableExceptionCode,
) => {
switch (code) {
case DistantTableExceptionCode.TIMEOUT_ERROR:
return msg`Request timed out.`;
case DistantTableExceptionCode.INTERNAL_SERVER_ERROR:
return msg`An unexpected error occurred.`;
default:
assertUnreachable(code);
}
};
export class DistantTableException extends CustomException<
keyof typeof DistantTableExceptionCode
> {
constructor(
message: string,
code: keyof typeof DistantTableExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ??
getDistantTableExceptionUserFriendlyMessage(code),
});
}
}
@@ -1,16 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RemoteServerEntity } from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import { DistantTableService } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.service';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@Module({
imports: [
WorkspaceDataSourceModule,
TypeOrmModule.forFeature([RemoteServerEntity]),
],
providers: [DistantTableService],
exports: [DistantTableService],
})
export class DistantTableModule {}
@@ -1,137 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource, type EntityManager } from 'typeorm';
import { v4 } from 'uuid';
import {
type RemoteServerEntity,
RemoteServerType,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import {
DistantTableException,
DistantTableExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.exception';
import { type DistantTables } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/types/distant-table';
import { STRIPE_DISTANT_TABLES } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/utils/stripe-distant-tables.util';
import { type PostgresTableSchemaColumn } from 'src/engine/metadata-modules/remote-server/types/postgres-table-schema-column';
import { isQueryTimeoutError } from 'src/engine/utils/query-timeout.util';
@Injectable()
export class DistantTableService {
constructor(
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {}
public async fetchDistantTables(
remoteServer: RemoteServerEntity<RemoteServerType>,
workspaceId: string,
tableName?: string,
): Promise<DistantTables> {
if (remoteServer.schema) {
return this.getDistantTablesFromDynamicSchema(
remoteServer,
workspaceId,
tableName,
);
}
return this.getDistantTablesFromStaticSchema(remoteServer);
}
public async getDistantTableColumns(
remoteServer: RemoteServerEntity<RemoteServerType>,
workspaceId: string,
tableName: string,
): Promise<PostgresTableSchemaColumn[]> {
const distantTables = await this.fetchDistantTables(
remoteServer,
workspaceId,
tableName,
);
return distantTables[tableName];
}
private async getDistantTablesFromDynamicSchema(
remoteServer: RemoteServerEntity<RemoteServerType>,
workspaceId: string,
tableName?: string,
): Promise<DistantTables> {
if (!remoteServer.schema) {
throw new DistantTableException(
'Remote server schema is not defined',
DistantTableExceptionCode.INTERNAL_SERVER_ERROR,
);
}
const tmpSchemaId = v4();
const tmpSchemaName = `${workspaceId}_${remoteServer.id}_${tmpSchemaId}`;
try {
const distantTables = await this.coreDataSource.transaction(
async (entityManager: EntityManager) => {
await entityManager.query(`CREATE SCHEMA "${tmpSchemaName}"`);
const tableLimitationsOptions = tableName
? ` LIMIT TO ("${tableName}")`
: '';
await entityManager.query(
`IMPORT FOREIGN SCHEMA "${remoteServer.schema}"${tableLimitationsOptions} FROM SERVER "${remoteServer.foreignDataWrapperId}" INTO "${tmpSchemaName}"`,
);
const createdForeignTableNames = await entityManager.query(
`SELECT table_name, column_name, data_type, udt_name FROM information_schema.columns WHERE table_schema = '${tmpSchemaName}'`,
);
await entityManager.query(`DROP SCHEMA "${tmpSchemaName}" CASCADE`);
return createdForeignTableNames.reduce(
// @ts-expect-error legacy noImplicitAny
(acc, { table_name, column_name, data_type, udt_name }) => {
if (!acc[table_name]) {
acc[table_name] = [];
}
acc[table_name].push({
columnName: column_name,
dataType: data_type,
udtName: udt_name,
});
return acc;
},
{},
);
},
);
return distantTables;
} catch (error) {
if (isQueryTimeoutError(error)) {
throw new DistantTableException(
`Could not find distant tables: ${error.message}`,
DistantTableExceptionCode.TIMEOUT_ERROR,
);
}
throw error;
}
}
private getDistantTablesFromStaticSchema(
remoteServer: RemoteServerEntity<RemoteServerType>,
): DistantTables {
switch (remoteServer.foreignDataWrapperType) {
case RemoteServerType.STRIPE_FDW:
return STRIPE_DISTANT_TABLES;
default:
throw new DistantTableException(
`Type ${remoteServer.foreignDataWrapperType} does not have a static schema.`,
DistantTableExceptionCode.INTERNAL_SERVER_ERROR,
);
}
}
}
@@ -1,5 +0,0 @@
import { type PostgresTableSchemaColumn } from 'src/engine/metadata-modules/remote-server/types/postgres-table-schema-column';
export type DistantTables = {
[distantTableName: string]: PostgresTableSchemaColumn[];
};
@@ -1,53 +0,0 @@
import { RemoteTableStatus } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto';
import { sortDistantTables } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/utils/sort-distant-tables.util';
const table1 = {
status: RemoteTableStatus.SYNCED,
name: 'table1',
};
const table2 = {
status: RemoteTableStatus.NOT_SYNCED,
name: 'table2',
};
describe('sortDistantTables', () => {
it('should return -1 when first param status is SYNCED and second param status is NOT_SYNCED', () => {
const result = sortDistantTables(table1, table2);
expect(result).toBe(-1);
});
it('should return 1 when first param status is NOT_SYNCED and second param status is SYNCED', () => {
const result = sortDistantTables(table2, table1);
expect(result).toBe(1);
});
it('should return -1 when same status and first param name is smaller than second param name', () => {
const result = sortDistantTables(
{ ...table1, status: RemoteTableStatus.NOT_SYNCED },
table2,
);
expect(result).toBe(-1);
});
it('should return 1 when same status and second param name is smaller than first param name', () => {
const result = sortDistantTables(table2, {
...table1,
status: RemoteTableStatus.NOT_SYNCED,
});
expect(result).toBe(1);
});
it('should be case insensitive', () => {
const result = sortDistantTables(
{ ...table1, name: 'table1', status: RemoteTableStatus.NOT_SYNCED },
{ ...table2, name: 'Table2' },
);
expect(result).toBe(-1);
});
});
@@ -1,28 +0,0 @@
import { RemoteTableStatus } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto';
export const sortDistantTables = (
table1: {
status: RemoteTableStatus;
name: string;
},
table2: {
status: RemoteTableStatus;
name: string;
},
) => {
if (
table1.status === RemoteTableStatus.SYNCED &&
table2.status === RemoteTableStatus.NOT_SYNCED
) {
return -1;
}
if (
table1.status === RemoteTableStatus.NOT_SYNCED &&
table2.status === RemoteTableStatus.SYNCED
) {
return 1;
}
return table1.name.toUpperCase() > table2.name.toUpperCase() ? 1 : -1;
};
@@ -1,194 +0,0 @@
import { type DistantTables } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/types/distant-table';
/* Commented out tables / columns are either:
- not supported by the Stripe API
- not supported by the wrapper
*/
export const STRIPE_DISTANT_TABLES: DistantTables = {
accounts: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'business_type', dataType: 'text', udtName: 'text' },
{ columnName: 'country', dataType: 'text', udtName: 'text' },
{ columnName: 'email', dataType: 'text', udtName: 'text' },
{ columnName: 'type', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
],
balance_transactions: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'amount', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'currency', dataType: 'text', udtName: 'text' },
{ columnName: 'description', dataType: 'text', udtName: 'text' },
{ columnName: 'fee', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'net', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'status', dataType: 'text', udtName: 'text' },
{ columnName: 'type', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
],
// charges: [
// { columnName: 'id', dataType: 'text', udtName: 'text' },
// { columnName: 'amount', dataType: 'bigint', udtName: 'int8' },
// { columnName: 'currency', dataType: 'text', udtName: 'text' },
// { columnName: 'customer', dataType: 'text', udtName: 'text' },
// { columnName: 'description', dataType: 'text', udtName: 'text' },
// { columnName: 'invoice', dataType: 'text', udtName: 'text' },
// { columnName: 'payment_intent', dataType: 'text', udtName: 'text' },
// { columnName: 'status', dataType: 'text', udtName: 'text' },
// { columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
// ],
customers: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'email', dataType: 'text', udtName: 'text' },
{ columnName: 'name', dataType: 'text', udtName: 'text' },
{ columnName: 'description', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
],
disputes: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'amount', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'currency', dataType: 'text', udtName: 'text' },
{ columnName: 'charge', dataType: 'text', udtName: 'text' },
{ columnName: 'payment_intent', dataType: 'text', udtName: 'text' },
{ columnName: 'reason', dataType: 'text', udtName: 'text' },
{ columnName: 'status', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
],
// events: [
// { columnName: 'id', dataType: 'text', udtName: 'text' },
// { columnName: 'type', dataType: 'text', udtName: 'text' },
// { columnName: 'api_version', dataType: 'text', udtName: 'text' },
// { columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
// ],
files: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'filename', dataType: 'text', udtName: 'text' },
{ columnName: 'purpose', dataType: 'text', udtName: 'text' },
{ columnName: 'title', dataType: 'text', udtName: 'text' },
{ columnName: 'size', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'type', dataType: 'text', udtName: 'text' },
{ columnName: 'url', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
{ columnName: 'expires_at', dataType: 'timestamp', udtName: 'timestamp' },
],
file_links: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'file', dataType: 'text', udtName: 'text' },
{ columnName: 'url', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
{ columnName: 'expired', dataType: 'bool', udtName: 'boolean' },
{ columnName: 'expires_at', dataType: 'timestamp', udtName: 'timestamp' },
],
invoices: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'customer', dataType: 'text', udtName: 'text' },
{ columnName: 'subscription', dataType: 'text', udtName: 'text' },
{ columnName: 'status', dataType: 'text', udtName: 'text' },
{ columnName: 'total', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'currency', dataType: 'text', udtName: 'text' },
{ columnName: 'period_start', dataType: 'timestamp', udtName: 'timestamp' },
{ columnName: 'period_end', dataType: 'timestamp', udtName: 'timestamp' },
],
mandates: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'payment_method', dataType: 'text', udtName: 'text' },
{ columnName: 'status', dataType: 'text', udtName: 'text' },
{ columnName: 'type', dataType: 'text', udtName: 'text' },
],
payouts: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'amount', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'currency', dataType: 'text', udtName: 'text' },
// { columnName: 'arrival_date', dataType: 'text', udtName: 'text' },
{ columnName: 'description', dataType: 'text', udtName: 'text' },
// { columnName: 'statement_descriptor', dataType: 'text', udtName: 'text' },
{ columnName: 'status', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
],
prices: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'active', dataType: 'bool', udtName: 'boolean' },
{ columnName: 'currency', dataType: 'text', udtName: 'text' },
{ columnName: 'product', dataType: 'text', udtName: 'text' },
// { columnName: 'unit_amount', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'type', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
],
products: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'name', dataType: 'text', udtName: 'text' },
{ columnName: 'active', dataType: 'bool', udtName: 'boolean' },
// { columnName: 'default_price', dataType: 'text', udtName: 'text' },
{ columnName: 'description', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
{ columnName: 'updated', dataType: 'timestamp', udtName: 'timestamp' },
],
refunds: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'amount', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'currency', dataType: 'text', udtName: 'text' },
{ columnName: 'charge', dataType: 'text', udtName: 'text' },
{ columnName: 'payment_intent', dataType: 'text', udtName: 'text' },
{ columnName: 'reason', dataType: 'text', udtName: 'text' },
{ columnName: 'status', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
],
// setup_attempts: [
// { columnName: 'id', dataType: 'text', udtName: 'text' },
// { columnName: 'application', dataType: 'text', udtName: 'text' },
// { columnName: 'customer', dataType: 'text', udtName: 'text' },
// { columnName: 'on_behalf_of', dataType: 'text', udtName: 'text' },
// { columnName: 'payment_method', dataType: 'text', udtName: 'text' },
// { columnName: 'setup_intent', dataType: 'text', udtName: 'text' },
// { columnName: 'status', dataType: 'text', udtName: 'text' },
// { columnName: 'usage', dataType: 'text', udtName: 'text' },
// { columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
// ],
// setup_intents: [
// { columnName: 'id', dataType: 'text', udtName: 'text' },
// { columnName: 'client_secret', dataType: 'text', udtName: 'text' },
// { columnName: 'description', dataType: 'text', udtName: 'text' },
// { columnName: 'customer', dataType: 'text', udtName: 'text' },
// { columnName: 'payment_method', dataType: 'text', udtName: 'text' },
// { columnName: 'status', dataType: 'text', udtName: 'text' },
// { columnName: 'usage', dataType: 'text', udtName: 'text' },
// { columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
// ],
subscriptions: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'customer', dataType: 'text', udtName: 'text' },
{ columnName: 'currency', dataType: 'text', udtName: 'text' },
// {
// columnName: 'current_period_start',
// dataType: 'timestamp',
// udtName: 'timestamp',
// },
// {
// columnName: 'current_period_end',
// dataType: 'timestamp',
// udtName: 'timestamp',
// },
],
// tokens: [
// { columnName: 'id', dataType: 'text', udtName: 'text' },
// { columnName: 'type', dataType: 'text', udtName: 'text' },
// { columnName: 'client_ip', dataType: 'text', udtName: 'text' },
// { columnName: 'used', dataType: 'bool', udtName: 'boolean' },
// { columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
// ],
topups: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'amount', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'currency', dataType: 'text', udtName: 'text' },
{ columnName: 'description', dataType: 'text', udtName: 'text' },
{ columnName: 'status', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
],
transfers: [
{ columnName: 'id', dataType: 'text', udtName: 'text' },
{ columnName: 'amount', dataType: 'bigint', udtName: 'int8' },
{ columnName: 'currency', dataType: 'text', udtName: 'text' },
{ columnName: 'description', dataType: 'text', udtName: 'text' },
{ columnName: 'destination', dataType: 'text', udtName: 'text' },
{ columnName: 'created', dataType: 'timestamp', udtName: 'timestamp' },
],
};
@@ -1,18 +0,0 @@
import { InputType, ID, Field } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { IsOptional } from 'class-validator';
@InputType()
export class FindManyRemoteTablesInput {
@IDField(() => ID, { description: 'The id of the remote server.' })
id!: string;
@IsOptional()
@Field(() => Boolean, {
description:
'Indicates if pending schema updates status should be computed.',
nullable: true,
})
shouldFetchPendingSchemaUpdates?: boolean;
}
@@ -1,12 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class RemoteTableInput {
@Field(() => UUIDScalarType)
remoteServerId: string;
@Field(() => String)
name: string;
}
@@ -1,48 +0,0 @@
import { ObjectType, Field, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { IsOptional } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
export enum RemoteTableStatus {
SYNCED = 'SYNCED',
NOT_SYNCED = 'NOT_SYNCED',
}
export enum DistantTableUpdate {
TABLE_DELETED = 'TABLE_DELETED',
COLUMNS_DELETED = 'COLUMNS_DELETED',
COLUMNS_ADDED = 'COLUMNS_ADDED',
COLUMNS_TYPE_CHANGED = 'COLUMNS_TYPE_CHANGED',
}
registerEnumType(RemoteTableStatus, {
name: 'RemoteTableStatus',
description: 'Status of the table',
});
registerEnumType(DistantTableUpdate, {
name: 'DistantTableUpdate',
description: 'Schema update on a table',
});
@ObjectType('RemoteTable')
export class RemoteTableDTO {
@IDField(() => UUIDScalarType, { nullable: true })
id?: string;
@Field(() => String)
name: string;
@Field(() => RemoteTableStatus)
status: RemoteTableStatus;
@IsOptional()
@Field(() => String, { nullable: true })
schema?: string;
@IsOptional()
@Field(() => [DistantTableUpdate], { nullable: true })
schemaPendingUpdates?: [DistantTableUpdate];
}
@@ -1,37 +0,0 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export enum ForeignTableExceptionCode {
FOREIGN_TABLE_MUTATION_NOT_ALLOWED = 'FOREIGN_TABLE_MUTATION_NOT_ALLOWED',
INVALID_FOREIGN_TABLE_INPUT = 'INVALID_FOREIGN_TABLE_INPUT',
}
const getForeignTableExceptionUserFriendlyMessage = (
code: ForeignTableExceptionCode,
) => {
switch (code) {
case ForeignTableExceptionCode.FOREIGN_TABLE_MUTATION_NOT_ALLOWED:
return msg`This foreign table cannot be modified.`;
case ForeignTableExceptionCode.INVALID_FOREIGN_TABLE_INPUT:
return msg`Invalid foreign table input.`;
default:
assertUnreachable(code);
}
};
export class ForeignTableException extends CustomException<ForeignTableExceptionCode> {
constructor(
message: string,
code: ForeignTableExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ??
getForeignTableExceptionUserFriendlyMessage(code),
});
}
}
@@ -1,19 +0,0 @@
import { Module } from '@nestjs/common';
import { ForeignTableService } from 'src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.service';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
@Module({
imports: [
WorkspaceMigrationModule,
WorkspaceMigrationRunnerModule,
WorkspaceDataSourceModule,
WorkspaceMetadataVersionModule,
],
providers: [ForeignTableService],
exports: [ForeignTableService],
})
export class ForeignTableModule {}
@@ -1,190 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import {
type 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 {
ForeignTableException,
ForeignTableExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.exception';
import { getForeignTableColumnName } from 'src/engine/metadata-modules/remote-server/remote-table/foreign-table/utils/get-foreign-table-column-name.util';
import { type PostgresTableSchemaColumn } from 'src/engine/metadata-modules/remote-server/types/postgres-table-schema-column';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import {
type ReferencedTable,
type WorkspaceMigrationColumnAction,
type WorkspaceMigrationForeignColumnDefinition,
type WorkspaceMigrationForeignTable,
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
@Injectable()
export class ForeignTableService {
constructor(
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {}
public async fetchForeignTableNamesWithinWorkspace(
_workspaceId: string,
foreignDataWrapperId: string,
): Promise<string[]> {
return (
(
await this.coreDataSource.query(
`SELECT foreign_table_name, foreign_server_name FROM information_schema.foreign_tables WHERE foreign_server_name = $1`,
[foreignDataWrapperId],
)
)
// @ts-expect-error legacy noImplicitAny
.map((foreignTable) => foreignTable.foreign_table_name)
);
}
public async createForeignTable(
workspaceId: string,
localTableName: string,
remoteServer: RemoteServerEntity<RemoteServerType>,
distantTableName: string,
distantTableColumns: PostgresTableSchemaColumn[],
) {
const referencedTable: ReferencedTable = this.buildReferencedTable(
remoteServer,
distantTableName,
);
const workspaceMigration =
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`create-foreign-table-${localTableName}`),
workspaceId,
[
{
name: localTableName,
action: WorkspaceMigrationTableActionType.CREATE_FOREIGN_TABLE,
foreignTable: {
columns: distantTableColumns.map(
(column) =>
({
columnName: getForeignTableColumnName(column.columnName),
columnType: column.dataType,
distantColumnName: column.columnName,
isNullable: false,
defaultValue: null,
}) satisfies WorkspaceMigrationForeignColumnDefinition,
),
referencedTable,
foreignDataWrapperId: remoteServer.foreignDataWrapperId,
} satisfies WorkspaceMigrationForeignTable,
},
],
);
// TODO: This should be done in a transaction. Waiting for a global refactoring of transaction management.
try {
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);
} catch {
this.workspaceMigrationService.deleteById(workspaceMigration.id);
throw new ForeignTableException(
'Could not create foreign table. The table may already exists or a column type may not be supported.',
ForeignTableExceptionCode.INVALID_FOREIGN_TABLE_INPUT,
);
}
}
public async updateForeignTable(
foreignTableName: string,
workspaceId: string,
columnsUpdates: WorkspaceMigrationColumnAction[],
) {
const workspaceMigration =
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`alter-foreign-table-${foreignTableName}`),
workspaceId,
[
{
name: foreignTableName,
action: WorkspaceMigrationTableActionType.ALTER_FOREIGN_TABLE,
columns: columnsUpdates,
},
],
);
// TODO: This should be done in a transaction. Waiting for a global refactoring of transaction management.
try {
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
return {
name: foreignTableName,
status: RemoteTableStatus.SYNCED,
schemaPendingUpdates: [],
};
} catch {
this.workspaceMigrationService.deleteById(workspaceMigration.id);
throw new ForeignTableException(
'Could not alter foreign table.',
ForeignTableExceptionCode.FOREIGN_TABLE_MUTATION_NOT_ALLOWED,
);
}
}
public async deleteForeignTable(
foreignTableName: string,
workspaceId: string,
) {
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`drop-foreign-table-${foreignTableName}`),
workspaceId,
[
{
name: foreignTableName,
action: WorkspaceMigrationTableActionType.DROP_FOREIGN_TABLE,
},
],
);
return this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);
}
private buildReferencedTable(
remoteServer: RemoteServerEntity<RemoteServerType>,
distantTableName: string,
): ReferencedTable {
switch (remoteServer.foreignDataWrapperType) {
case RemoteServerType.POSTGRES_FDW:
return {
table_name: distantTableName,
schema_name: remoteServer.schema,
};
case RemoteServerType.STRIPE_FDW:
return { object: distantTableName };
default:
throw new ForeignTableException(
'Foreign data wrapper not supported',
ForeignTableExceptionCode.INVALID_FOREIGN_TABLE_INPUT,
);
}
}
}
@@ -1,5 +0,0 @@
import camelCase from 'lodash.camelcase';
export const getForeignTableColumnName = (distantTableColumnName: string) => {
return camelCase(distantTableColumnName);
};
@@ -1,17 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
@Module({
imports: [
TypeOrmModule.forFeature([ObjectMetadataEntity, FieldMetadataEntity]),
WorkspaceMigrationModule,
],
providers: [RemoteTableRelationsService],
exports: [RemoteTableRelationsService],
})
export class RemoteTableRelationsModule {}
@@ -1,287 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {
type FieldMetadataSettings,
type FieldMetadataType,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { buildMigrationsToCreateRemoteTableRelations } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/utils/build-migrations-to-create-remote-table-relations.util';
import { buildMigrationsToRemoveRemoteTableRelations } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/utils/build-migrations-to-remove-remote-table-relations.util';
import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import {
ATTACHMENT_STANDARD_FIELD_IDS,
FAVORITE_STANDARD_FIELD_IDS,
TIMELINE_ACTIVITY_STANDARD_FIELD_IDS,
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { createForeignKeyDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
@Injectable()
export class RemoteTableRelationsService {
constructor(
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
private readonly workspaceMigrationService: WorkspaceMigrationService,
) {}
public async createForeignKeysMetadataAndMigrations(
workspaceId: string,
remoteObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType>
| undefined,
objectPrimaryKeyColumnType?: string,
) {
const objectPrimaryKeyFieldType = mapUdtNameToFieldType(
objectPrimaryKeyColumnType ?? 'uuid',
);
const favoriteObjectMetadata = await this.createFavoriteRelation(
workspaceId,
remoteObjectMetadata,
objectPrimaryKeyFieldType,
objectPrimaryKeyFieldSettings,
);
const attachmentObjectMetadata = await this.createAttachmentRelation(
workspaceId,
remoteObjectMetadata,
objectPrimaryKeyFieldType,
objectPrimaryKeyFieldSettings,
);
const timelineActivityObjectMetadata =
await this.createTimelineActivityRelation(
workspaceId,
remoteObjectMetadata,
objectPrimaryKeyFieldType,
objectPrimaryKeyFieldSettings,
);
// create migration to add foreign key columns
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`add-foreign-keys-${remoteObjectMetadata.nameSingular}`,
),
workspaceId,
buildMigrationsToCreateRemoteTableRelations(
remoteObjectMetadata.nameSingular,
[
favoriteObjectMetadata,
attachmentObjectMetadata,
timelineActivityObjectMetadata,
],
objectPrimaryKeyColumnType ?? 'uuid',
),
);
}
public async deleteForeignKeysMetadataAndCreateMigrations(
workspaceId: string,
remoteObjectMetadata: ObjectMetadataEntity,
) {
// find favorite, activityTarget, attachment, timelineActivity objects
const favoriteObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
nameSingular: 'favorite',
workspaceId: workspaceId,
});
const attachmentObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
nameSingular: 'attachment',
workspaceId: workspaceId,
});
const timelineActivityObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
nameSingular: 'timelineActivity',
workspaceId: workspaceId,
});
// compute the target column name
const targetColumnName = `${remoteObjectMetadata.nameSingular}Id`;
// find the foreign key fields to delete
const foreignKeyFieldsToDelete = await this.fieldMetadataRepository.find({
where: {
name: targetColumnName,
objectMetadataId: In([
favoriteObjectMetadata.id,
attachmentObjectMetadata.id,
timelineActivityObjectMetadata.id,
]),
workspaceId,
},
});
const foreignKeyFieldsToDeleteIds = foreignKeyFieldsToDelete.map(
(field) => field.id,
);
await this.fieldMetadataRepository.delete(foreignKeyFieldsToDeleteIds);
// create migration to drop foreign key columns
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`delete-foreign-keys-${remoteObjectMetadata.nameSingular}`,
),
workspaceId,
buildMigrationsToRemoveRemoteTableRelations(targetColumnName, [
favoriteObjectMetadata,
attachmentObjectMetadata,
timelineActivityObjectMetadata,
]),
);
}
private async createAttachmentRelation(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType>
| undefined,
) {
const attachmentObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
nameSingular: 'attachment',
workspaceId: workspaceId,
});
await this.fieldMetadataRepository.save(
// Foreign key
{
standardId: createForeignKeyDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: ATTACHMENT_STANDARD_FIELD_IDS.custom,
}),
objectMetadataId: attachmentObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: objectPrimaryKeyType,
name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
description: `Attachment ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
...(isDefined(objectPrimaryKeyFieldSettings)
? {
settings: {
...objectPrimaryKeyFieldSettings,
isForeignKey: true,
},
}
: {}),
},
);
return attachmentObjectMetadata;
}
private async createTimelineActivityRelation(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType>
| undefined,
) {
const timelineActivityObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
nameSingular: 'timelineActivity',
workspaceId: workspaceId,
});
await this.fieldMetadataRepository.save(
// Foreign key
{
standardId: createForeignKeyDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetCustom,
}),
objectMetadataId: timelineActivityObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: objectPrimaryKeyType,
name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
description: `Timeline Activity ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
...(isDefined(objectPrimaryKeyFieldSettings)
? {
settings: {
...objectPrimaryKeyFieldSettings,
isForeignKey: true,
},
}
: {}),
},
);
return timelineActivityObjectMetadata;
}
private async createFavoriteRelation(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType>
| undefined,
) {
const favoriteObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
nameSingular: 'favorite',
workspaceId: workspaceId,
});
await this.fieldMetadataRepository.save(
// Foreign key
{
standardId: createForeignKeyDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: FAVORITE_STANDARD_FIELD_IDS.custom,
}),
objectMetadataId: favoriteObjectMetadata.id,
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: objectPrimaryKeyType,
name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
description: `Favorite ${createdObjectMetadata.labelSingular} id foreign key`,
icon: undefined,
isNullable: true,
isSystem: true,
defaultValue: undefined,
...(isDefined(objectPrimaryKeyFieldSettings)
? {
settings: {
...objectPrimaryKeyFieldSettings,
isForeignKey: true,
},
}
: {}),
},
);
return favoriteObjectMetadata;
}
}
@@ -1,30 +0,0 @@
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
WorkspaceMigrationColumnActionType,
type WorkspaceMigrationColumnCreate,
type WorkspaceMigrationTableAction,
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
export const buildMigrationsToCreateRemoteTableRelations = (
createdObjectNameSingular: string,
targetObjectMetadataList: ObjectMetadataEntity[],
primaryKeyColumnType: string,
): WorkspaceMigrationTableAction[] =>
targetObjectMetadataList.map((targetObjectMetadata) => ({
name: computeObjectTargetTable(targetObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: computeColumnName(createdObjectNameSingular, {
isForeignKey: true,
}),
columnType: primaryKeyColumnType,
isNullable: true,
defaultValue: null,
} satisfies WorkspaceMigrationColumnCreate,
],
}));
@@ -1,26 +0,0 @@
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
WorkspaceMigrationColumnActionType,
type WorkspaceMigrationColumnDrop,
type WorkspaceMigrationTableAction,
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
export const buildMigrationsToRemoveRemoteTableRelations = (
targetColumnName: string,
targetObjectMetadataList: ObjectMetadataEntity[],
): WorkspaceMigrationTableAction[] =>
targetObjectMetadataList.map((objectMetadata) => ({
name: computeTableName(
objectMetadata.nameSingular,
objectMetadata.isCustom,
),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName: targetColumnName,
} satisfies WorkspaceMigrationColumnDrop,
],
}));
@@ -1,11 +0,0 @@
import { Module } from '@nestjs/common';
import { RemoteTableSchemaUpdateService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-schema-update/remote-table-schema-update.service';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@Module({
imports: [WorkspaceDataSourceModule],
providers: [RemoteTableSchemaUpdateService],
exports: [RemoteTableSchemaUpdateService],
})
export class RemoteTableSchemaUpdateModule {}
@@ -1,133 +0,0 @@
import { Injectable } from '@nestjs/common';
import { type DistantTables } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/types/distant-table';
import { DistantTableUpdate } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto';
import { getForeignTableColumnName as convertToForeignTableColumnName } from 'src/engine/metadata-modules/remote-server/remote-table/foreign-table/utils/get-foreign-table-column-name.util';
import { type RemoteTableEntity } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.entity';
import { fetchTableColumns } from 'src/engine/metadata-modules/remote-server/remote-table/utils/fetch-table-columns.util';
import { type PostgresTableSchemaColumn } from 'src/engine/metadata-modules/remote-server/types/postgres-table-schema-column';
import {
type WorkspaceMigrationColumnAction,
WorkspaceMigrationColumnActionType,
type WorkspaceMigrationColumnCreate,
type WorkspaceMigrationColumnDrop,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
@Injectable()
export class RemoteTableSchemaUpdateService {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
public computeForeignTableColumnsUpdates = (
foreignTableColumns: PostgresTableSchemaColumn[],
distantTableColumns: PostgresTableSchemaColumn[],
): WorkspaceMigrationColumnAction[] => {
const { columnsAdded, columnsDeleted } = this.compareForeignTableColumns(
foreignTableColumns,
distantTableColumns,
);
const columnsAddedUpdates: WorkspaceMigrationColumnCreate[] =
columnsAdded.map((columnAdded) => ({
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: columnAdded.name,
columnType: columnAdded.type,
isNullable: true,
defaultValue: null,
}));
const columnsDeletedUpdates: WorkspaceMigrationColumnDrop[] =
columnsDeleted.map((columnDeleted) => ({
action: WorkspaceMigrationColumnActionType.DROP,
columnName: columnDeleted,
}));
return [...columnsAddedUpdates, ...columnsDeletedUpdates];
};
public async getSchemaUpdatesBetweenForeignAndDistantTables({
workspaceId,
remoteTables,
distantTables,
}: {
workspaceId: string;
remoteTables: RemoteTableEntity[];
distantTables: DistantTables;
}): Promise<{ [tablename: string]: DistantTableUpdate[] }> {
const updates = {};
for (const remoteTable of remoteTables) {
const distantTable = distantTables[remoteTable.distantTableName];
const tableName = remoteTable.distantTableName;
if (!distantTable) {
// @ts-expect-error legacy noImplicitAny
updates[tableName] = [DistantTableUpdate.TABLE_DELETED];
continue;
}
const foreignTable = await fetchTableColumns(
this.workspaceDataSourceService,
workspaceId,
remoteTable.localTableName,
);
const { columnsAdded, columnsDeleted } = this.compareForeignTableColumns(
foreignTable,
distantTable,
);
if (columnsAdded.length > 0) {
// @ts-expect-error legacy noImplicitAny
updates[tableName] = [
// @ts-expect-error legacy noImplicitAny
...(updates[tableName] || []),
DistantTableUpdate.COLUMNS_ADDED,
];
}
if (columnsDeleted.length > 0) {
// @ts-expect-error legacy noImplicitAny
updates[tableName] = [
// @ts-expect-error legacy noImplicitAny
...(updates[tableName] || []),
DistantTableUpdate.COLUMNS_DELETED,
];
}
}
return updates;
}
private compareForeignTableColumns = (
foreignTableColumns: PostgresTableSchemaColumn[],
distantTableColumns: PostgresTableSchemaColumn[],
) => {
const foreignTableColumnNames = new Set(
foreignTableColumns.map((column) => column.columnName),
);
const distantTableColumnsWithConvertedName = distantTableColumns.map(
(column) => {
return {
name: convertToForeignTableColumnName(column.columnName),
type: column.dataType,
};
},
);
const columnsAdded = distantTableColumnsWithConvertedName.filter(
(column) => !foreignTableColumnNames.has(column.name),
);
const columnsDeleted = Array.from(foreignTableColumnNames).filter(
(columnName) =>
!distantTableColumnsWithConvertedName
.map((column) => column.name)
.includes(columnName),
);
return {
columnsAdded,
columnsDeleted,
};
};
}
@@ -1,43 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import {
RemoteServerEntity,
type RemoteServerType,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/workspace-sync/types/workspace-related-entity';
@Entity('remoteTable')
export class RemoteTableEntity extends WorkspaceRelatedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false })
distantTableName: string;
@Column({ nullable: false })
localTableName: string;
@Column({ nullable: false, type: 'uuid' })
remoteServerId: string;
@ManyToOne(() => RemoteServerEntity, (server) => server.syncedTables, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'remoteServerId' })
server: Relation<RemoteServerEntity<RemoteServerType>>;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}
@@ -1,48 +0,0 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export enum RemoteTableExceptionCode {
REMOTE_TABLE_NOT_FOUND = 'REMOTE_TABLE_NOT_FOUND',
INVALID_REMOTE_TABLE_INPUT = 'INVALID_REMOTE_TABLE_INPUT',
REMOTE_TABLE_ALREADY_EXISTS = 'REMOTE_TABLE_ALREADY_EXISTS',
NO_FOREIGN_TABLES_FOUND = 'NO_FOREIGN_TABLES_FOUND',
NO_OBJECT_METADATA_FOUND = 'NO_OBJECT_METADATA_FOUND',
NO_FIELD_METADATA_FOUND = 'NO_FIELD_METADATA_FOUND',
}
const getRemoteTableExceptionUserFriendlyMessage = (
code: RemoteTableExceptionCode,
) => {
switch (code) {
case RemoteTableExceptionCode.REMOTE_TABLE_NOT_FOUND:
return msg`Remote table not found.`;
case RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT:
return msg`Invalid remote table input.`;
case RemoteTableExceptionCode.REMOTE_TABLE_ALREADY_EXISTS:
return msg`Remote table already exists.`;
case RemoteTableExceptionCode.NO_FOREIGN_TABLES_FOUND:
return msg`No foreign tables found.`;
case RemoteTableExceptionCode.NO_OBJECT_METADATA_FOUND:
return msg`Object metadata not found.`;
case RemoteTableExceptionCode.NO_FIELD_METADATA_FOUND:
return msg`Field metadata not found.`;
default:
assertUnreachable(code);
}
};
export class RemoteTableException extends CustomException<RemoteTableExceptionCode> {
constructor(
message: string,
code: RemoteTableExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ?? getRemoteTableExceptionUserFriendlyMessage(code),
});
}
}
@@ -1,34 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
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 { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RemoteServerEntity } from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import { DistantTableModule } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.module';
import { ForeignTableModule } from 'src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.module';
import { RemoteTableSchemaUpdateModule } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-schema-update/remote-table-schema-update.module';
import { RemoteTableEntity } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.entity';
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 { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@Module({
imports: [
DistantTableModule,
TypeOrmModule.forFeature([RemoteServerEntity, RemoteTableEntity]),
DataSourceModule,
ObjectMetadataModule,
FieldMetadataModule,
PermissionsModule,
WorkspaceMetadataVersionModule,
WorkspaceDataSourceModule,
ForeignTableModule,
RemoteTableSchemaUpdateModule,
],
providers: [RemoteTableService, RemoteTableResolver],
exports: [RemoteTableService],
})
export class RemoteTableModule {}
@@ -1,88 +0,0 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { FindManyRemoteTablesInput } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/find-many-remote-tables-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';
import { remoteTableGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/remote-server/remote-table/utils/remote-table-graphql-api-exception-handler.util';
@UseGuards(
WorkspaceAuthGuard,
SettingsPermissionGuard(PermissionFlagType.DATA_MODEL),
)
@UsePipes(ResolverValidationPipe)
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
@Resolver()
export class RemoteTableResolver {
constructor(private readonly remoteTableService: RemoteTableService) {}
@Query(() => [RemoteTableDTO])
async findDistantTablesWithStatus(
@Args('input') input: FindManyRemoteTablesInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
try {
return await this.remoteTableService.findDistantTablesWithStatus(
input.id,
workspaceId,
input.shouldFetchPendingSchemaUpdates,
);
} catch (error) {
remoteTableGraphqlApiExceptionHandler(error);
}
}
@Mutation(() => RemoteTableDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL))
async syncRemoteTable(
@Args('input') input: RemoteTableInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
try {
return await this.remoteTableService.syncRemoteTable(input, workspaceId);
} catch (error) {
remoteTableGraphqlApiExceptionHandler(error);
}
}
@Mutation(() => RemoteTableDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL))
async unsyncRemoteTable(
@Args('input') input: RemoteTableInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
try {
return await this.remoteTableService.unsyncRemoteTable(
input,
workspaceId,
);
} catch (error) {
remoteTableGraphqlApiExceptionHandler(error);
}
}
@Mutation(() => RemoteTableDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL))
async syncRemoteTableSchemaChanges(
@Args('input') input: RemoteTableInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
try {
return await this.remoteTableService.syncRemoteTableSchemaChanges(
input,
workspaceId,
);
} catch (error) {
remoteTableGraphqlApiExceptionHandler(error);
}
}
}
@@ -1,626 +0,0 @@
import { Logger } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import isEmpty from 'lodash.isempty';
import { plural } from 'pluralize';
import { DataSource, Repository } from 'typeorm';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import {
RemoteServerEntity,
type RemoteServerType,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import { DistantTableService } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.service';
import { sortDistantTables } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/utils/sort-distant-tables.util';
import { type RemoteTableInput } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table-input';
import {
DistantTableUpdate,
RemoteTableStatus,
} from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto';
import { ForeignTableService } from 'src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.service';
import { RemoteTableSchemaUpdateService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-schema-update/remote-table-schema-update.service';
import { RemoteTableEntity } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.entity';
import {
RemoteTableException,
RemoteTableExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.exception';
import { fetchTableColumns } from 'src/engine/metadata-modules/remote-server/remote-table/utils/fetch-table-columns.util';
import { getRemoteTableLocalName } from 'src/engine/metadata-modules/remote-server/remote-table/utils/get-remote-table-local-name.util';
import {
mapUdtNameToFieldSettings,
mapUdtNameToFieldType,
} from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util';
import { type PostgresTableSchemaColumn } from 'src/engine/metadata-modules/remote-server/types/postgres-table-schema-column';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import {
type WorkspaceMigrationColumnAction,
WorkspaceMigrationColumnActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { camelCase } from 'src/utils/camel-case';
import { camelToTitleCase } from 'src/utils/camel-to-title-case';
export class RemoteTableService {
private readonly logger = new Logger(RemoteTableService.name);
constructor(
@InjectRepository(RemoteTableEntity)
private readonly remoteTableRepository: Repository<RemoteTableEntity>,
@InjectRepository(RemoteServerEntity)
private readonly remoteServerRepository: Repository<
RemoteServerEntity<RemoteServerType>
>,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
private readonly dataSourceService: DataSourceService,
private readonly objectMetadataService: ObjectMetadataService,
private readonly fieldMetadataService: FieldMetadataService,
private readonly distantTableService: DistantTableService,
private readonly foreignTableService: ForeignTableService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly remoteTableSchemaUpdateService: RemoteTableSchemaUpdateService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {}
public async findDistantTablesWithStatus(
id: string,
workspaceId: string,
shouldFetchPendingSchemaUpdates?: boolean,
) {
const remoteServer = await this.remoteServerRepository.findOne({
where: {
id,
workspaceId,
},
});
if (!remoteServer) {
throw new RemoteTableException(
'Remote server does not exist',
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
);
}
const currentRemoteTables = await this.findRemoteTablesByServerId({
remoteServerId: id,
workspaceId,
});
const currentRemoteTableDistantNames = currentRemoteTables.map(
(remoteTable) => remoteTable.distantTableName,
);
const distantTables = await this.distantTableService.fetchDistantTables(
remoteServer,
workspaceId,
);
const distantTablesWithStatus = Object.keys(distantTables).map(
(tableName) => ({
name: tableName,
schema: remoteServer.schema,
status: currentRemoteTableDistantNames.includes(tableName)
? RemoteTableStatus.SYNCED
: RemoteTableStatus.NOT_SYNCED,
}),
);
if (!shouldFetchPendingSchemaUpdates) {
return distantTablesWithStatus.sort(sortDistantTables);
}
const schemaPendingUpdates =
await this.remoteTableSchemaUpdateService.getSchemaUpdatesBetweenForeignAndDistantTables(
{
workspaceId,
remoteTables: currentRemoteTables,
distantTables,
},
);
const distantTablesWithPendingUpdates =
this.getDistantTablesWithPendingUpdates(
schemaPendingUpdates,
distantTablesWithStatus,
remoteServer.schema,
);
return distantTablesWithPendingUpdates.sort(sortDistantTables);
}
public async findRemoteTablesByServerId({
remoteServerId,
workspaceId,
}: {
remoteServerId: string;
workspaceId: string;
}) {
return this.remoteTableRepository.find({
where: {
remoteServerId,
workspaceId,
},
});
}
public async syncRemoteTable(input: RemoteTableInput, workspaceId: string) {
const remoteServer = await this.remoteServerRepository.findOne({
where: {
id: input.remoteServerId,
workspaceId,
},
});
if (!remoteServer) {
throw new RemoteTableException(
'Remote server does not exist',
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
);
}
const currentRemoteTableWithSameDistantName =
await this.remoteTableRepository.findOne({
where: {
distantTableName: input.name,
remoteServerId: remoteServer.id,
workspaceId,
},
});
if (currentRemoteTableWithSameDistantName) {
throw new RemoteTableException(
'Remote server does not exist',
RemoteTableExceptionCode.REMOTE_TABLE_ALREADY_EXISTS,
);
}
const dataSourceMetatada =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId,
);
const { baseName: localTableBaseName, suffix: localTableSuffix } =
await getRemoteTableLocalName(
input.name,
dataSourceMetatada.schema,
this.coreDataSource,
);
const localTableName = localTableSuffix
? `${localTableBaseName}${localTableSuffix}`
: localTableBaseName;
const remoteTableEntity = this.remoteTableRepository.create({
distantTableName: input.name,
localTableName,
workspaceId,
remoteServerId: remoteServer.id,
});
const distantTableColumns =
await this.distantTableService.getDistantTableColumns(
remoteServer,
workspaceId,
input.name,
);
if (!distantTableColumns) {
throw new RemoteTableException(
'Remote server does not exist',
RemoteTableExceptionCode.REMOTE_TABLE_NOT_FOUND,
);
}
// We only support remote tables with an id column for now.
const distantTableIdColumn = distantTableColumns.find(
(column) => column.columnName === 'id',
);
if (!distantTableIdColumn) {
throw new RemoteTableException(
'Remote server does not exist',
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
);
}
await this.foreignTableService.createForeignTable(
workspaceId,
localTableName,
remoteServer,
input.name,
distantTableColumns,
);
await this.createRemoteTableMetadata(
workspaceId,
localTableBaseName,
localTableSuffix,
distantTableColumns,
distantTableIdColumn,
dataSourceMetatada.id,
);
await this.remoteTableRepository.save(remoteTableEntity);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
return {
id: remoteTableEntity.id,
name: input.name,
schema: remoteServer.schema,
status: RemoteTableStatus.SYNCED,
};
}
public async unsyncRemoteTable(input: RemoteTableInput, workspaceId: string) {
const remoteServer = await this.remoteServerRepository.findOne({
where: {
id: input.remoteServerId,
workspaceId,
},
});
if (!remoteServer) {
throw new RemoteTableException(
'Remote server does not exist',
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
);
}
const remoteTable = await this.remoteTableRepository.findOne({
where: {
distantTableName: input.name,
remoteServerId: remoteServer.id,
workspaceId,
},
});
if (!remoteTable) {
throw new RemoteTableException(
'Remote table does not exist',
RemoteTableExceptionCode.REMOTE_TABLE_NOT_FOUND,
);
}
await this.unsyncOne(workspaceId, remoteTable, remoteServer);
return {
name: input.name,
schema: remoteServer.schema,
status: RemoteTableStatus.NOT_SYNCED,
};
}
public async unsyncAll(
workspaceId: string,
remoteServer: RemoteServerEntity<RemoteServerType>,
) {
const remoteTables = await this.remoteTableRepository.find({
where: {
remoteServerId: remoteServer.id,
workspaceId,
},
});
for (const remoteTable of remoteTables) {
await this.unsyncOne(workspaceId, remoteTable, remoteServer);
}
}
public async syncRemoteTableSchemaChanges(
input: RemoteTableInput,
workspaceId: string,
) {
const remoteServer = await this.remoteServerRepository.findOne({
where: {
id: input.remoteServerId,
workspaceId,
},
});
if (!remoteServer) {
throw new RemoteTableException(
'Remote server does not exist',
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
);
}
const remoteTable = await this.remoteTableRepository.findOne({
where: {
distantTableName: input.name,
remoteServerId: remoteServer.id,
workspaceId,
},
});
if (!remoteTable) {
throw new RemoteTableException(
'Remote table does not exist',
RemoteTableExceptionCode.REMOTE_TABLE_NOT_FOUND,
);
}
const distantTableColumns =
await this.distantTableService.getDistantTableColumns(
remoteServer,
workspaceId,
remoteTable.distantTableName,
);
if (!distantTableColumns) {
await this.unsyncOne(workspaceId, remoteTable, remoteServer);
return {
name: remoteTable.localTableName,
status: RemoteTableStatus.NOT_SYNCED,
schemaPendingUpdates: [],
};
}
const foreignTableColumns = await fetchTableColumns(
this.workspaceDataSourceService,
workspaceId,
remoteTable.localTableName,
);
const columnsUpdates =
this.remoteTableSchemaUpdateService.computeForeignTableColumnsUpdates(
foreignTableColumns,
distantTableColumns,
);
if (isEmpty(columnsUpdates)) {
this.logger.log(
`No update to perform on table "${remoteTable.localTableName}" for workspace ${workspaceId}`,
);
return {
name: remoteTable.localTableName,
status: RemoteTableStatus.SYNCED,
schemaPendingUpdates: [],
};
}
const updatedTable = await this.updateForeignTableAndFieldsMetadata(
remoteTable.localTableName,
workspaceId,
columnsUpdates,
);
return updatedTable;
}
private async unsyncOne(
workspaceId: string,
remoteTable: RemoteTableEntity,
remoteServer: RemoteServerEntity<RemoteServerType>,
) {
const currentForeignTableNames =
await this.foreignTableService.fetchForeignTableNamesWithinWorkspace(
workspaceId,
remoteServer.foreignDataWrapperId,
);
if (!currentForeignTableNames.includes(remoteTable.localTableName)) {
throw new RemoteTableException(
'Foreign table does not exist',
RemoteTableExceptionCode.NO_FOREIGN_TABLES_FOUND,
);
}
const objectMetadata =
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
where: { nameSingular: remoteTable.localTableName },
});
if (objectMetadata) {
await this.objectMetadataService.deleteOneObject({
deleteObjectInput: { id: objectMetadata.id },
workspaceId,
});
}
await this.foreignTableService.deleteForeignTable(
remoteTable.localTableName,
workspaceId,
);
await this.remoteTableRepository.delete(remoteTable.id);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
}
private async createRemoteTableMetadata(
workspaceId: string,
localTableBaseName: string,
localTableSuffix: number | undefined,
distantTableColumns: PostgresTableSchemaColumn[],
distantTableIdColumn: PostgresTableSchemaColumn,
dataSourceMetadataId: string,
) {
const localTableNameSingular = localTableSuffix
? `${localTableBaseName}${localTableSuffix}`
: localTableBaseName;
const localTableNamePlural = localTableSuffix
? `${plural(localTableBaseName)}${localTableSuffix}`
: plural(localTableBaseName);
const objectMetadata = await this.objectMetadataService.createOneObject({
createObjectInput: {
nameSingular: camelCase(localTableNameSingular),
namePlural: camelCase(localTableNamePlural),
labelSingular: camelToTitleCase(camelCase(localTableBaseName)),
labelPlural: camelToTitleCase(plural(camelCase(localTableBaseName))),
description: 'Remote table',
dataSourceId: dataSourceMetadataId,
icon: 'IconPlug',
isRemote: true,
primaryKeyColumnType: distantTableIdColumn.udtName,
primaryKeyFieldMetadataSettings: mapUdtNameToFieldSettings(
distantTableIdColumn.udtName,
),
},
workspaceId: workspaceId,
});
for (const column of distantTableColumns) {
const columnName = camelCase(column.columnName);
// TODO: return error to the user when a column cannot be managed
try {
const field = await this.createFieldMetadataForForeignTableColumn(
workspaceId,
columnName,
column.udtName,
objectMetadata.id,
);
if (columnName === 'id') {
await this.objectMetadataService.updateOneObject({
workspaceId,
updateObjectInput: {
id: objectMetadata.id,
update: {
labelIdentifierFieldMetadataId: field.id,
},
},
});
}
} catch (error) {
this.logger.error(
`Could not create field ${columnName} for remote table ${localTableNameSingular}: ${error}`,
);
}
}
}
private getDistantTablesWithPendingUpdates(
schemaPendingUpdates: { [tablename: string]: DistantTableUpdate[] },
distantTablesWithStatus: {
name: string;
schema: string;
status: RemoteTableStatus;
}[],
remoteServerSchema: string,
) {
const distantTablesWithUpdates = distantTablesWithStatus.map((table) => ({
...table,
schemaPendingUpdates: schemaPendingUpdates[table.name] || [],
}));
const deletedTables = Object.entries(schemaPendingUpdates)
.filter(([_tableName, updates]) =>
updates.includes(DistantTableUpdate.TABLE_DELETED),
)
.map(([tableName, updates]) => ({
name: tableName,
schema: remoteServerSchema,
status: RemoteTableStatus.SYNCED,
schemaPendingUpdates: updates,
}));
return [...distantTablesWithUpdates, ...deletedTables];
}
private async updateForeignTableAndFieldsMetadata(
foreignTableName: string,
workspaceId: string,
columnsUpdates: WorkspaceMigrationColumnAction[],
) {
const updatedForeignTable =
await this.foreignTableService.updateForeignTable(
foreignTableName,
workspaceId,
columnsUpdates,
);
const objectMetadata =
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
where: { nameSingular: foreignTableName },
});
if (!objectMetadata) {
throw new RemoteTableException(
`Cannot find associated object for table ${foreignTableName}`,
RemoteTableExceptionCode.NO_OBJECT_METADATA_FOUND,
);
}
for (const columnUpdate of columnsUpdates) {
this.updateFieldMetadataFromColumnUpdate(
columnUpdate,
workspaceId,
objectMetadata.id,
);
}
return updatedForeignTable;
}
private async updateFieldMetadataFromColumnUpdate(
columnUpdate: WorkspaceMigrationColumnAction,
workspaceId: string,
objectMetadataId: string,
) {
if (columnUpdate.action === WorkspaceMigrationColumnActionType.CREATE) {
await this.createFieldMetadataForForeignTableColumn(
workspaceId,
columnUpdate.columnName,
columnUpdate.columnType,
objectMetadataId,
);
}
if (columnUpdate.action === WorkspaceMigrationColumnActionType.DROP) {
const columnName = columnUpdate.columnName;
const fieldMetadataToDelete =
await this.fieldMetadataService.findOneWithinWorkspace(workspaceId, {
where: {
objectMetadataId: objectMetadataId,
name: columnName,
},
});
if (!fieldMetadataToDelete) {
throw new RemoteTableException(
`Cannot find associated field metadata for column ${columnName}`,
RemoteTableExceptionCode.NO_FIELD_METADATA_FOUND,
);
}
await this.fieldMetadataService.deleteOneField({
deleteOneFieldInput: { id: fieldMetadataToDelete.id },
workspaceId,
});
}
}
private async createFieldMetadataForForeignTableColumn(
workspaceId: string,
columnName: string,
columnType: string,
objectMetadataId: string,
): Promise<FlatFieldMetadata> {
return this.fieldMetadataService.createOneField({
createFieldInput: {
name: columnName,
label: camelToTitleCase(columnName),
description: 'Field of remote',
type: mapUdtNameToFieldType(columnType),
objectMetadataId: objectMetadataId,
isRemoteCreation: true,
isNullable: true,
icon: 'IconPlug',
settings: mapUdtNameToFieldSettings(columnType),
},
workspaceId: workspaceId,
});
}
}
@@ -1,27 +0,0 @@
import { type PostgresTableSchemaColumn } from 'src/engine/metadata-modules/remote-server/types/postgres-table-schema-column';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { type WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
export const fetchTableColumns = async (
workspaceDataSourceService: WorkspaceDataSourceService,
workspaceId: string,
tableName: string,
): Promise<PostgresTableSchemaColumn[]> => {
const schemaName = getWorkspaceSchemaName(workspaceId);
// TODO: executeRawQuery is deprecated and will throw
const res = await workspaceDataSourceService.executeRawQuery(
`SELECT column_name, data_type, udt_name
FROM information_schema.columns
WHERE table_schema = $1 AND table_name = $2`,
[schemaName, tableName],
workspaceId,
);
// @ts-expect-error legacy noImplicitAny
return res.map((column) => ({
columnName: column.column_name,
dataType: column.data_type,
udtName: column.udt_name,
}));
};
@@ -1,65 +0,0 @@
import { singular } from 'pluralize';
import { type DataSource } from 'typeorm';
import {
RemoteTableException,
RemoteTableExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.exception';
import { camelCase } from 'src/utils/camel-case';
const MAX_SUFFIX = 10;
type RemoteTableLocalName = {
baseName: string;
suffix?: number;
};
const isNameAvailable = async (
tableName: string,
workspaceSchemaName: string,
coreDataSource: DataSource,
) => {
const numberOfTablesWithSameName = +(
await coreDataSource.query(
`SELECT count(table_name) FROM information_schema.tables WHERE table_name LIKE $1 AND table_schema IN ('core', $2)`,
[tableName, workspaceSchemaName],
)
)[0].count;
return numberOfTablesWithSameName === 0;
};
export const getRemoteTableLocalName = async (
distantTableName: string,
workspaceSchemaName: string,
coreDataSource: DataSource,
): Promise<RemoteTableLocalName> => {
const baseName = singular(camelCase(distantTableName));
const isBaseNameValid = await isNameAvailable(
baseName,
workspaceSchemaName,
coreDataSource,
);
if (isBaseNameValid) {
return { baseName };
}
for (let suffix = 2; suffix < MAX_SUFFIX; suffix++) {
const name = `${baseName}${suffix}`;
const isNameWithSuffixValid = await isNameAvailable(
name,
workspaceSchemaName,
coreDataSource,
);
if (isNameWithSuffixValid) {
return { baseName, suffix };
}
}
throw new RemoteTableException(
'Table name is already taken',
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
);
};
@@ -1,32 +0,0 @@
import { assertUnreachable } from 'twenty-shared/utils';
import {
ConflictError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
RemoteTableException,
RemoteTableExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.exception';
export const remoteTableGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof RemoteTableException) {
switch (error.code) {
case RemoteTableExceptionCode.REMOTE_TABLE_NOT_FOUND:
case RemoteTableExceptionCode.NO_OBJECT_METADATA_FOUND:
case RemoteTableExceptionCode.NO_FOREIGN_TABLES_FOUND:
case RemoteTableExceptionCode.NO_FIELD_METADATA_FOUND:
throw new NotFoundError(error.message);
case RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT:
throw new UserInputError(error.message);
case RemoteTableExceptionCode.REMOTE_TABLE_ALREADY_EXISTS:
throw new ConflictError(error.message);
default: {
return assertUnreachable(error.code);
}
}
}
throw error;
};
@@ -1,48 +0,0 @@
import {
FieldMetadataType,
type FieldMetadataSettings,
NumberDataType,
} from 'twenty-shared/types';
export const mapUdtNameToFieldType = (udtName: string): FieldMetadataType => {
switch (udtName) {
case 'uuid':
return FieldMetadataType.UUID;
case 'varchar':
case 'text':
return FieldMetadataType.TEXT;
case 'bool':
return FieldMetadataType.BOOLEAN;
case 'timestamp':
case 'timestamptz':
return FieldMetadataType.DATE_TIME;
case 'integer':
case 'int2':
case 'int4':
case 'int8':
case 'bigint':
return FieldMetadataType.NUMBER;
default:
return FieldMetadataType.TEXT;
}
};
export const mapUdtNameToFieldSettings = (
udtName: string,
): FieldMetadataSettings<FieldMetadataType> | undefined => {
switch (udtName) {
case 'integer':
case 'int2':
case 'int4':
return {
dataType: NumberDataType.INT,
} as FieldMetadataSettings<FieldMetadataType.NUMBER>;
case 'int8':
case 'bigint':
return {
dataType: NumberDataType.BIGINT,
} as FieldMetadataSettings<FieldMetadataType.NUMBER>;
default:
return undefined;
}
};
@@ -1,5 +0,0 @@
export type PostgresTableSchemaColumn = {
columnName: string;
dataType: string;
udtName: string;
};
@@ -1,26 +0,0 @@
import { InputType, Field, ObjectType } from '@nestjs/graphql';
import { IsOptional } from 'class-validator';
@ObjectType()
@InputType()
export class UserMappingOptions {
@IsOptional()
@Field(() => String, { nullable: true })
user: string;
@IsOptional()
@Field(() => String, { nullable: true })
password: string;
}
@InputType()
export class UserMappingOptionsUpdateInput {
@IsOptional()
@Field(() => String, { nullable: true })
user?: string;
@IsOptional()
@Field(() => String, { nullable: true })
password?: string;
}
@@ -1,138 +0,0 @@
import { type DeepPartial } from 'typeorm';
import {
type ForeignDataWrapperOptions,
type RemoteServerEntity,
type RemoteServerType,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import {
RemoteServerException,
RemoteServerExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-server.exception';
import { type UserMappingOptions } from 'src/engine/metadata-modules/remote-server/types/user-mapping-options';
export const buildUpdateRemoteServerRawQuery = (
remoteServerToUpdate: DeepPartial<RemoteServerEntity<RemoteServerType>> &
Pick<RemoteServerEntity<RemoteServerType>, 'workspaceId' | 'id'>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): [any[], string] => {
const options: string[] = [];
const [parameters, parametersPositions] =
buildParametersAndPositions(remoteServerToUpdate);
if (remoteServerToUpdate.userMappingOptions) {
const userMappingOptionsQuery = buildJsonRawQuery(
remoteServerToUpdate.userMappingOptions,
parametersPositions,
'userMappingOptions',
);
options.push(userMappingOptionsQuery);
}
if (remoteServerToUpdate.foreignDataWrapperOptions) {
const foreignDataWrapperOptionsQuery = buildJsonRawQuery(
remoteServerToUpdate.foreignDataWrapperOptions,
parametersPositions,
'foreignDataWrapperOptions',
);
options.push(foreignDataWrapperOptionsQuery);
}
if (remoteServerToUpdate.schema) {
// @ts-expect-error legacy noImplicitAny
options.push(`"schema" = $${parametersPositions['schema']}`);
}
if (remoteServerToUpdate.label) {
// @ts-expect-error legacy noImplicitAny
options.push(`"label" = $${parametersPositions['label']}`);
}
if (options.length < 1) {
throw new RemoteServerException(
'No fields to update',
RemoteServerExceptionCode.INVALID_REMOTE_SERVER_INPUT,
);
}
const rawQuery = `UPDATE metadata."remoteServer" SET ${options.join(
', ',
)} WHERE "id"= $1 RETURNING *`;
return [parameters, rawQuery];
};
const buildParametersAndPositions = (
remoteServerToUpdate: DeepPartial<RemoteServerEntity<RemoteServerType>> &
Pick<RemoteServerEntity<RemoteServerType>, 'workspaceId' | 'id'>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): [any[], object] => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parameters: any[] = [remoteServerToUpdate.id];
const parametersPositions = {};
if (remoteServerToUpdate.userMappingOptions) {
Object.entries(remoteServerToUpdate.userMappingOptions).forEach(
([key, value]) => {
parameters.push(value);
// @ts-expect-error legacy noImplicitAny
parametersPositions[key] = parameters.length;
},
);
}
if (remoteServerToUpdate.foreignDataWrapperOptions) {
Object.entries(remoteServerToUpdate.foreignDataWrapperOptions).forEach(
([key, value]) => {
parameters.push(value);
// @ts-expect-error legacy noImplicitAny
parametersPositions[key] = parameters.length;
},
);
}
if (remoteServerToUpdate.schema) {
parameters.push(remoteServerToUpdate.schema);
// @ts-expect-error legacy noImplicitAny
parametersPositions['schema'] = parameters.length;
}
if (remoteServerToUpdate.label) {
parameters.push(remoteServerToUpdate.label);
// @ts-expect-error legacy noImplicitAny
parametersPositions['label'] = parameters.length;
}
return [parameters, parametersPositions];
};
const buildJsonRawQuery = (
options:
| Partial<UserMappingOptions>
| Partial<ForeignDataWrapperOptions<RemoteServerType>>,
parametersPositions: object,
objectName: string,
): string => {
const buildJsonSet = (
opts:
| Partial<UserMappingOptions>
| Partial<ForeignDataWrapperOptions<RemoteServerType>>,
): string => {
const [[firstKey, _], ...followingOptions] = Object.entries(opts);
// @ts-expect-error legacy noImplicitAny
let query = `jsonb_set("${objectName}", '{${firstKey}}', to_jsonb($${parametersPositions[firstKey]}::text))`;
followingOptions.forEach(([key, _]) => {
// @ts-expect-error legacy noImplicitAny
query = `jsonb_set(${query}, '{${key}}', to_jsonb($${parametersPositions[key]}::text))`;
});
return query;
};
return `"${objectName}" = ${buildJsonSet(options)}`;
};
@@ -1,35 +0,0 @@
import { assertUnreachable } from 'twenty-shared/utils';
import {
ConflictError,
ForbiddenError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
RemoteServerException,
RemoteServerExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-server.exception';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const remoteServerGraphqlApiExceptionHandler = (error: any) => {
if (error instanceof RemoteServerException) {
switch (error.code) {
case RemoteServerExceptionCode.REMOTE_SERVER_NOT_FOUND:
throw new NotFoundError(error.message);
case RemoteServerExceptionCode.INVALID_REMOTE_SERVER_INPUT:
throw new UserInputError(error.message);
case RemoteServerExceptionCode.REMOTE_SERVER_MUTATION_NOT_ALLOWED:
throw new ForbiddenError(error.message);
case RemoteServerExceptionCode.REMOTE_SERVER_ALREADY_EXISTS:
throw new ConflictError(error.message);
case RemoteServerExceptionCode.REMOTE_SERVER_CONNECTION_ERROR:
throw error;
default: {
return assertUnreachable(error.code);
}
}
}
throw error;
};
@@ -1,32 +0,0 @@
import { isDefined } from 'class-validator';
import {
RemoteServerException,
RemoteServerExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-server.exception';
const INPUT_REGEX = /^([A-Za-z0-9\-_.@]+)$/;
export const validateObjectAgainstInjections = (input: object) => {
for (const [key, value] of Object.entries(input)) {
// Password are encrypted so we don't need to validate them
if (key === 'password') {
continue;
}
if (!isDefined(value)) {
continue;
}
validateStringAgainstInjections(value.toString());
}
};
export const validateStringAgainstInjections = (input: string) => {
if (!INPUT_REGEX.test(input)) {
throw new RemoteServerException(
'Invalid remote server input',
RemoteServerExceptionCode.INVALID_REMOTE_SERVER_INPUT,
);
}
};

Some files were not shown because too many files have changed in this diff Show More