mirror of
https://github.com/lingble/twenty.git
synced 2025-11-06 07:37:56 +00:00
## Context This PR removes workspace-query-runner/builder in preparation for fully deprecating pg_graphql next steps: Remove from the setup and make a command to remove comments on schema/tables related to pg_graphql
This commit is contained in:
@@ -28,13 +28,6 @@ export type Scalars = {
|
|||||||
Upload: { input: any; output: any; }
|
Upload: { input: any; output: any; }
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AisqlQueryResult = {
|
|
||||||
__typename?: 'AISQLQueryResult';
|
|
||||||
queryFailedErrorMessage?: Maybe<Scalars['String']['output']>;
|
|
||||||
sqlQuery: Scalars['String']['output'];
|
|
||||||
sqlQueryResult?: Maybe<Scalars['String']['output']>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ActivateWorkspaceInput = {
|
export type ActivateWorkspaceInput = {
|
||||||
displayName?: InputMaybe<Scalars['String']['input']>;
|
displayName?: InputMaybe<Scalars['String']['input']>;
|
||||||
};
|
};
|
||||||
@@ -862,7 +855,6 @@ export type Query = {
|
|||||||
findOneRemoteServerById: RemoteServer;
|
findOneRemoteServerById: RemoteServer;
|
||||||
findWorkspaceFromInviteHash: Workspace;
|
findWorkspaceFromInviteHash: Workspace;
|
||||||
findWorkspaceInvitations: Array<WorkspaceInvitation>;
|
findWorkspaceInvitations: Array<WorkspaceInvitation>;
|
||||||
getAISQLQuery: AisqlQueryResult;
|
|
||||||
getAvailablePackages: Scalars['JSON']['output'];
|
getAvailablePackages: Scalars['JSON']['output'];
|
||||||
getPostgresCredentials?: Maybe<PostgresCredentials>;
|
getPostgresCredentials?: Maybe<PostgresCredentials>;
|
||||||
getProductPrices: ProductPricesEntity;
|
getProductPrices: ProductPricesEntity;
|
||||||
@@ -930,11 +922,6 @@ export type QueryFindWorkspaceFromInviteHashArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueryGetAisqlQueryArgs = {
|
|
||||||
text: Scalars['String']['input'];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type QueryGetProductPricesArgs = {
|
export type QueryGetProductPricesArgs = {
|
||||||
product: Scalars['String']['input'];
|
product: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as Apollo from '@apollo/client';
|
|
||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
import * as Apollo from '@apollo/client';
|
||||||
export type Maybe<T> = T | null;
|
export type Maybe<T> = T | null;
|
||||||
export type InputMaybe<T> = Maybe<T>;
|
export type InputMaybe<T> = Maybe<T>;
|
||||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||||
@@ -21,13 +21,6 @@ export type Scalars = {
|
|||||||
Upload: any;
|
Upload: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AisqlQueryResult = {
|
|
||||||
__typename?: 'AISQLQueryResult';
|
|
||||||
queryFailedErrorMessage?: Maybe<Scalars['String']>;
|
|
||||||
sqlQuery: Scalars['String'];
|
|
||||||
sqlQueryResult?: Maybe<Scalars['String']>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ActivateWorkspaceInput = {
|
export type ActivateWorkspaceInput = {
|
||||||
displayName?: InputMaybe<Scalars['String']>;
|
displayName?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
@@ -160,13 +153,7 @@ export type ClientConfig = {
|
|||||||
support: Support;
|
support: Support;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateServerlessFunctionFromFileInput = {
|
|
||||||
description?: InputMaybe<Scalars['String']>;
|
|
||||||
name: Scalars['String'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CreateServerlessFunctionInput = {
|
export type CreateServerlessFunctionInput = {
|
||||||
code: Scalars['String'];
|
|
||||||
description?: InputMaybe<Scalars['String']>;
|
description?: InputMaybe<Scalars['String']>;
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
};
|
};
|
||||||
@@ -302,6 +289,36 @@ export type GetServerlessFunctionSourceCodeInput = {
|
|||||||
version?: Scalars['String'];
|
version?: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type IndexConnection = {
|
||||||
|
__typename?: 'IndexConnection';
|
||||||
|
/** Array of edges. */
|
||||||
|
edges: Array<IndexEdge>;
|
||||||
|
/** Paging information */
|
||||||
|
pageInfo: PageInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IndexIndexFieldMetadatasConnection = {
|
||||||
|
__typename?: 'IndexIndexFieldMetadatasConnection';
|
||||||
|
/** Array of edges. */
|
||||||
|
edges: Array<IndexFieldEdge>;
|
||||||
|
/** Paging information */
|
||||||
|
pageInfo: PageInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IndexObjectMetadataConnection = {
|
||||||
|
__typename?: 'IndexObjectMetadataConnection';
|
||||||
|
/** Array of edges. */
|
||||||
|
edges: Array<ObjectEdge>;
|
||||||
|
/** Paging information */
|
||||||
|
pageInfo: PageInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Type of the index */
|
||||||
|
export enum IndexType {
|
||||||
|
Btree = 'BTREE',
|
||||||
|
Gin = 'GIN'
|
||||||
|
}
|
||||||
|
|
||||||
export type InvalidatePassword = {
|
export type InvalidatePassword = {
|
||||||
__typename?: 'InvalidatePassword';
|
__typename?: 'InvalidatePassword';
|
||||||
/** Boolean that confirms query was dispatched */
|
/** Boolean that confirms query was dispatched */
|
||||||
@@ -344,7 +361,6 @@ export type Mutation = {
|
|||||||
createOneAppToken: AppToken;
|
createOneAppToken: AppToken;
|
||||||
createOneObject: Object;
|
createOneObject: Object;
|
||||||
createOneServerlessFunction: ServerlessFunction;
|
createOneServerlessFunction: ServerlessFunction;
|
||||||
createOneServerlessFunctionFromFile: ServerlessFunction;
|
|
||||||
deactivateWorkflowVersion: Scalars['Boolean'];
|
deactivateWorkflowVersion: Scalars['Boolean'];
|
||||||
deleteCurrentWorkspace: Workspace;
|
deleteCurrentWorkspace: Workspace;
|
||||||
deleteOneObject: Object;
|
deleteOneObject: Object;
|
||||||
@@ -426,12 +442,6 @@ export type MutationCreateOneServerlessFunctionArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationCreateOneServerlessFunctionFromFileArgs = {
|
|
||||||
file: Scalars['Upload'];
|
|
||||||
input: CreateServerlessFunctionFromFileInput;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type MutationDeactivateWorkflowVersionArgs = {
|
export type MutationDeactivateWorkflowVersionArgs = {
|
||||||
workflowVersionId: Scalars['String'];
|
workflowVersionId: Scalars['String'];
|
||||||
};
|
};
|
||||||
@@ -520,9 +530,8 @@ export type MutationSignUpArgs = {
|
|||||||
|
|
||||||
|
|
||||||
export type MutationTrackArgs = {
|
export type MutationTrackArgs = {
|
||||||
data: Scalars['JSON'];
|
action: Scalars['String'];
|
||||||
sessionId: Scalars['String'];
|
payload: Scalars['JSON'];
|
||||||
type: Scalars['String'];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -589,6 +598,14 @@ export type ObjectFieldsConnection = {
|
|||||||
pageInfo: PageInfo;
|
pageInfo: PageInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ObjectIndexMetadatasConnection = {
|
||||||
|
__typename?: 'ObjectIndexMetadatasConnection';
|
||||||
|
/** Array of edges. */
|
||||||
|
edges: Array<IndexEdge>;
|
||||||
|
/** Paging information */
|
||||||
|
pageInfo: PageInfo;
|
||||||
|
};
|
||||||
|
|
||||||
/** Onboarding status */
|
/** Onboarding status */
|
||||||
export enum OnboardingStatus {
|
export enum OnboardingStatus {
|
||||||
Completed = 'COMPLETED',
|
Completed = 'COMPLETED',
|
||||||
@@ -654,15 +671,16 @@ export type Query = {
|
|||||||
currentWorkspace: Workspace;
|
currentWorkspace: Workspace;
|
||||||
findWorkspaceFromInviteHash: Workspace;
|
findWorkspaceFromInviteHash: Workspace;
|
||||||
findWorkspaceInvitations: Array<WorkspaceInvitation>;
|
findWorkspaceInvitations: Array<WorkspaceInvitation>;
|
||||||
getAISQLQuery: AisqlQueryResult;
|
|
||||||
getAvailablePackages: Scalars['JSON'];
|
getAvailablePackages: Scalars['JSON'];
|
||||||
getPostgresCredentials?: Maybe<PostgresCredentials>;
|
getPostgresCredentials?: Maybe<PostgresCredentials>;
|
||||||
getProductPrices: ProductPricesEntity;
|
getProductPrices: ProductPricesEntity;
|
||||||
getServerlessFunctionSourceCode?: Maybe<Scalars['String']>;
|
getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']>;
|
||||||
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
|
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
|
||||||
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
|
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
|
||||||
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
|
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
|
||||||
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
|
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
|
||||||
|
index: Index;
|
||||||
|
indexMetadatas: IndexConnection;
|
||||||
object: Object;
|
object: Object;
|
||||||
objects: ObjectConnection;
|
objects: ObjectConnection;
|
||||||
serverlessFunction: ServerlessFunction;
|
serverlessFunction: ServerlessFunction;
|
||||||
@@ -692,11 +710,6 @@ export type QueryFindWorkspaceFromInviteHashArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueryGetAisqlQueryArgs = {
|
|
||||||
text: Scalars['String'];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type QueryGetProductPricesArgs = {
|
export type QueryGetProductPricesArgs = {
|
||||||
product: Scalars['String'];
|
product: Scalars['String'];
|
||||||
};
|
};
|
||||||
@@ -831,7 +844,6 @@ export type ServerlessFunction = {
|
|||||||
latestVersion?: Maybe<Scalars['String']>;
|
latestVersion?: Maybe<Scalars['String']>;
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
runtime: Scalars['String'];
|
runtime: Scalars['String'];
|
||||||
sourceCodeHash: Scalars['String'];
|
|
||||||
syncStatus: ServerlessFunctionSyncStatus;
|
syncStatus: ServerlessFunctionSyncStatus;
|
||||||
updatedAt: Scalars['DateTime'];
|
updatedAt: Scalars['DateTime'];
|
||||||
};
|
};
|
||||||
@@ -1028,7 +1040,7 @@ export type UpdateOneObjectInput = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateServerlessFunctionInput = {
|
export type UpdateServerlessFunctionInput = {
|
||||||
code: Scalars['String'];
|
code: Scalars['JSON'];
|
||||||
description?: InputMaybe<Scalars['String']>;
|
description?: InputMaybe<Scalars['String']>;
|
||||||
/** Id of the serverless function to execute */
|
/** Id of the serverless function to execute */
|
||||||
id: Scalars['UUID'];
|
id: Scalars['UUID'];
|
||||||
@@ -1213,6 +1225,7 @@ export type Field = {
|
|||||||
isCustom?: Maybe<Scalars['Boolean']>;
|
isCustom?: Maybe<Scalars['Boolean']>;
|
||||||
isNullable?: Maybe<Scalars['Boolean']>;
|
isNullable?: Maybe<Scalars['Boolean']>;
|
||||||
isSystem?: Maybe<Scalars['Boolean']>;
|
isSystem?: Maybe<Scalars['Boolean']>;
|
||||||
|
isUnique?: Maybe<Scalars['Boolean']>;
|
||||||
label: Scalars['String'];
|
label: Scalars['String'];
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
object?: Maybe<Object>;
|
object?: Maybe<Object>;
|
||||||
@@ -1241,6 +1254,71 @@ export type FieldFilter = {
|
|||||||
or?: InputMaybe<Array<FieldFilter>>;
|
or?: InputMaybe<Array<FieldFilter>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Index = {
|
||||||
|
__typename?: 'index';
|
||||||
|
createdAt: Scalars['DateTime'];
|
||||||
|
id: Scalars['UUID'];
|
||||||
|
indexFieldMetadatas: IndexIndexFieldMetadatasConnection;
|
||||||
|
indexType: IndexType;
|
||||||
|
indexWhereClause?: Maybe<Scalars['String']>;
|
||||||
|
isCustom?: Maybe<Scalars['Boolean']>;
|
||||||
|
isUnique: Scalars['Boolean'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
objectMetadata: IndexObjectMetadataConnection;
|
||||||
|
updatedAt: Scalars['DateTime'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type IndexIndexFieldMetadatasArgs = {
|
||||||
|
filter?: IndexFieldFilter;
|
||||||
|
paging?: CursorPaging;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type IndexObjectMetadataArgs = {
|
||||||
|
filter?: ObjectFilter;
|
||||||
|
paging?: CursorPaging;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IndexEdge = {
|
||||||
|
__typename?: 'indexEdge';
|
||||||
|
/** Cursor for this node. */
|
||||||
|
cursor: Scalars['ConnectionCursor'];
|
||||||
|
/** The node containing the index */
|
||||||
|
node: Index;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IndexField = {
|
||||||
|
__typename?: 'indexField';
|
||||||
|
createdAt: Scalars['DateTime'];
|
||||||
|
fieldMetadataId: Scalars['UUID'];
|
||||||
|
id: Scalars['UUID'];
|
||||||
|
order: Scalars['Float'];
|
||||||
|
updatedAt: Scalars['DateTime'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IndexFieldEdge = {
|
||||||
|
__typename?: 'indexFieldEdge';
|
||||||
|
/** Cursor for this node. */
|
||||||
|
cursor: Scalars['ConnectionCursor'];
|
||||||
|
/** The node containing the indexField */
|
||||||
|
node: IndexField;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IndexFieldFilter = {
|
||||||
|
and?: InputMaybe<Array<IndexFieldFilter>>;
|
||||||
|
fieldMetadataId?: InputMaybe<UuidFilterComparison>;
|
||||||
|
id?: InputMaybe<UuidFilterComparison>;
|
||||||
|
or?: InputMaybe<Array<IndexFieldFilter>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IndexFilter = {
|
||||||
|
and?: InputMaybe<Array<IndexFilter>>;
|
||||||
|
id?: InputMaybe<UuidFilterComparison>;
|
||||||
|
isCustom?: InputMaybe<BooleanFieldComparison>;
|
||||||
|
or?: InputMaybe<Array<IndexFilter>>;
|
||||||
|
};
|
||||||
|
|
||||||
export type Object = {
|
export type Object = {
|
||||||
__typename?: 'object';
|
__typename?: 'object';
|
||||||
createdAt: Scalars['DateTime'];
|
createdAt: Scalars['DateTime'];
|
||||||
@@ -1250,6 +1328,7 @@ export type Object = {
|
|||||||
icon?: Maybe<Scalars['String']>;
|
icon?: Maybe<Scalars['String']>;
|
||||||
id: Scalars['UUID'];
|
id: Scalars['UUID'];
|
||||||
imageIdentifierFieldMetadataId?: Maybe<Scalars['String']>;
|
imageIdentifierFieldMetadataId?: Maybe<Scalars['String']>;
|
||||||
|
indexMetadatas: ObjectIndexMetadatasConnection;
|
||||||
isActive: Scalars['Boolean'];
|
isActive: Scalars['Boolean'];
|
||||||
isCustom: Scalars['Boolean'];
|
isCustom: Scalars['Boolean'];
|
||||||
isRemote: Scalars['Boolean'];
|
isRemote: Scalars['Boolean'];
|
||||||
@@ -1268,6 +1347,12 @@ export type ObjectFieldsArgs = {
|
|||||||
paging?: CursorPaging;
|
paging?: CursorPaging;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type ObjectIndexMetadatasArgs = {
|
||||||
|
filter?: IndexFilter;
|
||||||
|
paging?: CursorPaging;
|
||||||
|
};
|
||||||
|
|
||||||
export type ObjectEdge = {
|
export type ObjectEdge = {
|
||||||
__typename?: 'objectEdge';
|
__typename?: 'objectEdge';
|
||||||
/** Cursor for this node. */
|
/** Cursor for this node. */
|
||||||
@@ -1276,6 +1361,16 @@ export type ObjectEdge = {
|
|||||||
node: Object;
|
node: Object;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ObjectFilter = {
|
||||||
|
and?: InputMaybe<Array<ObjectFilter>>;
|
||||||
|
id?: InputMaybe<UuidFilterComparison>;
|
||||||
|
isActive?: InputMaybe<BooleanFieldComparison>;
|
||||||
|
isCustom?: InputMaybe<BooleanFieldComparison>;
|
||||||
|
isRemote?: InputMaybe<BooleanFieldComparison>;
|
||||||
|
isSystem?: InputMaybe<BooleanFieldComparison>;
|
||||||
|
or?: InputMaybe<Array<ObjectFilter>>;
|
||||||
|
};
|
||||||
|
|
||||||
export type Relation = {
|
export type Relation = {
|
||||||
__typename?: 'relation';
|
__typename?: 'relation';
|
||||||
createdAt: Scalars['DateTime'];
|
createdAt: Scalars['DateTime'];
|
||||||
@@ -1511,13 +1606,6 @@ export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]
|
|||||||
|
|
||||||
export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } };
|
export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } };
|
||||||
|
|
||||||
export type GetAisqlQueryQueryVariables = Exact<{
|
|
||||||
text: Scalars['String'];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
|
|
||||||
export type GetAisqlQueryQuery = { __typename?: 'Query', getAISQLQuery: { __typename?: 'AISQLQueryResult', sqlQuery: string, sqlQueryResult?: string | null, queryFailedErrorMessage?: string | null } };
|
|
||||||
|
|
||||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
|
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
|
||||||
|
|
||||||
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
||||||
@@ -1962,7 +2050,7 @@ export type TrackMutationFn = Apollo.MutationFunction<TrackMutation, TrackMutati
|
|||||||
* @example
|
* @example
|
||||||
* const [trackMutation, { data, loading, error }] = useTrackMutation({
|
* const [trackMutation, { data, loading, error }] = useTrackMutation({
|
||||||
* variables: {
|
* variables: {
|
||||||
* action: // value for 'type'
|
* action: // value for 'action'
|
||||||
* payload: // value for 'payload'
|
* payload: // value for 'payload'
|
||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
@@ -2756,43 +2844,6 @@ export function useSkipSyncEmailOnboardingStepMutation(baseOptions?: Apollo.Muta
|
|||||||
export type SkipSyncEmailOnboardingStepMutationHookResult = ReturnType<typeof useSkipSyncEmailOnboardingStepMutation>;
|
export type SkipSyncEmailOnboardingStepMutationHookResult = ReturnType<typeof useSkipSyncEmailOnboardingStepMutation>;
|
||||||
export type SkipSyncEmailOnboardingStepMutationResult = Apollo.MutationResult<SkipSyncEmailOnboardingStepMutation>;
|
export type SkipSyncEmailOnboardingStepMutationResult = Apollo.MutationResult<SkipSyncEmailOnboardingStepMutation>;
|
||||||
export type SkipSyncEmailOnboardingStepMutationOptions = Apollo.BaseMutationOptions<SkipSyncEmailOnboardingStepMutation, SkipSyncEmailOnboardingStepMutationVariables>;
|
export type SkipSyncEmailOnboardingStepMutationOptions = Apollo.BaseMutationOptions<SkipSyncEmailOnboardingStepMutation, SkipSyncEmailOnboardingStepMutationVariables>;
|
||||||
export const GetAisqlQueryDocument = gql`
|
|
||||||
query GetAISQLQuery($text: String!) {
|
|
||||||
getAISQLQuery(text: $text) {
|
|
||||||
sqlQuery
|
|
||||||
sqlQueryResult
|
|
||||||
queryFailedErrorMessage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* __useGetAisqlQueryQuery__
|
|
||||||
*
|
|
||||||
* To run a query within a React component, call `useGetAisqlQueryQuery` and pass it any options that fit your needs.
|
|
||||||
* When your component renders, `useGetAisqlQueryQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
|
||||||
* you can use to render your UI.
|
|
||||||
*
|
|
||||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const { data, loading, error } = useGetAisqlQueryQuery({
|
|
||||||
* variables: {
|
|
||||||
* text: // value for 'text'
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
export function useGetAisqlQueryQuery(baseOptions: Apollo.QueryHookOptions<GetAisqlQueryQuery, GetAisqlQueryQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useQuery<GetAisqlQueryQuery, GetAisqlQueryQueryVariables>(GetAisqlQueryDocument, options);
|
|
||||||
}
|
|
||||||
export function useGetAisqlQueryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetAisqlQueryQuery, GetAisqlQueryQueryVariables>) {
|
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
|
||||||
return Apollo.useLazyQuery<GetAisqlQueryQuery, GetAisqlQueryQueryVariables>(GetAisqlQueryDocument, options);
|
|
||||||
}
|
|
||||||
export type GetAisqlQueryQueryHookResult = ReturnType<typeof useGetAisqlQueryQuery>;
|
|
||||||
export type GetAisqlQueryLazyQueryHookResult = ReturnType<typeof useGetAisqlQueryLazyQuery>;
|
|
||||||
export type GetAisqlQueryQueryResult = Apollo.QueryResult<GetAisqlQueryQuery, GetAisqlQueryQueryVariables>;
|
|
||||||
export const DeleteUserAccountDocument = gql`
|
export const DeleteUserAccountDocument = gql`
|
||||||
mutation DeleteUserAccount {
|
mutation DeleteUserAccount {
|
||||||
deleteUser {
|
deleteUser {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
export const TRACK = gql`
|
export const TRACK = gql`
|
||||||
mutation Track($type: String!, $sessionId: String!, $data: JSON!) {
|
mutation Track($action: String!, $payload: JSON!) {
|
||||||
track(type: $type, sessionId: $sessionId, data: $data) {
|
track(action: $action, payload: $payload) {
|
||||||
success
|
success
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ const makeRequest = async () => {
|
|||||||
|
|
||||||
await client.mutate({
|
await client.mutate({
|
||||||
mutation: gql`
|
mutation: gql`
|
||||||
mutation Track($type: String!, $sessionId: String!, $data: JSON!) {
|
mutation Track($action: String!, $payload: JSON!) {
|
||||||
track(type: $type, sessionId: $sessionId, data: $data) {
|
track(action: $action, payload: $payload) {
|
||||||
success
|
success
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { gql } from '@apollo/client';
|
|
||||||
|
|
||||||
export const getCopilot = gql`
|
|
||||||
query GetAISQLQuery($text: String!) {
|
|
||||||
getAISQLQuery(text: $text) {
|
|
||||||
sqlQuery
|
|
||||||
sqlQueryResult
|
|
||||||
queryFailedErrorMessage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { TestingModule, Test } from '@nestjs/testing';
|
|
||||||
|
|
||||||
import { ArgsAliasFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory';
|
|
||||||
import { ArgsStringFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/args-string.factory';
|
|
||||||
|
|
||||||
describe('ArgsStringFactory', () => {
|
|
||||||
let service: ArgsStringFactory;
|
|
||||||
const argsAliasCreate = jest.fn();
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
ArgsStringFactory,
|
|
||||||
{
|
|
||||||
provide: ArgsAliasFactory,
|
|
||||||
useValue: {
|
|
||||||
create: argsAliasCreate,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
service = module.get<ArgsStringFactory>(ArgsStringFactory);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(service).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('create', () => {
|
|
||||||
it('should return null when args are missing', () => {
|
|
||||||
const args = undefined;
|
|
||||||
|
|
||||||
const result = service.create(args, []);
|
|
||||||
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a string with the args when args are present', () => {
|
|
||||||
const args = {
|
|
||||||
id: '1',
|
|
||||||
name: 'field_name',
|
|
||||||
};
|
|
||||||
|
|
||||||
argsAliasCreate.mockReturnValue(args);
|
|
||||||
|
|
||||||
const result = service.create(args, []);
|
|
||||||
|
|
||||||
expect(result).toEqual('id: "1", name: "field_name"');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a string with the args when args are present and the value is an object', () => {
|
|
||||||
const args = {
|
|
||||||
id: '1',
|
|
||||||
name: {
|
|
||||||
firstName: 'test',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
argsAliasCreate.mockReturnValue(args);
|
|
||||||
|
|
||||||
const result = service.create(args, []);
|
|
||||||
|
|
||||||
expect(result).toEqual('id: "1", name: {firstName:"test"}');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('when orderBy is present, should return an array of objects', () => {
|
|
||||||
const args = {
|
|
||||||
orderBy: [{ id: 'AscNullsFirst' }, { name: 'AscNullsFirst' }],
|
|
||||||
};
|
|
||||||
|
|
||||||
argsAliasCreate.mockReturnValue(args);
|
|
||||||
|
|
||||||
const result = service.create(args, []);
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
'orderBy: [{id: AscNullsFirst}, {name: AscNullsFirst}]',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('when orderBy is present with position criteria, should return position at the end of the list', () => {
|
|
||||||
const args = {
|
|
||||||
orderBy: [
|
|
||||||
{ position: 'AscNullsFirst' },
|
|
||||||
{ id: 'AscNullsFirst' },
|
|
||||||
{ name: 'AscNullsFirst' },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
argsAliasCreate.mockReturnValue(args);
|
|
||||||
|
|
||||||
const result = service.create(args, []);
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
'orderBy: [{id: AscNullsFirst}, {name: AscNullsFirst}, {position: AscNullsFirst}]',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('when orderBy is present with position in the middle, should return position at the end of the list', () => {
|
|
||||||
const args = {
|
|
||||||
orderBy: [
|
|
||||||
{ id: 'AscNullsFirst' },
|
|
||||||
{ position: 'AscNullsFirst' },
|
|
||||||
{ name: 'AscNullsFirst' },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
argsAliasCreate.mockReturnValue(args);
|
|
||||||
|
|
||||||
const result = service.create(args, []);
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
'orderBy: [{id: AscNullsFirst}, {name: AscNullsFirst}, {position: AscNullsFirst}]',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
|
||||||
|
|
||||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
|
||||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
|
||||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ArgsAliasFactory {
|
|
||||||
private readonly logger = new Logger(ArgsAliasFactory.name);
|
|
||||||
|
|
||||||
create(
|
|
||||||
args: Record<string, any>,
|
|
||||||
fieldMetadataCollection: FieldMetadataInterface[],
|
|
||||||
): Record<string, any> {
|
|
||||||
const fieldMetadataMap = new Map(
|
|
||||||
fieldMetadataCollection.map((fieldMetadata) => [
|
|
||||||
fieldMetadata.name,
|
|
||||||
fieldMetadata,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.createArgsObjectRecursive(args, fieldMetadataMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
private createArgsObjectRecursive(
|
|
||||||
args: Record<string, any>,
|
|
||||||
fieldMetadataMap: Map<string, FieldMetadataInterface>,
|
|
||||||
) {
|
|
||||||
// If it's not an object, we don't need to do anything
|
|
||||||
if (typeof args !== 'object' || args === null) {
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's an array, we need to map all items
|
|
||||||
if (Array.isArray(args)) {
|
|
||||||
return args.map((arg) =>
|
|
||||||
this.createArgsObjectRecursive(arg, fieldMetadataMap),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newArgs = {};
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(args)) {
|
|
||||||
const fieldMetadata = fieldMetadataMap.get(key);
|
|
||||||
|
|
||||||
// If it's a composite type, we need to transform args to properly map column name
|
|
||||||
if (
|
|
||||||
fieldMetadata &&
|
|
||||||
value !== null &&
|
|
||||||
isCompositeFieldMetadataType(fieldMetadata.type)
|
|
||||||
) {
|
|
||||||
// Get composite type definition
|
|
||||||
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
|
|
||||||
|
|
||||||
if (!compositeType) {
|
|
||||||
this.logger.error(
|
|
||||||
`Composite type definition not found for type: ${fieldMetadata.type}`,
|
|
||||||
);
|
|
||||||
throw new Error(
|
|
||||||
`Composite type definition not found for type: ${fieldMetadata.type}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through sub values and map them to composite property
|
|
||||||
for (const [subKey, subValue] of Object.entries(value)) {
|
|
||||||
// Find composite property
|
|
||||||
const compositeProperty = compositeType.properties.find(
|
|
||||||
(property) => property.name === subKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (compositeProperty) {
|
|
||||||
const columnName = computeCompositeColumnName(
|
|
||||||
fieldMetadata,
|
|
||||||
compositeProperty,
|
|
||||||
);
|
|
||||||
|
|
||||||
newArgs[columnName] = subValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (fieldMetadata) {
|
|
||||||
newArgs[key] = value;
|
|
||||||
} else {
|
|
||||||
// Recurse if value is a nested object, otherwise append field or alias
|
|
||||||
newArgs[key] = this.createArgsObjectRecursive(value, fieldMetadataMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newArgs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
|
||||||
|
|
||||||
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
|
|
||||||
import { isDefined } from 'src/utils/is-defined';
|
|
||||||
|
|
||||||
import { ArgsAliasFactory } from './args-alias.factory';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ArgsStringFactory {
|
|
||||||
constructor(private readonly argsAliasFactory: ArgsAliasFactory) {}
|
|
||||||
|
|
||||||
create(
|
|
||||||
initialArgs: Record<string, any> | undefined,
|
|
||||||
fieldMetadataCollection: FieldMetadataInterface[],
|
|
||||||
softDeletable?: boolean,
|
|
||||||
): string | null {
|
|
||||||
if (!initialArgs) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (softDeletable) {
|
|
||||||
initialArgs.filter = {
|
|
||||||
and: [initialArgs.filter, { deletedAt: { is: 'NULL' } }].filter(
|
|
||||||
isDefined,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let argsString = '';
|
|
||||||
const computedArgs = this.argsAliasFactory.create(
|
|
||||||
initialArgs,
|
|
||||||
fieldMetadataCollection,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const key in computedArgs) {
|
|
||||||
// Check if the value is not undefined
|
|
||||||
if (computedArgs[key] === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof computedArgs[key] === 'string') {
|
|
||||||
// If it's a string, add quotes
|
|
||||||
argsString += `${key}: "${computedArgs[key]}", `;
|
|
||||||
} else if (
|
|
||||||
typeof computedArgs[key] === 'object' &&
|
|
||||||
computedArgs[key] !== null
|
|
||||||
) {
|
|
||||||
if (key === 'orderBy') {
|
|
||||||
argsString += `${key}: ${this.buildStringifiedOrderBy(
|
|
||||||
computedArgs[key],
|
|
||||||
)}, `;
|
|
||||||
} else {
|
|
||||||
// If it's an object (and not null), stringify it
|
|
||||||
argsString += `${key}: ${stringifyWithoutKeyQuote(
|
|
||||||
computedArgs[key],
|
|
||||||
)}, `;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For other types (number, boolean), add as is
|
|
||||||
argsString += `${key}: ${computedArgs[key]}, `;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove trailing comma and space, if present
|
|
||||||
if (argsString.endsWith(', ')) {
|
|
||||||
argsString = argsString.slice(0, -2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return argsString;
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildStringifiedOrderBy(
|
|
||||||
keyValuePairArray: Array<Record<string, any>>,
|
|
||||||
): string {
|
|
||||||
if (
|
|
||||||
keyValuePairArray.length !== 0 &&
|
|
||||||
Object.keys(keyValuePairArray[0]).length === 0
|
|
||||||
) {
|
|
||||||
return `[]`;
|
|
||||||
}
|
|
||||||
// if position argument is present we want to put it at the very last
|
|
||||||
let orderByString = keyValuePairArray
|
|
||||||
.sort((_, obj) => (Object.hasOwnProperty.call(obj, 'position') ? -1 : 0))
|
|
||||||
.map((obj) => {
|
|
||||||
const [key] = Object.keys(obj);
|
|
||||||
const value = obj[key];
|
|
||||||
|
|
||||||
return `{${key}: ${value}}`;
|
|
||||||
})
|
|
||||||
.join(', ');
|
|
||||||
|
|
||||||
if (orderByString.endsWith(', ')) {
|
|
||||||
orderByString = orderByString.slice(0, -2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `[${orderByString}]`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
|
||||||
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
|
||||||
import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
|
||||||
|
|
||||||
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
||||||
|
|
||||||
import { ArgsAliasFactory } from './args-alias.factory';
|
|
||||||
import { FieldsStringFactory } from './fields-string.factory';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CreateManyQueryFactory {
|
|
||||||
constructor(
|
|
||||||
private readonly fieldsStringFactory: FieldsStringFactory,
|
|
||||||
private readonly argsAliasFactory: ArgsAliasFactory,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async create<Record extends IRecord = IRecord>(
|
|
||||||
args: CreateManyResolverArgs<Partial<Record>>,
|
|
||||||
options: WorkspaceQueryBuilderOptions,
|
|
||||||
) {
|
|
||||||
const fieldsString = await this.fieldsStringFactory.create(
|
|
||||||
options.info,
|
|
||||||
options.fieldMetadataCollection,
|
|
||||||
options.objectMetadataCollection,
|
|
||||||
);
|
|
||||||
|
|
||||||
const computedArgsData = this.argsAliasFactory.create(
|
|
||||||
args.data,
|
|
||||||
options.fieldMetadataCollection,
|
|
||||||
);
|
|
||||||
|
|
||||||
return `
|
|
||||||
mutation {
|
|
||||||
insertInto${computeObjectTargetTable(
|
|
||||||
options.objectMetadataItem,
|
|
||||||
)}Collection(objects: ${stringifyWithoutKeyQuote(
|
|
||||||
computedArgsData.map((datum) => ({
|
|
||||||
id: uuidv4(),
|
|
||||||
...datum,
|
|
||||||
})),
|
|
||||||
)}) {
|
|
||||||
affectedCount
|
|
||||||
records {
|
|
||||||
${fieldsString}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
|
||||||
import { DeleteManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
|
||||||
|
|
||||||
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
||||||
|
|
||||||
import { FieldsStringFactory } from './fields-string.factory';
|
|
||||||
|
|
||||||
export interface DeleteManyQueryFactoryOptions
|
|
||||||
extends WorkspaceQueryBuilderOptions {
|
|
||||||
atMost?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DeleteManyQueryFactory {
|
|
||||||
constructor(private readonly fieldsStringFactory: FieldsStringFactory) {}
|
|
||||||
|
|
||||||
async create(
|
|
||||||
args: DeleteManyResolverArgs,
|
|
||||||
options: DeleteManyQueryFactoryOptions,
|
|
||||||
) {
|
|
||||||
const fieldsString = await this.fieldsStringFactory.create(
|
|
||||||
options.info,
|
|
||||||
options.fieldMetadataCollection,
|
|
||||||
options.objectMetadataCollection,
|
|
||||||
);
|
|
||||||
|
|
||||||
return `
|
|
||||||
mutation {
|
|
||||||
deleteFrom${computeObjectTargetTable(
|
|
||||||
options.objectMetadataItem,
|
|
||||||
)}Collection(filter: ${stringifyWithoutKeyQuote(
|
|
||||||
args.filter,
|
|
||||||
)}, atMost: ${options.atMost ?? 1}) {
|
|
||||||
affectedCount
|
|
||||||
records {
|
|
||||||
${fieldsString}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
|
||||||
import { DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
|
||||||
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
||||||
|
|
||||||
import { FieldsStringFactory } from './fields-string.factory';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DeleteOneQueryFactory {
|
|
||||||
constructor(private readonly fieldsStringFactory: FieldsStringFactory) {}
|
|
||||||
|
|
||||||
async create(
|
|
||||||
args: DeleteOneResolverArgs,
|
|
||||||
options: WorkspaceQueryBuilderOptions,
|
|
||||||
) {
|
|
||||||
const fieldsString = await this.fieldsStringFactory.create(
|
|
||||||
options.info,
|
|
||||||
options.fieldMetadataCollection,
|
|
||||||
options.objectMetadataCollection,
|
|
||||||
);
|
|
||||||
|
|
||||||
return `
|
|
||||||
mutation {
|
|
||||||
deleteFrom${computeObjectTargetTable(
|
|
||||||
options.objectMetadataItem,
|
|
||||||
)}Collection(filter: { id: { eq: "${args.id}" } }) {
|
|
||||||
affectedCount
|
|
||||||
records {
|
|
||||||
${fieldsString}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +1,8 @@
|
|||||||
import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory';
|
import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory';
|
||||||
|
|
||||||
import { ArgsAliasFactory } from './args-alias.factory';
|
|
||||||
import { ArgsStringFactory } from './args-string.factory';
|
|
||||||
import { CreateManyQueryFactory } from './create-many-query.factory';
|
|
||||||
import { DeleteManyQueryFactory } from './delete-many-query.factory';
|
|
||||||
import { DeleteOneQueryFactory } from './delete-one-query.factory';
|
|
||||||
import { FieldAliasFactory } from './field-alias.factory';
|
|
||||||
import { FieldsStringFactory } from './fields-string.factory';
|
|
||||||
import { FindDuplicatesQueryFactory } from './find-duplicates-query.factory';
|
|
||||||
import { FindManyQueryFactory } from './find-many-query.factory';
|
|
||||||
import { FindOneQueryFactory } from './find-one-query.factory';
|
|
||||||
import { RecordPositionQueryFactory } from './record-position-query.factory';
|
import { RecordPositionQueryFactory } from './record-position-query.factory';
|
||||||
import { RelationFieldAliasFactory } from './relation-field-alias.factory';
|
|
||||||
import { UpdateManyQueryFactory } from './update-many-query.factory';
|
|
||||||
import { UpdateOneQueryFactory } from './update-one-query.factory';
|
|
||||||
|
|
||||||
export const workspaceQueryBuilderFactories = [
|
export const workspaceQueryBuilderFactories = [
|
||||||
ArgsAliasFactory,
|
|
||||||
ArgsStringFactory,
|
|
||||||
RelationFieldAliasFactory,
|
|
||||||
CreateManyQueryFactory,
|
|
||||||
DeleteOneQueryFactory,
|
|
||||||
FieldAliasFactory,
|
|
||||||
FieldsStringFactory,
|
|
||||||
FindManyQueryFactory,
|
|
||||||
FindOneQueryFactory,
|
|
||||||
FindDuplicatesQueryFactory,
|
|
||||||
RecordPositionQueryFactory,
|
RecordPositionQueryFactory,
|
||||||
UpdateOneQueryFactory,
|
|
||||||
UpdateManyQueryFactory,
|
|
||||||
DeleteManyQueryFactory,
|
|
||||||
ForeignDataWrapperServerQueryFactory,
|
ForeignDataWrapperServerQueryFactory,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
|
||||||
|
|
||||||
import { createCompositeFieldKey } from 'src/engine/api/graphql/workspace-query-builder/utils/composite-field-metadata.util';
|
|
||||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
|
||||||
import {
|
|
||||||
computeColumnName,
|
|
||||||
computeCompositeColumnName,
|
|
||||||
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
|
||||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class FieldAliasFactory {
|
|
||||||
private readonly logger = new Logger(FieldAliasFactory.name);
|
|
||||||
|
|
||||||
create(fieldKey: string, fieldMetadata: FieldMetadataInterface) {
|
|
||||||
// If it's not a composite field, we can just return the alias
|
|
||||||
if (!isCompositeFieldMetadataType(fieldMetadata.type)) {
|
|
||||||
const alias = computeColumnName(fieldMetadata);
|
|
||||||
|
|
||||||
return `${fieldKey}: ${alias}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's a composite field, we need to get the definition
|
|
||||||
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
|
|
||||||
|
|
||||||
if (!compositeType) {
|
|
||||||
this.logger.error(
|
|
||||||
`Composite type not found for field metadata type: ${fieldMetadata.type}`,
|
|
||||||
);
|
|
||||||
throw new Error(
|
|
||||||
`Composite type not found for field metadata type: ${fieldMetadata.type}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return compositeType.properties
|
|
||||||
.map((property) => {
|
|
||||||
// Generate a prefixed key for the composite field, this will be computed when the query has ran
|
|
||||||
const compositeKey = createCompositeFieldKey(
|
|
||||||
fieldMetadata.name,
|
|
||||||
property.name,
|
|
||||||
);
|
|
||||||
const alias = computeCompositeColumnName(fieldMetadata, property);
|
|
||||||
|
|
||||||
return `${compositeKey}: ${alias}`;
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { GraphQLResolveInfo } from 'graphql';
|
|
||||||
import graphqlFields from 'graphql-fields';
|
|
||||||
import isEmpty from 'lodash.isempty';
|
|
||||||
|
|
||||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
|
||||||
import { Record } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
|
||||||
|
|
||||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
|
||||||
|
|
||||||
import { FieldAliasFactory } from './field-alias.factory';
|
|
||||||
import { RelationFieldAliasFactory } from './relation-field-alias.factory';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class FieldsStringFactory {
|
|
||||||
constructor(
|
|
||||||
private readonly fieldAliasFactory: FieldAliasFactory,
|
|
||||||
private readonly relationFieldAliasFactory: RelationFieldAliasFactory,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async create(
|
|
||||||
info: GraphQLResolveInfo,
|
|
||||||
fieldMetadataCollection: FieldMetadataInterface[],
|
|
||||||
objectMetadataCollection: ObjectMetadataInterface[],
|
|
||||||
withSoftDeleted?: boolean,
|
|
||||||
): Promise<string> {
|
|
||||||
const selectedFields: Partial<Record> = graphqlFields(info);
|
|
||||||
|
|
||||||
const res = await this.createFieldsStringRecursive(
|
|
||||||
info,
|
|
||||||
selectedFields,
|
|
||||||
fieldMetadataCollection,
|
|
||||||
objectMetadataCollection,
|
|
||||||
withSoftDeleted ?? false,
|
|
||||||
);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createFieldsStringRecursive(
|
|
||||||
info: GraphQLResolveInfo,
|
|
||||||
selectedFields: Partial<Record>,
|
|
||||||
fieldMetadataCollection: FieldMetadataInterface[],
|
|
||||||
objectMetadataCollection: ObjectMetadataInterface[],
|
|
||||||
withSoftDeleted: boolean,
|
|
||||||
accumulator = '',
|
|
||||||
): Promise<string> {
|
|
||||||
const fieldMetadataMap = new Map(
|
|
||||||
fieldMetadataCollection.map((metadata) => [metadata.name, metadata]),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const [fieldKey, fieldValue] of Object.entries(selectedFields)) {
|
|
||||||
let fieldAlias: string | null;
|
|
||||||
|
|
||||||
if (fieldMetadataMap.has(fieldKey)) {
|
|
||||||
// We're sure that the field exists in the map after this if condition
|
|
||||||
// ES6 should tackle that more properly
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const fieldMetadata = fieldMetadataMap.get(fieldKey)!;
|
|
||||||
|
|
||||||
// If the field is a relation field, we need to create a special alias
|
|
||||||
if (isRelationFieldMetadataType(fieldMetadata.type)) {
|
|
||||||
const alias = await this.relationFieldAliasFactory.create(
|
|
||||||
fieldKey,
|
|
||||||
fieldValue,
|
|
||||||
fieldMetadata,
|
|
||||||
objectMetadataCollection,
|
|
||||||
info,
|
|
||||||
withSoftDeleted,
|
|
||||||
);
|
|
||||||
|
|
||||||
fieldAlias = alias;
|
|
||||||
} else {
|
|
||||||
// Otherwise we just need to create a simple alias
|
|
||||||
const alias = this.fieldAliasFactory.create(fieldKey, fieldMetadata);
|
|
||||||
|
|
||||||
fieldAlias = alias;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldAlias ??= fieldKey;
|
|
||||||
|
|
||||||
// Recurse if value is a nested object, otherwise append field or alias
|
|
||||||
if (
|
|
||||||
!fieldMetadataMap.has(fieldKey) &&
|
|
||||||
fieldValue &&
|
|
||||||
typeof fieldValue === 'object' &&
|
|
||||||
!isEmpty(fieldValue)
|
|
||||||
) {
|
|
||||||
accumulator += `${fieldKey} {\n`;
|
|
||||||
accumulator = await this.createFieldsStringRecursive(
|
|
||||||
info,
|
|
||||||
fieldValue,
|
|
||||||
fieldMetadataCollection,
|
|
||||||
objectMetadataCollection,
|
|
||||||
withSoftDeleted,
|
|
||||||
accumulator,
|
|
||||||
);
|
|
||||||
accumulator += `}\n`;
|
|
||||||
} else {
|
|
||||||
accumulator += `${fieldAlias}\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return accumulator;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import isEmpty from 'lodash.isempty';
|
|
||||||
|
|
||||||
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
|
||||||
import { Record } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
|
||||||
import { FindDuplicatesResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
|
||||||
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
||||||
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
|
|
||||||
import { ArgsAliasFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory';
|
|
||||||
import { DuplicateService } from 'src/engine/core-modules/duplicate/duplicate.service';
|
|
||||||
|
|
||||||
import { FieldsStringFactory } from './fields-string.factory';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class FindDuplicatesQueryFactory {
|
|
||||||
constructor(
|
|
||||||
private readonly fieldsStringFactory: FieldsStringFactory,
|
|
||||||
private readonly argsAliasFactory: ArgsAliasFactory,
|
|
||||||
private readonly duplicateService: DuplicateService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async create(
|
|
||||||
args: FindDuplicatesResolverArgs,
|
|
||||||
options: WorkspaceQueryBuilderOptions,
|
|
||||||
existingRecords?: Record[],
|
|
||||||
) {
|
|
||||||
const fieldsString = await this.fieldsStringFactory.create(
|
|
||||||
options.info,
|
|
||||||
options.fieldMetadataCollection,
|
|
||||||
options.objectMetadataCollection,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingRecords) {
|
|
||||||
const query = existingRecords.reduce((acc, record, index) => {
|
|
||||||
return (
|
|
||||||
acc + this.buildQuery(fieldsString, options, undefined, record, index)
|
|
||||||
);
|
|
||||||
}, '');
|
|
||||||
|
|
||||||
return `query {
|
|
||||||
${query}
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = args.data?.reduce((acc, dataItem, index) => {
|
|
||||||
const argsData = this.argsAliasFactory.create(
|
|
||||||
dataItem ?? {},
|
|
||||||
options.fieldMetadataCollection,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
acc +
|
|
||||||
this.buildQuery(
|
|
||||||
fieldsString,
|
|
||||||
options,
|
|
||||||
argsData as Record,
|
|
||||||
undefined,
|
|
||||||
index,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, '');
|
|
||||||
|
|
||||||
return `query {
|
|
||||||
${query}
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
buildQuery(
|
|
||||||
fieldsString: string,
|
|
||||||
options: WorkspaceQueryBuilderOptions,
|
|
||||||
data?: Record,
|
|
||||||
existingRecord?: Record,
|
|
||||||
index?: number,
|
|
||||||
) {
|
|
||||||
const duplicateCondition =
|
|
||||||
this.duplicateService.buildDuplicateConditionForGraphQL(
|
|
||||||
options.objectMetadataItem,
|
|
||||||
data ?? existingRecord,
|
|
||||||
existingRecord?.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const filters = stringifyWithoutKeyQuote(duplicateCondition);
|
|
||||||
|
|
||||||
return `${computeObjectTargetTable(
|
|
||||||
options.objectMetadataItem,
|
|
||||||
)}Collection${index}: ${computeObjectTargetTable(
|
|
||||||
options.objectMetadataItem,
|
|
||||||
)}Collection${
|
|
||||||
isEmpty(duplicateCondition?.or) ? '(first: 0)' : `(filter: ${filters})`
|
|
||||||
} {
|
|
||||||
${fieldsString}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import {
|
|
||||||
RecordFilter,
|
|
||||||
RecordOrderBy,
|
|
||||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
|
||||||
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
|
||||||
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
|
||||||
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
||||||
|
|
||||||
import { ArgsStringFactory } from './args-string.factory';
|
|
||||||
import { FieldsStringFactory } from './fields-string.factory';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class FindManyQueryFactory {
|
|
||||||
constructor(
|
|
||||||
private readonly fieldsStringFactory: FieldsStringFactory,
|
|
||||||
private readonly argsStringFactory: ArgsStringFactory,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async create<
|
|
||||||
Filter extends RecordFilter = RecordFilter,
|
|
||||||
OrderBy extends RecordOrderBy = RecordOrderBy,
|
|
||||||
>(
|
|
||||||
args: FindManyResolverArgs<Filter, OrderBy>,
|
|
||||||
options: WorkspaceQueryBuilderOptions,
|
|
||||||
) {
|
|
||||||
const fieldsString = await this.fieldsStringFactory.create(
|
|
||||||
options.info,
|
|
||||||
options.fieldMetadataCollection,
|
|
||||||
options.objectMetadataCollection,
|
|
||||||
);
|
|
||||||
const argsString = this.argsStringFactory.create(
|
|
||||||
args,
|
|
||||||
options.fieldMetadataCollection,
|
|
||||||
!options.withSoftDeleted,
|
|
||||||
);
|
|
||||||
|
|
||||||
return `
|
|
||||||
query {
|
|
||||||
${computeObjectTargetTable(options.objectMetadataItem)}Collection${
|
|
||||||
argsString ? `(${argsString})` : ''
|
|
||||||
} {
|
|
||||||
${fieldsString}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
|
||||||
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
|
||||||
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
|
||||||
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
||||||
|
|
||||||
import { ArgsStringFactory } from './args-string.factory';
|
|
||||||
import { FieldsStringFactory } from './fields-string.factory';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class FindOneQueryFactory {
|
|
||||||
constructor(
|
|
||||||
private readonly fieldsStringFactory: FieldsStringFactory,
|
|
||||||
private readonly argsStringFactory: ArgsStringFactory,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async create<Filter extends RecordFilter = RecordFilter>(
|
|
||||||
args: FindOneResolverArgs<Filter>,
|
|
||||||
options: WorkspaceQueryBuilderOptions,
|
|
||||||
) {
|
|
||||||
const fieldsString = await this.fieldsStringFactory.create(
|
|
||||||
options.info,
|
|
||||||
options.fieldMetadataCollection,
|
|
||||||
options.objectMetadataCollection,
|
|
||||||
options.withSoftDeleted,
|
|
||||||
);
|
|
||||||
const argsString = this.argsStringFactory.create(
|
|
||||||
args,
|
|
||||||
options.fieldMetadataCollection,
|
|
||||||
!options.withSoftDeleted,
|
|
||||||
);
|
|
||||||
|
|
||||||
return `
|
|
||||||
query {
|
|
||||||
${computeObjectTargetTable(options.objectMetadataItem)}Collection${
|
|
||||||
argsString ? `(${argsString})` : ''
|
|
||||||
} {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
${fieldsString}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { GraphQLResolveInfo } from 'graphql';
|
|
||||||
|
|
||||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
|
||||||
|
|
||||||
import { getFieldArgumentsByKey } from 'src/engine/api/graphql/workspace-query-builder/utils/get-field-arguments-by-key.util';
|
|
||||||
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
|
||||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
||||||
import {
|
|
||||||
deduceRelationDirection,
|
|
||||||
RelationDirection,
|
|
||||||
} from 'src/engine/utils/deduce-relation-direction.util';
|
|
||||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
|
||||||
|
|
||||||
import { ArgsStringFactory } from './args-string.factory';
|
|
||||||
import { FieldsStringFactory } from './fields-string.factory';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RelationFieldAliasFactory {
|
|
||||||
constructor(
|
|
||||||
@Inject(forwardRef(() => FieldsStringFactory))
|
|
||||||
private readonly fieldsStringFactory: CircularDep<FieldsStringFactory>,
|
|
||||||
private readonly argsStringFactory: ArgsStringFactory,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
create(
|
|
||||||
fieldKey: string,
|
|
||||||
fieldValue: any,
|
|
||||||
fieldMetadata: FieldMetadataInterface,
|
|
||||||
objectMetadataCollection: ObjectMetadataInterface[],
|
|
||||||
info: GraphQLResolveInfo,
|
|
||||||
withSoftDeleted?: boolean,
|
|
||||||
): Promise<string> {
|
|
||||||
if (!isRelationFieldMetadataType(fieldMetadata.type)) {
|
|
||||||
throw new Error(`Field ${fieldMetadata.name} is not a relation field`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.createRelationAlias(
|
|
||||||
fieldKey,
|
|
||||||
fieldValue,
|
|
||||||
fieldMetadata,
|
|
||||||
objectMetadataCollection,
|
|
||||||
info,
|
|
||||||
withSoftDeleted,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createRelationAlias(
|
|
||||||
fieldKey: string,
|
|
||||||
fieldValue: any,
|
|
||||||
fieldMetadata: FieldMetadataInterface,
|
|
||||||
objectMetadataCollection: ObjectMetadataInterface[],
|
|
||||||
info: GraphQLResolveInfo,
|
|
||||||
withSoftDeleted?: boolean,
|
|
||||||
): Promise<string> {
|
|
||||||
const relationMetadata =
|
|
||||||
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
|
|
||||||
|
|
||||||
if (!relationMetadata) {
|
|
||||||
throw new Error(
|
|
||||||
`Relation metadata not found for field ${fieldMetadata.name}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fieldMetadata.workspaceId) {
|
|
||||||
throw new Error(
|
|
||||||
`Workspace id not found for field ${fieldMetadata.name} in object metadata ${fieldMetadata.objectMetadataId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const relationDirection = deduceRelationDirection(
|
|
||||||
fieldMetadata,
|
|
||||||
relationMetadata,
|
|
||||||
);
|
|
||||||
// Retrieve the referenced object metadata based on the relation direction
|
|
||||||
// Mandatory to handle n+n relations
|
|
||||||
const referencedObjectMetadata = objectMetadataCollection.find(
|
|
||||||
(objectMetadata) =>
|
|
||||||
objectMetadata.id ===
|
|
||||||
(relationDirection == RelationDirection.TO
|
|
||||||
? relationMetadata.fromObjectMetadataId
|
|
||||||
: relationMetadata.toObjectMetadataId),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!referencedObjectMetadata) {
|
|
||||||
throw new Error(
|
|
||||||
`Referenced object metadata not found for relation ${relationMetadata.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's a relation destination is of kind MANY, we need to add the collection suffix and extract the args
|
|
||||||
if (
|
|
||||||
relationMetadata.relationType === RelationMetadataType.ONE_TO_MANY &&
|
|
||||||
relationDirection === RelationDirection.FROM
|
|
||||||
) {
|
|
||||||
const args = getFieldArgumentsByKey(info, fieldKey);
|
|
||||||
|
|
||||||
const argsString = this.argsStringFactory.create(
|
|
||||||
args,
|
|
||||||
referencedObjectMetadata.fields ?? [],
|
|
||||||
!withSoftDeleted,
|
|
||||||
);
|
|
||||||
const fieldsString =
|
|
||||||
await this.fieldsStringFactory.createFieldsStringRecursive(
|
|
||||||
info,
|
|
||||||
fieldValue,
|
|
||||||
referencedObjectMetadata.fields ?? [],
|
|
||||||
objectMetadataCollection,
|
|
||||||
withSoftDeleted ?? false,
|
|
||||||
);
|
|
||||||
|
|
||||||
return `
|
|
||||||
${fieldKey}: ${computeObjectTargetTable(
|
|
||||||
referencedObjectMetadata,
|
|
||||||
)}Collection${argsString ? `(${argsString})` : ''} {
|
|
||||||
${fieldsString}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let relationAlias = `${fieldKey}: ${computeColumnName(fieldMetadata)}`;
|
|
||||||
|
|
||||||
// For one to one relations, pg_graphql use the target TableName on the side that is not storing the foreign key
|
|
||||||
// so we need to alias it to the field key
|
|
||||||
if (
|
|
||||||
relationMetadata.relationType === RelationMetadataType.ONE_TO_ONE &&
|
|
||||||
relationDirection === RelationDirection.FROM
|
|
||||||
) {
|
|
||||||
relationAlias = `${fieldKey}: ${computeObjectTargetTable(
|
|
||||||
referencedObjectMetadata,
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
const fieldsString =
|
|
||||||
await this.fieldsStringFactory.createFieldsStringRecursive(
|
|
||||||
info,
|
|
||||||
fieldValue,
|
|
||||||
referencedObjectMetadata.fields ?? [],
|
|
||||||
objectMetadataCollection,
|
|
||||||
withSoftDeleted ?? false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Otherwise it means it's a relation destination is of kind ONE
|
|
||||||
return `
|
|
||||||
${relationAlias} {
|
|
||||||
${fieldsString}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Record as IRecord,
|
|
||||||
RecordFilter,
|
|
||||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
|
||||||
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
|
||||||
import { UpdateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
|
||||||
|
|
||||||
import { ArgsAliasFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/args-alias.factory';
|
|
||||||
import { FieldsStringFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/fields-string.factory';
|
|
||||||
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
||||||
|
|
||||||
export interface UpdateManyQueryFactoryOptions
|
|
||||||
extends WorkspaceQueryBuilderOptions {
|
|
||||||
atMost?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UpdateManyQueryFactory {
|
|
||||||
constructor(
|
|
||||||
private readonly fieldsStringFactory: FieldsStringFactory,
|
|
||||||
private readonly argsAliasFactory: ArgsAliasFactory,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async create<
|
|
||||||
Record extends IRecord = IRecord,
|
|
||||||
Filter extends RecordFilter = RecordFilter,
|
|
||||||
>(
|
|
||||||
args: UpdateManyResolverArgs<Partial<Record>, Filter>,
|
|
||||||
options: UpdateManyQueryFactoryOptions,
|
|
||||||
) {
|
|
||||||
const fieldsString = await this.fieldsStringFactory.create(
|
|
||||||
options.info,
|
|
||||||
options.fieldMetadataCollection,
|
|
||||||
options.objectMetadataCollection,
|
|
||||||
);
|
|
||||||
|
|
||||||
const computedArgsData = this.argsAliasFactory.create(
|
|
||||||
args.data,
|
|
||||||
options.fieldMetadataCollection,
|
|
||||||
);
|
|
||||||
|
|
||||||
const argsData = {
|
|
||||||
...computedArgsData,
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return `
|
|
||||||
mutation {
|
|
||||||
update${computeObjectTargetTable(options.objectMetadataItem)}Collection(
|
|
||||||
set: ${stringifyWithoutKeyQuote(argsData)},
|
|
||||||
filter: ${stringifyWithoutKeyQuote(args.filter)},
|
|
||||||
atMost: ${options.atMost ?? 1},
|
|
||||||
) {
|
|
||||||
affectedCount
|
|
||||||
records {
|
|
||||||
${fieldsString}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
|
||||||
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
|
||||||
import { UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
|
||||||
|
|
||||||
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
||||||
|
|
||||||
import { ArgsAliasFactory } from './args-alias.factory';
|
|
||||||
import { FieldsStringFactory } from './fields-string.factory';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UpdateOneQueryFactory {
|
|
||||||
constructor(
|
|
||||||
private readonly fieldsStringFactory: FieldsStringFactory,
|
|
||||||
private readonly argsAliasFactory: ArgsAliasFactory,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async create<Record extends IRecord = IRecord>(
|
|
||||||
args: UpdateOneResolverArgs<Partial<Record>>,
|
|
||||||
options: WorkspaceQueryBuilderOptions,
|
|
||||||
) {
|
|
||||||
const fieldsString = await this.fieldsStringFactory.create(
|
|
||||||
options.info,
|
|
||||||
options.fieldMetadataCollection,
|
|
||||||
options.objectMetadataCollection,
|
|
||||||
);
|
|
||||||
|
|
||||||
const computedArgsData = this.argsAliasFactory.create(
|
|
||||||
args.data,
|
|
||||||
options.fieldMetadataCollection,
|
|
||||||
);
|
|
||||||
|
|
||||||
const argsData = {
|
|
||||||
...computedArgsData,
|
|
||||||
id: undefined, // do not allow updating an existing object's id
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return `
|
|
||||||
mutation {
|
|
||||||
update${computeObjectTargetTable(
|
|
||||||
options.objectMetadataItem,
|
|
||||||
)}Collection(set: ${stringifyWithoutKeyQuote(
|
|
||||||
argsData,
|
|
||||||
)}, filter: { id: { eq: "${args.id}" } }) {
|
|
||||||
affectedCount
|
|
||||||
records {
|
|
||||||
${fieldsString}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { WorkspaceQueryBuilderOptions } from 'src/engine/api/graphql/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
|
||||||
import {
|
|
||||||
Record as IRecord,
|
|
||||||
RecordFilter,
|
|
||||||
RecordOrderBy,
|
|
||||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
|
||||||
import {
|
|
||||||
FindManyResolverArgs,
|
|
||||||
FindOneResolverArgs,
|
|
||||||
CreateManyResolverArgs,
|
|
||||||
UpdateOneResolverArgs,
|
|
||||||
DeleteOneResolverArgs,
|
|
||||||
UpdateManyResolverArgs,
|
|
||||||
DeleteManyResolverArgs,
|
|
||||||
FindDuplicatesResolverArgs,
|
|
||||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
|
||||||
|
|
||||||
import { FindManyQueryFactory } from './factories/find-many-query.factory';
|
|
||||||
import { FindOneQueryFactory } from './factories/find-one-query.factory';
|
|
||||||
import { CreateManyQueryFactory } from './factories/create-many-query.factory';
|
|
||||||
import { UpdateOneQueryFactory } from './factories/update-one-query.factory';
|
|
||||||
import { DeleteOneQueryFactory } from './factories/delete-one-query.factory';
|
|
||||||
import {
|
|
||||||
UpdateManyQueryFactory,
|
|
||||||
UpdateManyQueryFactoryOptions,
|
|
||||||
} from './factories/update-many-query.factory';
|
|
||||||
import {
|
|
||||||
DeleteManyQueryFactory,
|
|
||||||
DeleteManyQueryFactoryOptions,
|
|
||||||
} from './factories/delete-many-query.factory';
|
|
||||||
import { FindDuplicatesQueryFactory } from './factories/find-duplicates-query.factory';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class WorkspaceQueryBuilderFactory {
|
|
||||||
constructor(
|
|
||||||
private readonly findManyQueryFactory: FindManyQueryFactory,
|
|
||||||
private readonly findOneQueryFactory: FindOneQueryFactory,
|
|
||||||
private readonly findDuplicatesQueryFactory: FindDuplicatesQueryFactory,
|
|
||||||
private readonly createManyQueryFactory: CreateManyQueryFactory,
|
|
||||||
private readonly updateOneQueryFactory: UpdateOneQueryFactory,
|
|
||||||
private readonly deleteOneQueryFactory: DeleteOneQueryFactory,
|
|
||||||
private readonly updateManyQueryFactory: UpdateManyQueryFactory,
|
|
||||||
private readonly deleteManyQueryFactory: DeleteManyQueryFactory,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
findMany<
|
|
||||||
Filter extends RecordFilter = RecordFilter,
|
|
||||||
OrderBy extends RecordOrderBy = RecordOrderBy,
|
|
||||||
>(
|
|
||||||
args: FindManyResolverArgs<Filter, OrderBy>,
|
|
||||||
options: WorkspaceQueryBuilderOptions,
|
|
||||||
): Promise<string> {
|
|
||||||
return this.findManyQueryFactory.create<Filter, OrderBy>(args, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
findOne<Filter extends RecordFilter = RecordFilter>(
|
|
||||||
args: FindOneResolverArgs<Filter>,
|
|
||||||
options: WorkspaceQueryBuilderOptions,
|
|
||||||
): Promise<string> {
|
|
||||||
return this.findOneQueryFactory.create<Filter>(args, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
findDuplicates(
|
|
||||||
args: FindDuplicatesResolverArgs,
|
|
||||||
options: WorkspaceQueryBuilderOptions,
|
|
||||||
existingRecords?: IRecord[],
|
|
||||||
): Promise<string> {
|
|
||||||
return this.findDuplicatesQueryFactory.create(
|
|
||||||
args,
|
|
||||||
options,
|
|
||||||
existingRecords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
createMany<Record extends IRecord = IRecord>(
|
|
||||||
args: CreateManyResolverArgs<Partial<Record>>,
|
|
||||||
options: WorkspaceQueryBuilderOptions,
|
|
||||||
): Promise<string> {
|
|
||||||
return this.createManyQueryFactory.create<Record>(args, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateOne<Record extends IRecord = IRecord>(
|
|
||||||
initialArgs: UpdateOneResolverArgs<Partial<Record>>,
|
|
||||||
options: WorkspaceQueryBuilderOptions,
|
|
||||||
): Promise<string> {
|
|
||||||
return this.updateOneQueryFactory.create<Record>(initialArgs, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteOne(
|
|
||||||
args: DeleteOneResolverArgs,
|
|
||||||
options: WorkspaceQueryBuilderOptions,
|
|
||||||
): Promise<string> {
|
|
||||||
return this.deleteOneQueryFactory.create(args, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMany<
|
|
||||||
Record extends IRecord = IRecord,
|
|
||||||
Filter extends RecordFilter = RecordFilter,
|
|
||||||
>(
|
|
||||||
args: UpdateManyResolverArgs<Partial<Record>, Filter>,
|
|
||||||
options: UpdateManyQueryFactoryOptions,
|
|
||||||
): Promise<string> {
|
|
||||||
return this.updateManyQueryFactory.create(args, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteMany<Filter extends RecordFilter = RecordFilter>(
|
|
||||||
args: DeleteManyResolverArgs<Filter>,
|
|
||||||
options: DeleteManyQueryFactoryOptions,
|
|
||||||
): Promise<string> {
|
|
||||||
return this.deleteManyQueryFactory.create(args, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
|
||||||
import { FieldsStringFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/fields-string.factory';
|
|
||||||
import { RecordPositionQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
|
import { RecordPositionQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory';
|
||||||
import { DuplicateModule } from 'src/engine/core-modules/duplicate/duplicate.module';
|
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||||
|
|
||||||
import { WorkspaceQueryBuilderFactory } from './workspace-query-builder.factory';
|
|
||||||
|
|
||||||
import { workspaceQueryBuilderFactories } from './factories/factories';
|
import { workspaceQueryBuilderFactories } from './factories/factories';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ObjectMetadataModule, DuplicateModule],
|
imports: [ObjectMetadataModule],
|
||||||
providers: [...workspaceQueryBuilderFactories, WorkspaceQueryBuilderFactory],
|
providers: [...workspaceQueryBuilderFactories],
|
||||||
exports: [
|
exports: [RecordPositionQueryFactory],
|
||||||
WorkspaceQueryBuilderFactory,
|
|
||||||
FieldsStringFactory,
|
|
||||||
RecordPositionQueryFactory,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class WorkspaceQueryBuilderModule {}
|
export class WorkspaceQueryBuilderModule {}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { TelemetryListener } from 'src/engine/api/graphql/workspace-query-runner
|
|||||||
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
|
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
|
||||||
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
||||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||||
import { DuplicateModule } from 'src/engine/core-modules/duplicate/duplicate.module';
|
|
||||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||||
@@ -17,8 +16,6 @@ import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repos
|
|||||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
|
|
||||||
import { WorkspaceQueryRunnerService } from './workspace-query-runner.service';
|
|
||||||
|
|
||||||
import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listener';
|
import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listener';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -31,17 +28,15 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
|
|||||||
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
||||||
AnalyticsModule,
|
AnalyticsModule,
|
||||||
TelemetryModule,
|
TelemetryModule,
|
||||||
DuplicateModule,
|
|
||||||
FileModule,
|
FileModule,
|
||||||
FeatureFlagModule,
|
FeatureFlagModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
WorkspaceQueryRunnerService,
|
|
||||||
...workspaceQueryRunnerFactories,
|
...workspaceQueryRunnerFactories,
|
||||||
EntityEventsToDbListener,
|
EntityEventsToDbListener,
|
||||||
TelemetryListener,
|
TelemetryListener,
|
||||||
RecordPositionBackfillCommand,
|
RecordPositionBackfillCommand,
|
||||||
],
|
],
|
||||||
exports: [WorkspaceQueryRunnerService, ...workspaceQueryRunnerFactories],
|
exports: [...workspaceQueryRunnerFactories],
|
||||||
})
|
})
|
||||||
export class WorkspaceQueryRunnerModule {}
|
export class WorkspaceQueryRunnerModule {}
|
||||||
|
|||||||
@@ -1,942 +0,0 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
|
|
||||||
import isEmpty from 'lodash.isempty';
|
|
||||||
import { DataSource, In } from 'typeorm';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Record as IRecord,
|
|
||||||
RecordFilter,
|
|
||||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
|
||||||
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
|
||||||
import {
|
|
||||||
CreateManyResolverArgs,
|
|
||||||
CreateOneResolverArgs,
|
|
||||||
DeleteManyResolverArgs,
|
|
||||||
DeleteOneResolverArgs,
|
|
||||||
DestroyManyResolverArgs,
|
|
||||||
FindDuplicatesResolverArgs,
|
|
||||||
ResolverArgsType,
|
|
||||||
RestoreManyResolverArgs,
|
|
||||||
UpdateManyResolverArgs,
|
|
||||||
UpdateOneResolverArgs,
|
|
||||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
|
||||||
|
|
||||||
import { WorkspaceQueryBuilderFactory } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.factory';
|
|
||||||
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory';
|
|
||||||
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
|
||||||
import {
|
|
||||||
CallWebhookJobsJob,
|
|
||||||
CallWebhookJobsJobData,
|
|
||||||
CallWebhookJobsJobOperation,
|
|
||||||
} from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job';
|
|
||||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
|
||||||
import { parseResult } from 'src/engine/api/graphql/workspace-query-runner/utils/parse-result.util';
|
|
||||||
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
|
|
||||||
import {
|
|
||||||
WorkspaceQueryRunnerException,
|
|
||||||
WorkspaceQueryRunnerExceptionCode,
|
|
||||||
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
|
|
||||||
import { DuplicateService } from 'src/engine/core-modules/duplicate/duplicate.service';
|
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
|
||||||
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
|
||||||
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
|
||||||
import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
|
||||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
|
||||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
|
||||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
|
||||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
||||||
import { isQueryTimeoutError } from 'src/engine/utils/query-timeout.util';
|
|
||||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
|
||||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
|
||||||
import { isDefined } from 'src/utils/is-defined';
|
|
||||||
|
|
||||||
import {
|
|
||||||
PGGraphQLMutation,
|
|
||||||
PGGraphQLResult,
|
|
||||||
} from './interfaces/pg-graphql.interface';
|
|
||||||
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface';
|
|
||||||
import {
|
|
||||||
PgGraphQLConfig,
|
|
||||||
computePgGraphQLError,
|
|
||||||
} from './utils/compute-pg-graphql-error.util';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class WorkspaceQueryRunnerService {
|
|
||||||
private readonly logger = new Logger(WorkspaceQueryRunnerService.name);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
|
||||||
private readonly workspaceQueryBuilderFactory: WorkspaceQueryBuilderFactory,
|
|
||||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
|
||||||
private readonly queryRunnerArgsFactory: QueryRunnerArgsFactory,
|
|
||||||
private readonly queryResultGettersFactory: QueryResultGettersFactory,
|
|
||||||
@InjectMessageQueue(MessageQueue.webhookQueue)
|
|
||||||
private readonly messageQueueService: MessageQueueService,
|
|
||||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
|
||||||
private readonly workspaceQueryHookService: WorkspaceQueryHookService,
|
|
||||||
private readonly environmentService: EnvironmentService,
|
|
||||||
private readonly duplicateService: DuplicateService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async findDuplicates<TRecord extends IRecord = IRecord>(
|
|
||||||
args: FindDuplicatesResolverArgs<Partial<TRecord>>,
|
|
||||||
options: WorkspaceQueryRunnerOptions,
|
|
||||||
): Promise<IConnection<TRecord> | undefined> {
|
|
||||||
if (!args.data && !args.ids) {
|
|
||||||
throw new WorkspaceQueryRunnerException(
|
|
||||||
'You have to provide either "data" or "id" argument',
|
|
||||||
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!args.ids && isEmpty(args.data)) {
|
|
||||||
throw new WorkspaceQueryRunnerException(
|
|
||||||
'The "data" condition can not be empty when ID input not provided',
|
|
||||||
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { authContext, objectMetadataItem } = options;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`running findDuplicates for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const hookedArgs =
|
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
|
||||||
authContext,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
'findDuplicates',
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
|
|
||||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
|
||||||
hookedArgs,
|
|
||||||
options,
|
|
||||||
ResolverArgsType.FindDuplicates,
|
|
||||||
)) as FindDuplicatesResolverArgs<TRecord>;
|
|
||||||
|
|
||||||
let existingRecords: IRecord[] | undefined = undefined;
|
|
||||||
|
|
||||||
if (computedArgs.ids && computedArgs.ids.length > 0) {
|
|
||||||
existingRecords = await this.duplicateService.findExistingRecords(
|
|
||||||
computedArgs.ids,
|
|
||||||
objectMetadataItem,
|
|
||||||
authContext.workspace.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!existingRecords || existingRecords.length === 0) {
|
|
||||||
throw new WorkspaceQueryRunnerException(
|
|
||||||
`Object with id ${args.ids} not found`,
|
|
||||||
WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.findDuplicates(
|
|
||||||
computedArgs,
|
|
||||||
options,
|
|
||||||
existingRecords,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await this.execute(query, authContext.workspace.id);
|
|
||||||
|
|
||||||
return this.parseResult<IConnection<TRecord>>(
|
|
||||||
result,
|
|
||||||
objectMetadataItem,
|
|
||||||
'',
|
|
||||||
authContext.workspace.id,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createMany<Record extends IRecord = IRecord>(
|
|
||||||
args: CreateManyResolverArgs<Partial<Record>>,
|
|
||||||
options: WorkspaceQueryRunnerOptions,
|
|
||||||
): Promise<Record[] | undefined> {
|
|
||||||
const { authContext, objectMetadataItem } = options;
|
|
||||||
|
|
||||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
|
||||||
|
|
||||||
if (args.upsert) {
|
|
||||||
return await this.upsertMany(args, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
args.data.forEach((record) => {
|
|
||||||
if (record?.id) {
|
|
||||||
assertIsValidUuid(record.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const hookedArgs =
|
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
|
||||||
authContext,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
'createMany',
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
|
|
||||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
|
||||||
hookedArgs,
|
|
||||||
options,
|
|
||||||
ResolverArgsType.CreateMany,
|
|
||||||
)) as CreateManyResolverArgs<Record>;
|
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.createMany(
|
|
||||||
computedArgs,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await this.execute(query, authContext.workspace.id);
|
|
||||||
|
|
||||||
const parsedResults = (
|
|
||||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
|
||||||
result,
|
|
||||||
objectMetadataItem,
|
|
||||||
'insertInto',
|
|
||||||
authContext.workspace.id,
|
|
||||||
)
|
|
||||||
)?.records;
|
|
||||||
|
|
||||||
await this.workspaceQueryHookService.executePostQueryHooks(
|
|
||||||
authContext,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
'createMany',
|
|
||||||
parsedResults,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.triggerWebhooks<Record>(
|
|
||||||
parsedResults,
|
|
||||||
CallWebhookJobsJobOperation.create,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.workspaceEventEmitter.emit(
|
|
||||||
`${objectMetadataItem.nameSingular}.created`,
|
|
||||||
parsedResults.map(
|
|
||||||
(record) =>
|
|
||||||
({
|
|
||||||
userId: authContext.user?.id,
|
|
||||||
recordId: record.id,
|
|
||||||
objectMetadata: objectMetadataItem,
|
|
||||||
properties: {
|
|
||||||
after: record,
|
|
||||||
},
|
|
||||||
}) satisfies ObjectRecordCreateEvent<any>,
|
|
||||||
),
|
|
||||||
authContext.workspace.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return parsedResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
async upsertMany<Record extends IRecord = IRecord>(
|
|
||||||
args: CreateManyResolverArgs<Partial<Record>>,
|
|
||||||
options: WorkspaceQueryRunnerOptions,
|
|
||||||
): Promise<Record[] | undefined> {
|
|
||||||
console.log(
|
|
||||||
`running upsertMany for ${options.objectMetadataItem.nameSingular} on workspace ${options.authContext.workspace.id}`,
|
|
||||||
);
|
|
||||||
const ids = args.data
|
|
||||||
.map((item) => item.id)
|
|
||||||
.filter((id) => id !== undefined);
|
|
||||||
|
|
||||||
const existingRecords =
|
|
||||||
ids.length > 0
|
|
||||||
? await this.duplicateService.findExistingRecords(
|
|
||||||
ids as string[],
|
|
||||||
options.objectMetadataItem,
|
|
||||||
options.authContext.workspace.id,
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const existingRecordsMap = new Map(
|
|
||||||
existingRecords.map((record) => [record.id, record]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const results: Record[] = [];
|
|
||||||
const recordsToCreate: Partial<Record>[] = [];
|
|
||||||
|
|
||||||
for (const payload of args.data) {
|
|
||||||
if (payload.id && existingRecordsMap.has(payload.id)) {
|
|
||||||
const result = await this.updateOne(
|
|
||||||
{ id: payload.id, data: payload },
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
results.push(result);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
recordsToCreate.push(payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recordsToCreate.length > 0) {
|
|
||||||
const createResults = await this.createMany(
|
|
||||||
{ data: recordsToCreate } as CreateManyResolverArgs<Partial<Record>>,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (createResults) {
|
|
||||||
results.push(...createResults);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createOne<Record extends IRecord = IRecord>(
|
|
||||||
args: CreateOneResolverArgs<Partial<Record>>,
|
|
||||||
options: WorkspaceQueryRunnerOptions,
|
|
||||||
): Promise<Record | undefined> {
|
|
||||||
const results = await this.createMany(
|
|
||||||
{ data: [args.data], upsert: args.upsert },
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
return results?.[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateOne<Record extends IRecord = IRecord>(
|
|
||||||
args: UpdateOneResolverArgs<Partial<Record>>,
|
|
||||||
options: WorkspaceQueryRunnerOptions,
|
|
||||||
): Promise<Record | undefined> {
|
|
||||||
const { authContext, objectMetadataItem } = options;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`running updateOne for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const repository =
|
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
|
||||||
authContext.workspace.id,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
);
|
|
||||||
|
|
||||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
|
||||||
assertIsValidUuid(args.id);
|
|
||||||
|
|
||||||
const existingRecord = await repository.findOne({
|
|
||||||
where: { id: args.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!existingRecord) {
|
|
||||||
throw new WorkspaceQueryRunnerException(
|
|
||||||
`Object with id ${args.id} not found`,
|
|
||||||
WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hookedArgs =
|
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
|
||||||
authContext,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
'updateOne',
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.updateOne(
|
|
||||||
hookedArgs,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await this.execute(query, authContext.workspace.id);
|
|
||||||
|
|
||||||
const parsedResults = (
|
|
||||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
|
||||||
result,
|
|
||||||
objectMetadataItem,
|
|
||||||
'update',
|
|
||||||
authContext.workspace.id,
|
|
||||||
)
|
|
||||||
)?.records;
|
|
||||||
|
|
||||||
await this.triggerWebhooks<Record>(
|
|
||||||
parsedResults,
|
|
||||||
CallWebhookJobsJobOperation.update,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.workspaceEventEmitter.emit(
|
|
||||||
`${objectMetadataItem.nameSingular}.updated`,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
userId: authContext.user?.id,
|
|
||||||
recordId: existingRecord.id,
|
|
||||||
objectMetadata: objectMetadataItem,
|
|
||||||
properties: {
|
|
||||||
updatedFields: Object.keys(args.data),
|
|
||||||
before: this.removeNestedProperties(existingRecord as Record),
|
|
||||||
after: this.removeNestedProperties(parsedResults?.[0]),
|
|
||||||
},
|
|
||||||
} satisfies ObjectRecordUpdateEvent<any>,
|
|
||||||
],
|
|
||||||
authContext.workspace.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return parsedResults?.[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateMany<Record extends IRecord = IRecord>(
|
|
||||||
args: UpdateManyResolverArgs<Partial<Record>>,
|
|
||||||
options: WorkspaceQueryRunnerOptions,
|
|
||||||
): Promise<Record[] | undefined> {
|
|
||||||
const { authContext, objectMetadataItem } = options;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`running updateMany for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const repository =
|
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
|
||||||
authContext.workspace.id,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
);
|
|
||||||
|
|
||||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
|
||||||
args.filter?.id?.in?.forEach((id) => assertIsValidUuid(id));
|
|
||||||
|
|
||||||
const existingRecords = await repository.find({
|
|
||||||
where: { id: In(args.filter?.id?.in) },
|
|
||||||
});
|
|
||||||
const mappedRecords = new Map(
|
|
||||||
existingRecords.map((record) => [record.id, record]),
|
|
||||||
);
|
|
||||||
const maximumRecordAffected = this.environmentService.get(
|
|
||||||
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
|
||||||
);
|
|
||||||
|
|
||||||
const hookedArgs =
|
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
|
||||||
authContext,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
'updateMany',
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.updateMany(
|
|
||||||
hookedArgs,
|
|
||||||
{
|
|
||||||
...options,
|
|
||||||
atMost: maximumRecordAffected,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await this.execute(query, authContext.workspace.id);
|
|
||||||
|
|
||||||
const parsedResults = (
|
|
||||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
|
||||||
result,
|
|
||||||
objectMetadataItem,
|
|
||||||
'update',
|
|
||||||
authContext.workspace.id,
|
|
||||||
)
|
|
||||||
)?.records;
|
|
||||||
|
|
||||||
await this.triggerWebhooks<Record>(
|
|
||||||
parsedResults,
|
|
||||||
CallWebhookJobsJobOperation.update,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
const eventsToEmit: ObjectRecordUpdateEvent<any>[] = parsedResults
|
|
||||||
.map((record) => {
|
|
||||||
const existingRecord = mappedRecords.get(record.id);
|
|
||||||
|
|
||||||
if (!existingRecord) {
|
|
||||||
this.logger.warn(
|
|
||||||
`Record with id ${record.id} not found in the database`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
userId: authContext.user?.id,
|
|
||||||
recordId: existingRecord.id,
|
|
||||||
objectMetadata: objectMetadataItem,
|
|
||||||
properties: {
|
|
||||||
updatedFields: Object.keys(args.data),
|
|
||||||
before: this.removeNestedProperties(existingRecord as Record),
|
|
||||||
after: this.removeNestedProperties(record),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(isDefined);
|
|
||||||
|
|
||||||
this.workspaceEventEmitter.emit(
|
|
||||||
`${objectMetadataItem.nameSingular}.updated`,
|
|
||||||
eventsToEmit,
|
|
||||||
authContext.workspace.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return parsedResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteMany<
|
|
||||||
Record extends IRecord = IRecord,
|
|
||||||
Filter extends RecordFilter = RecordFilter,
|
|
||||||
>(
|
|
||||||
args: DeleteManyResolverArgs<Filter>,
|
|
||||||
options: WorkspaceQueryRunnerOptions,
|
|
||||||
): Promise<Record[] | undefined> {
|
|
||||||
const { authContext, objectMetadataItem } = options;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`running deleteMany for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
|
||||||
|
|
||||||
const maximumRecordAffected = this.environmentService.get(
|
|
||||||
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
|
||||||
);
|
|
||||||
|
|
||||||
const hookedArgs =
|
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
|
||||||
authContext,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
'deleteMany',
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.updateMany(
|
|
||||||
{
|
|
||||||
filter: hookedArgs.filter,
|
|
||||||
data: {
|
|
||||||
deletedAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...options,
|
|
||||||
atMost: maximumRecordAffected,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const repository =
|
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
|
||||||
authContext.workspace.id,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
);
|
|
||||||
|
|
||||||
const existingRecords = await repository.find({
|
|
||||||
where: { id: In(args.filter?.id?.in) },
|
|
||||||
});
|
|
||||||
const mappedRecords = new Map(
|
|
||||||
existingRecords.map((record) => [record.id, record]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await this.execute(query, authContext.workspace.id);
|
|
||||||
|
|
||||||
const parsedResults = (
|
|
||||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
|
||||||
result,
|
|
||||||
objectMetadataItem,
|
|
||||||
'update',
|
|
||||||
authContext.workspace.id,
|
|
||||||
)
|
|
||||||
)?.records;
|
|
||||||
|
|
||||||
await this.triggerWebhooks<Record>(
|
|
||||||
parsedResults,
|
|
||||||
CallWebhookJobsJobOperation.delete,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.workspaceEventEmitter.emit(
|
|
||||||
`${objectMetadataItem.nameSingular}.deleted`,
|
|
||||||
parsedResults.map((record) => {
|
|
||||||
const existingRecord = mappedRecords.get(record.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
userId: authContext.user?.id,
|
|
||||||
recordId: record.id,
|
|
||||||
objectMetadata: objectMetadataItem,
|
|
||||||
properties: {
|
|
||||||
before: this.removeNestedProperties({
|
|
||||||
...existingRecord,
|
|
||||||
...record,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
} satisfies ObjectRecordDeleteEvent<any>;
|
|
||||||
}),
|
|
||||||
authContext.workspace.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return parsedResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
async destroyMany<
|
|
||||||
Record extends IRecord = IRecord,
|
|
||||||
Filter extends RecordFilter = RecordFilter,
|
|
||||||
>(
|
|
||||||
args: DestroyManyResolverArgs<Filter>,
|
|
||||||
options: WorkspaceQueryRunnerOptions,
|
|
||||||
): Promise<Record[] | undefined> {
|
|
||||||
const { authContext, objectMetadataItem } = options;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`running destroyMany for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
|
||||||
|
|
||||||
const maximumRecordAffected = this.environmentService.get(
|
|
||||||
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
|
||||||
);
|
|
||||||
|
|
||||||
const hookedArgs =
|
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
|
||||||
authContext,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
'destroyMany',
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.deleteMany(
|
|
||||||
{
|
|
||||||
filter: {
|
|
||||||
...hookedArgs.filter,
|
|
||||||
deletedAt: { is: 'NOT_NULL' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...options,
|
|
||||||
atMost: maximumRecordAffected,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await this.execute(query, authContext.workspace.id);
|
|
||||||
|
|
||||||
const parsedResults = (
|
|
||||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
|
||||||
result,
|
|
||||||
objectMetadataItem,
|
|
||||||
'deleteFrom',
|
|
||||||
authContext.workspace.id,
|
|
||||||
)
|
|
||||||
)?.records;
|
|
||||||
|
|
||||||
await this.triggerWebhooks<Record>(
|
|
||||||
parsedResults,
|
|
||||||
CallWebhookJobsJobOperation.delete,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
return parsedResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
async restoreMany<
|
|
||||||
Record extends IRecord = IRecord,
|
|
||||||
Filter extends RecordFilter = RecordFilter,
|
|
||||||
>(
|
|
||||||
args: RestoreManyResolverArgs<Filter>,
|
|
||||||
options: WorkspaceQueryRunnerOptions,
|
|
||||||
): Promise<Record[] | undefined> {
|
|
||||||
const { authContext, objectMetadataItem } = options;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`running restoreMany for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
|
||||||
|
|
||||||
const maximumRecordAffected = this.environmentService.get(
|
|
||||||
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
|
||||||
);
|
|
||||||
|
|
||||||
const hookedArgs =
|
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
|
||||||
authContext,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
'restoreMany',
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.updateMany(
|
|
||||||
{
|
|
||||||
filter: {
|
|
||||||
...hookedArgs.filter,
|
|
||||||
deletedAt: { is: 'NOT_NULL' },
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...options,
|
|
||||||
atMost: maximumRecordAffected,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await this.execute(query, authContext.workspace.id);
|
|
||||||
|
|
||||||
const parsedResults = (
|
|
||||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
|
||||||
result,
|
|
||||||
objectMetadataItem,
|
|
||||||
'update',
|
|
||||||
authContext.workspace.id,
|
|
||||||
)
|
|
||||||
)?.records;
|
|
||||||
|
|
||||||
await this.triggerWebhooks<Record>(
|
|
||||||
parsedResults,
|
|
||||||
CallWebhookJobsJobOperation.create,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.workspaceEventEmitter.emit(
|
|
||||||
`${objectMetadataItem.nameSingular}.created`,
|
|
||||||
parsedResults.map(
|
|
||||||
(record) =>
|
|
||||||
({
|
|
||||||
userId: authContext.user?.id,
|
|
||||||
recordId: record.id,
|
|
||||||
objectMetadata: objectMetadataItem,
|
|
||||||
properties: {
|
|
||||||
after: this.removeNestedProperties(record),
|
|
||||||
},
|
|
||||||
}) satisfies ObjectRecordCreateEvent<any>,
|
|
||||||
),
|
|
||||||
authContext.workspace.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return parsedResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteOne<Record extends IRecord = IRecord>(
|
|
||||||
args: DeleteOneResolverArgs,
|
|
||||||
options: WorkspaceQueryRunnerOptions,
|
|
||||||
): Promise<Record | undefined> {
|
|
||||||
const { authContext, objectMetadataItem } = options;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`running deleteOne for ${objectMetadataItem.nameSingular} on workspace ${authContext.workspace.id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const repository =
|
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
|
||||||
authContext.workspace.id,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
);
|
|
||||||
|
|
||||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
|
||||||
assertIsValidUuid(args.id);
|
|
||||||
|
|
||||||
const hookedArgs =
|
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
|
||||||
authContext,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
'deleteOne',
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.updateOne(
|
|
||||||
{
|
|
||||||
id: hookedArgs.id,
|
|
||||||
data: {
|
|
||||||
deletedAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
const existingRecord = await repository.findOne({
|
|
||||||
where: { id: args.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await this.execute(query, authContext.workspace.id);
|
|
||||||
|
|
||||||
const parsedResults = (
|
|
||||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
|
||||||
result,
|
|
||||||
objectMetadataItem,
|
|
||||||
'update',
|
|
||||||
authContext.workspace.id,
|
|
||||||
)
|
|
||||||
)?.records;
|
|
||||||
|
|
||||||
await this.triggerWebhooks<Record>(
|
|
||||||
parsedResults,
|
|
||||||
CallWebhookJobsJobOperation.delete,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.workspaceEventEmitter.emit(
|
|
||||||
`${objectMetadataItem.nameSingular}.deleted`,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
userId: authContext.user?.id,
|
|
||||||
recordId: args.id,
|
|
||||||
objectMetadata: objectMetadataItem,
|
|
||||||
properties: {
|
|
||||||
before: {
|
|
||||||
...(existingRecord ?? {}),
|
|
||||||
...this.removeNestedProperties(parsedResults?.[0]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies ObjectRecordDeleteEvent<any>,
|
|
||||||
],
|
|
||||||
authContext.workspace.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return parsedResults?.[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeNestedProperties<Record extends IRecord = IRecord>(
|
|
||||||
record: Record,
|
|
||||||
) {
|
|
||||||
if (!record) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sanitizedRecord = {};
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(record)) {
|
|
||||||
if (value && typeof value === 'object' && value['edges']) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === '__typename') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
sanitizedRecord[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sanitizedRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
async executeSQL(
|
|
||||||
workspaceDataSource: DataSource,
|
|
||||||
workspaceId: string,
|
|
||||||
sqlQuery: string,
|
|
||||||
parameters?: any[],
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
return await workspaceDataSource?.transaction(
|
|
||||||
async (transactionManager) => {
|
|
||||||
await transactionManager.query(`
|
|
||||||
SET LOCAL search_path TO ${this.workspaceDataSourceService.getSchemaName(
|
|
||||||
workspaceId,
|
|
||||||
)};
|
|
||||||
`);
|
|
||||||
|
|
||||||
const results = transactionManager.query(sqlQuery, parameters);
|
|
||||||
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
if (isQueryTimeoutError(error)) {
|
|
||||||
throw new WorkspaceQueryRunnerException(
|
|
||||||
'The SQL request took too long to process, resulting in a query read timeout. To resolve this issue, consider modifying your query by reducing the depth of relationships or limiting the number of records being fetched.',
|
|
||||||
WorkspaceQueryRunnerExceptionCode.QUERY_TIMEOUT,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(
|
|
||||||
query: string,
|
|
||||||
workspaceId: string,
|
|
||||||
): Promise<PGGraphQLResult | undefined> {
|
|
||||||
const workspaceDataSource =
|
|
||||||
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.executeSQL(
|
|
||||||
workspaceDataSource,
|
|
||||||
workspaceId,
|
|
||||||
`SELECT graphql.resolve($1);`,
|
|
||||||
[query],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async parseResult<Result>(
|
|
||||||
graphqlResult: PGGraphQLResult | undefined,
|
|
||||||
objectMetadataItem: ObjectMetadataInterface,
|
|
||||||
command: string,
|
|
||||||
workspaceId: string,
|
|
||||||
isMultiQuery = false,
|
|
||||||
): Promise<Result> {
|
|
||||||
const entityKey = `${command}${computeObjectTargetTable(
|
|
||||||
objectMetadataItem,
|
|
||||||
)}Collection`;
|
|
||||||
const result = !isMultiQuery
|
|
||||||
? graphqlResult?.[0]?.resolve?.data?.[entityKey]
|
|
||||||
: Object.keys(graphqlResult?.[0]?.resolve?.data).reduce(
|
|
||||||
(acc: IRecord[], dataItem, index) => {
|
|
||||||
acc.push(graphqlResult?.[0]?.resolve?.data[`${entityKey}${index}`]);
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const errors = graphqlResult?.[0]?.resolve?.errors;
|
|
||||||
|
|
||||||
if (
|
|
||||||
result &&
|
|
||||||
['update', 'deleteFrom'].includes(command) &&
|
|
||||||
!result.affectedCount
|
|
||||||
) {
|
|
||||||
throw new WorkspaceQueryRunnerException(
|
|
||||||
'No rows were affected.',
|
|
||||||
WorkspaceQueryRunnerExceptionCode.NO_ROWS_AFFECTED,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors && errors.length > 0) {
|
|
||||||
const error = computePgGraphQLError(
|
|
||||||
command,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
errors,
|
|
||||||
{
|
|
||||||
atMost: this.environmentService.get(
|
|
||||||
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
|
||||||
),
|
|
||||||
} satisfies PgGraphQLConfig,
|
|
||||||
);
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultWithGetters = await this.queryResultGettersFactory.create(
|
|
||||||
result,
|
|
||||||
objectMetadataItem,
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return parseResult(resultWithGetters);
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerWebhooks<Record>(
|
|
||||||
jobsData: Record[] | undefined,
|
|
||||||
operation: CallWebhookJobsJobOperation,
|
|
||||||
options: WorkspaceQueryRunnerOptions,
|
|
||||||
) {
|
|
||||||
if (!Array.isArray(jobsData)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
jobsData.forEach((jobData) => {
|
|
||||||
this.messageQueueService.add<CallWebhookJobsJobData>(
|
|
||||||
CallWebhookJobsJob.name,
|
|
||||||
{
|
|
||||||
record: jobData,
|
|
||||||
workspaceId: options.authContext.workspace.id,
|
|
||||||
operation,
|
|
||||||
objectMetadataItem: options.objectMetadataItem,
|
|
||||||
},
|
|
||||||
{ retryLimit: 3 },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
|
||||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
|
||||||
import { AISQLQueryResolver } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.resolver';
|
|
||||||
import { AISQLQueryService } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.service';
|
|
||||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
|
||||||
import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module';
|
|
||||||
import { LLMChatModelModule } from 'src/engine/core-modules/llm-chat-model/llm-chat-model.module';
|
|
||||||
import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module';
|
|
||||||
import { LLMTracingModule } from 'src/engine/core-modules/llm-tracing/llm-tracing.module';
|
|
||||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
|
||||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
WorkspaceDataSourceModule,
|
|
||||||
WorkspaceQueryRunnerModule,
|
|
||||||
UserModule,
|
|
||||||
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
|
||||||
LLMChatModelModule,
|
|
||||||
LLMTracingModule,
|
|
||||||
EnvironmentModule,
|
|
||||||
ObjectMetadataModule,
|
|
||||||
WorkspaceSyncMetadataModule,
|
|
||||||
],
|
|
||||||
exports: [],
|
|
||||||
providers: [AISQLQueryResolver, AISQLQueryService],
|
|
||||||
})
|
|
||||||
export class AISQLQueryModule {}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { PromptTemplate } from '@langchain/core/prompts';
|
|
||||||
|
|
||||||
export const sqlGenerationPromptTemplate = PromptTemplate.fromTemplate<{
|
|
||||||
llmOutputJsonSchema: string;
|
|
||||||
sqlCreateTableStatements: string;
|
|
||||||
userQuestion: string;
|
|
||||||
}>(`Always respond following this JSON Schema: {llmOutputJsonSchema}
|
|
||||||
|
|
||||||
Based on the table schema below, write a PostgreSQL query that would answer the user's question. All column names must be enclosed in double quotes.
|
|
||||||
|
|
||||||
{sqlCreateTableStatements}
|
|
||||||
|
|
||||||
Question: {userQuestion}
|
|
||||||
SQL Query:`);
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { ForbiddenException, UseGuards } from '@nestjs/common';
|
|
||||||
import { Args, ArgsType, Field, Query, Resolver } from '@nestjs/graphql';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
|
|
||||||
import { AISQLQueryService } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.service';
|
|
||||||
import { AISQLQueryResult } from 'src/engine/core-modules/ai-sql-query/dtos/ai-sql-query-result.dto';
|
|
||||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
|
||||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
|
||||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
|
||||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
|
||||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
|
||||||
|
|
||||||
@ArgsType()
|
|
||||||
class GetAISQLQueryArgs {
|
|
||||||
@Field(() => String)
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
|
||||||
@Resolver(() => AISQLQueryResult)
|
|
||||||
export class AISQLQueryResolver {
|
|
||||||
constructor(
|
|
||||||
private readonly aiSqlQueryService: AISQLQueryService,
|
|
||||||
@InjectRepository(FeatureFlagEntity, 'core')
|
|
||||||
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Query(() => AISQLQueryResult)
|
|
||||||
async getAISQLQuery(
|
|
||||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
|
||||||
@AuthUser() user: User,
|
|
||||||
@Args() { text }: GetAISQLQueryArgs,
|
|
||||||
) {
|
|
||||||
const isCopilotEnabledFeatureFlag =
|
|
||||||
await this.featureFlagRepository.findOneBy({
|
|
||||||
workspaceId,
|
|
||||||
key: FeatureFlagKey.IsCopilotEnabled,
|
|
||||||
value: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isCopilotEnabledFeatureFlag?.value) {
|
|
||||||
throw new ForbiddenException(
|
|
||||||
`${FeatureFlagKey.IsCopilotEnabled} feature flag is disabled`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const traceMetadata = {
|
|
||||||
userId: user.id,
|
|
||||||
userEmail: user.email,
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.aiSqlQueryService.generateAndExecute(
|
|
||||||
workspaceId,
|
|
||||||
text,
|
|
||||||
traceMetadata,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { StructuredOutputParser } from '@langchain/core/output_parsers';
|
|
||||||
import { RunnableSequence } from '@langchain/core/runnables';
|
|
||||||
import groupBy from 'lodash.groupby';
|
|
||||||
import { DataSource, QueryFailedError } from 'typeorm';
|
|
||||||
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
||||||
|
|
||||||
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
|
|
||||||
import { sqlGenerationPromptTemplate } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.prompt-templates';
|
|
||||||
import { AISQLQueryResult } from 'src/engine/core-modules/ai-sql-query/dtos/ai-sql-query-result.dto';
|
|
||||||
import { LLMChatModelService } from 'src/engine/core-modules/llm-chat-model/llm-chat-model.service';
|
|
||||||
import { LLMTracingService } from 'src/engine/core-modules/llm-tracing/llm-tracing.service';
|
|
||||||
import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants';
|
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
|
||||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
|
||||||
import { StandardObjectFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-object.factory';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AISQLQueryService {
|
|
||||||
private readonly logger = new Logger(AISQLQueryService.name);
|
|
||||||
constructor(
|
|
||||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
|
||||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
|
||||||
private readonly llmChatModelService: LLMChatModelService,
|
|
||||||
private readonly llmTracingService: LLMTracingService,
|
|
||||||
private readonly standardObjectFactory: StandardObjectFactory,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
private getLabelIdentifierName(
|
|
||||||
objectMetadata: ObjectMetadataEntity,
|
|
||||||
_dataSourceId,
|
|
||||||
_workspaceId,
|
|
||||||
_workspaceFeatureFlagsMap,
|
|
||||||
): string | undefined {
|
|
||||||
const customObjectLabelIdentifierFieldMetadata = objectMetadata.fields.find(
|
|
||||||
(fieldMetadata) =>
|
|
||||||
fieldMetadata.id === objectMetadata.labelIdentifierFieldMetadataId,
|
|
||||||
);
|
|
||||||
|
|
||||||
/* const standardObjectMetadataCollection = this.standardObjectFactory.create(
|
|
||||||
standardObjectMetadataDefinitions,
|
|
||||||
{ workspaceId, dataSourceId },
|
|
||||||
workspaceFeatureFlagsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
const standardObjectLabelIdentifierFieldMetadata =
|
|
||||||
standardObjectMetadataCollection
|
|
||||||
.find(
|
|
||||||
(standardObjectMetadata) =>
|
|
||||||
standardObjectMetadata.nameSingular === objectMetadata.nameSingular,
|
|
||||||
)
|
|
||||||
?.fields.find(
|
|
||||||
(field: PartialFieldMetadata) =>
|
|
||||||
field.name === DEFAULT_LABEL_IDENTIFIER_FIELD_NAME,
|
|
||||||
) as PartialFieldMetadata; */
|
|
||||||
|
|
||||||
const labelIdentifierFieldMetadata =
|
|
||||||
customObjectLabelIdentifierFieldMetadata; /*??
|
|
||||||
standardObjectLabelIdentifierFieldMetadata*/
|
|
||||||
|
|
||||||
return (
|
|
||||||
labelIdentifierFieldMetadata?.name ?? DEFAULT_LABEL_IDENTIFIER_FIELD_NAME
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getColInfosByTableName(dataSource: DataSource) {
|
|
||||||
const { schema } = dataSource.options as PostgresConnectionOptions;
|
|
||||||
|
|
||||||
// From LangChain sql_utils.ts
|
|
||||||
const sqlQuery = `SELECT
|
|
||||||
t.table_name,
|
|
||||||
c.*
|
|
||||||
FROM
|
|
||||||
information_schema.tables t
|
|
||||||
JOIN information_schema.columns c
|
|
||||||
ON t.table_name = c.table_name
|
|
||||||
WHERE
|
|
||||||
t.table_schema = '${schema}'
|
|
||||||
AND c.table_schema = '${schema}'
|
|
||||||
ORDER BY
|
|
||||||
t.table_name,
|
|
||||||
c.ordinal_position;`;
|
|
||||||
const colInfos = await dataSource.query<
|
|
||||||
{
|
|
||||||
table_name: string;
|
|
||||||
column_name: string;
|
|
||||||
data_type: string | undefined;
|
|
||||||
is_nullable: 'YES' | 'NO';
|
|
||||||
}[]
|
|
||||||
>(sqlQuery);
|
|
||||||
|
|
||||||
return groupBy(colInfos, (colInfo) => colInfo.table_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCreateTableStatement(tableName: string, colInfos: any[]) {
|
|
||||||
return `CREATE TABLE ${tableName} (\n ${colInfos
|
|
||||||
.map(
|
|
||||||
(colInfo) =>
|
|
||||||
`${colInfo.column_name} ${colInfo.data_type} ${
|
|
||||||
colInfo.is_nullable === 'YES' ? '' : 'NOT NULL'
|
|
||||||
}`,
|
|
||||||
)
|
|
||||||
.join(', ')});`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getRelationDescriptions() {
|
|
||||||
// TODO - Construct sentences like the following:
|
|
||||||
// investorId: a foreign key referencing the person table, indicating the investor who owns this portfolio company.
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private getTableDescription(tableName: string, colInfos: any[]) {
|
|
||||||
return [
|
|
||||||
this.getCreateTableStatement(tableName, colInfos),
|
|
||||||
this.getRelationDescriptions(),
|
|
||||||
].join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getWorkspaceSchemaDescription(
|
|
||||||
dataSource: DataSource,
|
|
||||||
): Promise<string> {
|
|
||||||
const colInfoByTableName = await this.getColInfosByTableName(dataSource);
|
|
||||||
|
|
||||||
return Object.entries(colInfoByTableName)
|
|
||||||
.map(([tableName, colInfos]) =>
|
|
||||||
this.getTableDescription(tableName, colInfos),
|
|
||||||
)
|
|
||||||
.join('\n\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
private async generateWithDataSource(
|
|
||||||
workspaceId: string,
|
|
||||||
workspaceDataSource: DataSource,
|
|
||||||
userQuestion: string,
|
|
||||||
traceMetadata: Record<string, string> = {},
|
|
||||||
) {
|
|
||||||
const workspaceSchemaName =
|
|
||||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
|
||||||
|
|
||||||
workspaceDataSource.setOptions({
|
|
||||||
schema: workspaceSchemaName,
|
|
||||||
});
|
|
||||||
|
|
||||||
const workspaceSchemaDescription =
|
|
||||||
await this.getWorkspaceSchemaDescription(workspaceDataSource);
|
|
||||||
|
|
||||||
const llmOutputSchema = z.object({
|
|
||||||
sqlQuery: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const llmOutputJsonSchema = JSON.stringify(
|
|
||||||
zodToJsonSchema(llmOutputSchema),
|
|
||||||
);
|
|
||||||
|
|
||||||
const structuredOutputParser =
|
|
||||||
StructuredOutputParser.fromZodSchema(llmOutputSchema);
|
|
||||||
|
|
||||||
const sqlQueryGeneratorChain = RunnableSequence.from([
|
|
||||||
sqlGenerationPromptTemplate,
|
|
||||||
this.llmChatModelService.getJSONChatModel(),
|
|
||||||
structuredOutputParser,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const metadata = {
|
|
||||||
workspaceId,
|
|
||||||
...traceMetadata,
|
|
||||||
};
|
|
||||||
const tracingCallbackHandler =
|
|
||||||
this.llmTracingService.getCallbackHandler(metadata);
|
|
||||||
|
|
||||||
const { sqlQuery } = await sqlQueryGeneratorChain.invoke(
|
|
||||||
{
|
|
||||||
llmOutputJsonSchema,
|
|
||||||
sqlCreateTableStatements: workspaceSchemaDescription,
|
|
||||||
userQuestion,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
callbacks: [tracingCallbackHandler],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return sqlQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
async generate(
|
|
||||||
workspaceId: string,
|
|
||||||
userQuestion: string,
|
|
||||||
traceMetadata: Record<string, string> = {},
|
|
||||||
) {
|
|
||||||
const workspaceDataSource =
|
|
||||||
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.generateWithDataSource(
|
|
||||||
workspaceId,
|
|
||||||
workspaceDataSource,
|
|
||||||
userQuestion,
|
|
||||||
traceMetadata,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateAndExecute(
|
|
||||||
workspaceId: string,
|
|
||||||
userQuestion: string,
|
|
||||||
traceMetadata: Record<string, string> = {},
|
|
||||||
): Promise<AISQLQueryResult> {
|
|
||||||
const workspaceDataSource =
|
|
||||||
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const sqlQuery = await this.generateWithDataSource(
|
|
||||||
workspaceId,
|
|
||||||
workspaceDataSource,
|
|
||||||
userQuestion,
|
|
||||||
traceMetadata,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sqlQueryResult: Record<string, any>[] =
|
|
||||||
await this.workspaceQueryRunnerService.executeSQL(
|
|
||||||
workspaceDataSource,
|
|
||||||
workspaceId,
|
|
||||||
sqlQuery,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
sqlQuery,
|
|
||||||
sqlQueryResult: JSON.stringify(sqlQueryResult),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof QueryFailedError) {
|
|
||||||
return {
|
|
||||||
sqlQuery,
|
|
||||||
queryFailedErrorMessage: error.message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.error(error.message, error.stack);
|
|
||||||
|
|
||||||
return {
|
|
||||||
sqlQuery,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Field, ObjectType } from '@nestjs/graphql';
|
|
||||||
|
|
||||||
import { IsOptional } from 'class-validator';
|
|
||||||
|
|
||||||
@ObjectType('AISQLQueryResult')
|
|
||||||
export class AISQLQueryResult {
|
|
||||||
@Field(() => String)
|
|
||||||
sqlQuery: string;
|
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
|
||||||
@IsOptional()
|
|
||||||
sqlQueryResult?: string;
|
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
|
||||||
@IsOptional()
|
|
||||||
queryFailedErrorMessage?: string;
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ import { HttpAdapterHost } from '@nestjs/core';
|
|||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
|
import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
|
||||||
import { AISQLQueryModule } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.module';
|
|
||||||
import { AppTokenModule } from 'src/engine/core-modules/app-token/app-token.module';
|
import { AppTokenModule } from 'src/engine/core-modules/app-token/app-token.module';
|
||||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||||
@@ -62,7 +61,6 @@ import { FileModule } from './file/file.module';
|
|||||||
UserModule,
|
UserModule,
|
||||||
WorkspaceModule,
|
WorkspaceModule,
|
||||||
WorkspaceInvitationModule,
|
WorkspaceInvitationModule,
|
||||||
AISQLQueryModule,
|
|
||||||
PostgresCredentialsModule,
|
PostgresCredentialsModule,
|
||||||
WorkflowTriggerApiModule,
|
WorkflowTriggerApiModule,
|
||||||
WorkspaceEventEmitterModule,
|
WorkspaceEventEmitterModule,
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { DuplicateService } from 'src/engine/core-modules/duplicate/duplicate.service';
|
|
||||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [WorkspaceDataSourceModule],
|
|
||||||
exports: [DuplicateService],
|
|
||||||
providers: [DuplicateService],
|
|
||||||
})
|
|
||||||
export class DuplicateModule {}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Record as IRecord,
|
|
||||||
Record,
|
|
||||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
|
||||||
|
|
||||||
import { settings } from 'src/engine/constants/settings';
|
|
||||||
import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants';
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
||||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DuplicateService {
|
|
||||||
constructor(
|
|
||||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async findExistingRecords(
|
|
||||||
recordIds: (string | number)[],
|
|
||||||
objectMetadata: ObjectMetadataInterface,
|
|
||||||
workspaceId: string,
|
|
||||||
) {
|
|
||||||
const dataSourceSchema =
|
|
||||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
|
||||||
|
|
||||||
const results = await this.workspaceDataSourceService.executeRawQuery(
|
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
*
|
|
||||||
FROM
|
|
||||||
${dataSourceSchema}."${computeObjectTargetTable(
|
|
||||||
objectMetadata,
|
|
||||||
)}" p
|
|
||||||
WHERE
|
|
||||||
p."id" IN (${recordIds
|
|
||||||
.map((_, index) => `$${index + 1}`)
|
|
||||||
.join(', ')})
|
|
||||||
`,
|
|
||||||
recordIds,
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return results as IRecord[];
|
|
||||||
}
|
|
||||||
|
|
||||||
buildDuplicateConditionForGraphQL(
|
|
||||||
objectMetadata: ObjectMetadataInterface,
|
|
||||||
argsData?: Partial<Record>,
|
|
||||||
filteringByExistingRecordId?: string,
|
|
||||||
) {
|
|
||||||
if (!argsData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const criteriaCollection =
|
|
||||||
this.getApplicableDuplicateCriteriaCollection(objectMetadata);
|
|
||||||
|
|
||||||
const criteriaWithMatchingArgs = criteriaCollection.filter((criteria) =>
|
|
||||||
criteria.columnNames.every((columnName) => {
|
|
||||||
const value = argsData[columnName] as string | undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
!!value && value.length >= settings.minLengthOfStringForDuplicateCheck
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const filterCriteria = criteriaWithMatchingArgs.map((criteria) =>
|
|
||||||
Object.fromEntries(
|
|
||||||
criteria.columnNames.map((columnName) => [
|
|
||||||
columnName,
|
|
||||||
{ eq: argsData[columnName] },
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
// when filtering by an existing record, we need to filter that explicit record out
|
|
||||||
...(filteringByExistingRecordId && {
|
|
||||||
id: { neq: filteringByExistingRecordId },
|
|
||||||
}),
|
|
||||||
// keep condition as "or" to get results by more duplicate criteria
|
|
||||||
or: filterCriteria,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private getApplicableDuplicateCriteriaCollection(
|
|
||||||
objectMetadataItem: ObjectMetadataInterface,
|
|
||||||
) {
|
|
||||||
return DUPLICATE_CRITERIA_COLLECTION.filter(
|
|
||||||
(duplicateCriteria) =>
|
|
||||||
duplicateCriteria.objectName === objectMetadataItem.nameSingular,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -39,8 +39,6 @@ export class WorkspaceManagerService {
|
|||||||
schemaName,
|
schemaName,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.setWorkspaceMaxRow(workspaceId, schemaName);
|
|
||||||
|
|
||||||
await this.workspaceSyncMetadataService.synchronize({
|
await this.workspaceSyncMetadataService.synchronize({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
dataSourceId: dataSourceMetadata.id,
|
dataSourceId: dataSourceMetadata.id,
|
||||||
@@ -69,8 +67,6 @@ export class WorkspaceManagerService {
|
|||||||
schemaName,
|
schemaName,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.setWorkspaceMaxRow(workspaceId, schemaName);
|
|
||||||
|
|
||||||
await this.workspaceSyncMetadataService.synchronize({
|
await this.workspaceSyncMetadataService.synchronize({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
dataSourceId: dataSourceMetadata.id,
|
dataSourceId: dataSourceMetadata.id,
|
||||||
@@ -79,24 +75,6 @@ export class WorkspaceManagerService {
|
|||||||
await this.prefillWorkspaceWithDemoObjects(dataSourceMetadata, workspaceId);
|
await this.prefillWorkspaceWithDemoObjects(dataSourceMetadata, workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* We are updating the pg_graphql max_rows from 30 (default value) to 60
|
|
||||||
*
|
|
||||||
* @params workspaceId, schemaName
|
|
||||||
* @param workspaceId
|
|
||||||
*/
|
|
||||||
private async setWorkspaceMaxRow(workspaceId, schemaName) {
|
|
||||||
const workspaceDataSource =
|
|
||||||
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
await workspaceDataSource.query(
|
|
||||||
`comment on schema ${schemaName} is e'@graphql({"max_rows": 60})'`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* We are prefilling a few standard objects with data to make it easier for the user to get started.
|
* We are prefilling a few standard objects with data to make it easier for the user to get started.
|
||||||
|
|||||||
@@ -271,11 +271,6 @@ export class WorkspaceMigrationRunnerService {
|
|||||||
columns,
|
columns,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable totalCount for the table
|
|
||||||
await queryRunner.query(`
|
|
||||||
COMMENT ON TABLE "${schemaName}"."${tableName}" IS '@graphql({"totalCount": {"enabled": true}})';
|
|
||||||
`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user