Fix message channel processing (#12021)

Several users have complained about not being able to read their emails
anymore.

This is because the find-messages post query hook is expecting
ObjectRecord[] as an input but is actually getting a graphql Connection

Typing was wrong. This PR fixes the typing and make sure the post query
hook always get an ObjectRecord[]
This commit is contained in:
Charles Bochet
2025-05-13 21:16:23 +02:00
committed by GitHub
parent c0a0214879
commit 0202586d36
48 changed files with 217 additions and 129 deletions

View File

@ -24,4 +24,5 @@ export enum GraphqlQueryRunnerExceptionCode {
RELATION_TARGET_OBJECT_METADATA_NOT_FOUND = 'RELATION_TARGET_OBJECT_METADATA_NOT_FOUND',
NOT_IMPLEMENTED = 'NOT_IMPLEMENTED',
OBJECT_METADATA_COLLECTION_NOT_FOUND = 'OBJECT_METADATA_COLLECTION_NOT_FOUND',
INVALID_POST_HOOK_PAYLOAD = 'INVALID_POST_HOOK_PAYLOAD',
}

View File

@ -171,20 +171,14 @@ export abstract class GraphqlQueryBaseResolverService<
options.objectMetadataMaps,
);
const resultWithGettersArray = Array.isArray(resultWithGetters)
? resultWithGetters
: [resultWithGetters];
await this.workspaceQueryHookService.executePostQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
operationName,
resultWithGettersArray,
resultWithGetters,
);
return Array.isArray(resultWithGetters)
? resultWithGettersArray
: resultWithGettersArray[0];
return resultWithGetters;
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error, options);
}

View File

@ -1,8 +1,9 @@
import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
import { ResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
export interface WorkspaceQueryHookInstance {
export interface WorkspacePreQueryHookInstance {
execute(
authContext: AuthContext,
objectName: string,
@ -10,10 +11,10 @@ export interface WorkspaceQueryHookInstance {
): Promise<ResolverArgs>;
}
export interface WorkspaceQueryPostHookInstance {
export interface WorkspacePostQueryHookInstance {
execute(
authContext: AuthContext,
objectName: string,
payload: unknown[],
payload: QueryResultFieldValue,
): Promise<void>;
}

View File

@ -4,7 +4,10 @@ import { Module } from '@nestjs/core/injector/module';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import {
WorkspacePostQueryHookInstance,
WorkspacePreQueryHookInstance,
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
@ -19,16 +22,16 @@ interface WorkspaceQueryHookData<T> {
export class WorkspaceQueryHookStorage {
private preHookInstances = new Map<
WorkspaceQueryHookKey,
WorkspaceQueryHookData<WorkspaceQueryHookInstance>[]
WorkspaceQueryHookData<WorkspacePreQueryHookInstance>[]
>();
private postHookInstances = new Map<
WorkspaceQueryHookKey,
WorkspaceQueryHookData<WorkspaceQueryHookInstance>[]
WorkspaceQueryHookData<WorkspacePostQueryHookInstance>[]
>();
registerWorkspaceQueryPreHookInstance(
key: WorkspaceQueryHookKey,
data: WorkspaceQueryHookData<WorkspaceQueryHookInstance>,
data: WorkspaceQueryHookData<WorkspacePreQueryHookInstance>,
) {
if (!this.preHookInstances.has(key)) {
this.preHookInstances.set(key, []);
@ -39,11 +42,11 @@ export class WorkspaceQueryHookStorage {
getWorkspaceQueryPreHookInstances(
key: WorkspaceQueryHookKey,
): WorkspaceQueryHookData<WorkspaceQueryHookInstance>[] {
): WorkspaceQueryHookData<WorkspacePreQueryHookInstance>[] {
const methodName = key.split('.')?.[1] as
| WorkspaceResolverBuilderMethodNames
| undefined;
let wildcardInstances: WorkspaceQueryHookData<WorkspaceQueryHookInstance>[] =
let wildcardInstances: WorkspaceQueryHookData<WorkspacePreQueryHookInstance>[] =
[];
if (!methodName) {
@ -62,9 +65,9 @@ export class WorkspaceQueryHookStorage {
return [...wildcardInstances, ...(this.preHookInstances.get(key) ?? [])];
}
registerWorkspaceQueryPostHookInstance(
registerWorkspacePostQueryHookInstance(
key: WorkspaceQueryHookKey,
data: WorkspaceQueryHookData<WorkspaceQueryHookInstance>,
data: WorkspaceQueryHookData<WorkspacePostQueryHookInstance>,
) {
if (!this.postHookInstances.has(key)) {
this.postHookInstances.set(key, []);
@ -73,13 +76,13 @@ export class WorkspaceQueryHookStorage {
this.postHookInstances.get(key)?.push(data);
}
getWorkspaceQueryPostHookInstances(
getWorkspacePostQueryHookInstances(
key: WorkspaceQueryHookKey,
): WorkspaceQueryHookData<WorkspaceQueryHookInstance>[] {
): WorkspaceQueryHookData<WorkspacePostQueryHookInstance>[] {
const methodName = key.split('.')?.[1] as
| WorkspaceResolverBuilderMethodNames
| undefined;
let wildcardInstances: WorkspaceQueryHookData<WorkspaceQueryHookInstance>[] =
let wildcardInstances: WorkspaceQueryHookData<WorkspacePostQueryHookInstance>[] =
[];
if (!methodName) {

View File

@ -3,8 +3,21 @@ import { DiscoveryService, ModuleRef, createContextId } from '@nestjs/core';
import { Injector } from '@nestjs/core/injector/injector';
import { Module } from '@nestjs/core/injector/module';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
import {
WorkspacePostQueryHookInstance,
WorkspacePreQueryHookInstance,
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { isQueryResultFieldValueAConnection } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-connection.guard';
import { isQueryResultFieldValueANestedRecordArray } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-nested-record-array.guard';
import { isQueryResultFieldValueARecordArray } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-record-array.guard';
import { isQueryResultFieldValueARecord } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-record.guard';
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
@ -68,12 +81,12 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
}
}
async handleHook(
payload: Parameters<WorkspaceQueryHookInstance['execute']>,
async handlePreHook(
executeParams: Parameters<WorkspacePreQueryHookInstance['execute']>,
instance: object,
host: Module,
isRequestScoped: boolean,
): Promise<ReturnType<WorkspaceQueryHookInstance['execute']>> {
): Promise<ReturnType<WorkspacePreQueryHookInstance['execute']>> {
const methodName = 'execute';
if (isRequestScoped) {
@ -83,7 +96,7 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
this.moduleRef.registerRequestByContextId(
{
req: {
workspaceId: payload?.[0].workspace.id,
workspaceId: executeParams?.[0].workspace.id,
},
},
contextId,
@ -97,9 +110,82 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
contextId,
);
return contextInstance[methodName].call(contextInstance, ...payload);
return contextInstance[methodName].call(
contextInstance,
...executeParams,
);
} else {
return instance[methodName].call(instance, ...payload);
return instance[methodName].call(instance, ...executeParams);
}
}
private transformPayload(payload: QueryResultFieldValue): ObjectRecord[] {
if (isQueryResultFieldValueAConnection(payload)) {
return payload.edges.map((edge) => edge.node);
}
if (isQueryResultFieldValueANestedRecordArray(payload)) {
return payload.records;
}
if (isQueryResultFieldValueARecordArray(payload)) {
return payload;
}
if (isQueryResultFieldValueARecord(payload)) {
return [payload];
}
throw new GraphqlQueryRunnerException(
`Unsupported payload type: ${payload}`,
GraphqlQueryRunnerExceptionCode.INVALID_POST_HOOK_PAYLOAD,
);
}
async handlePostHook(
executeParams: Parameters<WorkspacePostQueryHookInstance['execute']>,
instance: object,
host: Module,
isRequestScoped: boolean,
): Promise<ReturnType<WorkspacePostQueryHookInstance['execute']>> {
const methodName = 'execute';
const transformedPayload = this.transformPayload(executeParams[2]);
if (isRequestScoped) {
const contextId = createContextId();
if (this.moduleRef.registerRequestByContextId) {
this.moduleRef.registerRequestByContextId(
{
req: {
workspaceId: executeParams?.[0].workspace.id,
},
},
contextId,
);
}
const contextInstance = await this.injector.loadPerContext(
instance,
host,
host.providers,
contextId,
);
return contextInstance[methodName].call(
contextInstance,
executeParams[0],
executeParams[1],
transformedPayload,
);
} else {
return instance[methodName].call(
instance,
executeParams[0],
executeParams[1],
transformedPayload,
);
}
}
@ -115,17 +201,17 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
this.workspaceQueryHookStorage.registerWorkspaceQueryPreHookInstance(
key,
{
instance: instance as WorkspaceQueryHookInstance,
instance: instance as WorkspacePreQueryHookInstance,
host,
isRequestScoped,
},
);
break;
case WorkspaceQueryHookType.PostHook:
this.workspaceQueryHookStorage.registerWorkspaceQueryPostHookInstance(
this.workspaceQueryHookStorage.registerWorkspacePostQueryHookInstance(
key,
{
instance: instance as WorkspaceQueryHookInstance,
instance: instance as WorkspacePostQueryHookInstance,
host,
isRequestScoped,
},

View File

@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import merge from 'lodash.merge';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
@ -37,7 +37,7 @@ export class WorkspaceQueryHookService {
for (const preHookInstance of preHookInstances) {
// Deep merge all return of handleHook into payload before returning it
const hookPayload = await this.workspaceQueryHookExplorer.handleHook(
const hookPayload = await this.workspaceQueryHookExplorer.handlePreHook(
[authContext, objectName, payload],
preHookInstance.instance,
preHookInstance.host,
@ -53,24 +53,23 @@ export class WorkspaceQueryHookService {
public async executePostQueryHooks<
T extends WorkspaceResolverBuilderMethodNames,
U extends ObjectRecord = ObjectRecord,
>(
authContext: AuthContext,
// TODO: We should allow wildcard for object name
objectName: string,
methodName: T,
payload: U[],
payload: QueryResultFieldValue,
): Promise<void> {
const key: WorkspaceQueryHookKey = `${objectName}.${methodName}`;
const postHookInstances =
this.workspaceQueryHookStorage.getWorkspaceQueryPostHookInstances(key);
this.workspaceQueryHookStorage.getWorkspacePostQueryHookInstances(key);
if (!postHookInstances) {
return;
}
for (const postHookInstance of postHookInstances) {
await this.workspaceQueryHookExplorer.handleHook(
await this.workspaceQueryHookExplorer.handlePostHook(
[authContext, objectName, payload],
postHookInstance.instance,
postHookInstance.host,