Files Field - Add new controller (#17561)

Add new files field controller to fetch files from FILES field
This commit is contained in:
Etienne
2026-01-30 14:44:11 +01:00
committed by GitHub
parent 9439afde22
commit 4b66ae5320
24 changed files with 959 additions and 141 deletions
@@ -1,4 +1,4 @@
import { Injectable, Logger } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import {
FieldMetadataType,
@@ -10,10 +10,12 @@ import { isDefined } from 'twenty-shared/utils';
import { type QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface';
import { FilesFieldQueryResultGetterHandler } from 'src/engine/api/common/common-result-getters/handlers/field-handlers/files-field-query-result-getter.handler';
import { ActivityQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler';
import { AttachmentQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler';
import { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler';
import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler';
import { FilesFieldService } from 'src/engine/core-modules/file/files-field/files-field.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
@@ -30,15 +32,22 @@ import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object
// Right now the factory will override any change made on relations by the handlers
@Injectable()
export class CommonResultGettersService {
private readonly logger = new Logger(CommonResultGettersService.name);
private handlers: Map<string, QueryResultGetterHandlerInterface>;
private objectHandlers: Map<string, QueryResultGetterHandlerInterface>;
private fieldHandlers: Map<
FieldMetadataType,
QueryResultGetterHandlerInterface
>;
constructor(private readonly fileService: FileService) {
this.initializeHandlers();
constructor(
private readonly fileService: FileService,
private readonly filesFieldService: FilesFieldService,
) {
this.initializeObjectHandlers();
this.initializeFieldHandlers();
}
private initializeHandlers() {
this.handlers = new Map<string, QueryResultGetterHandlerInterface>([
private initializeObjectHandlers() {
this.objectHandlers = new Map<string, QueryResultGetterHandlerInterface>([
['attachment', new AttachmentQueryResultGetterHandler(this.fileService)],
['person', new PersonQueryResultGetterHandler(this.fileService)],
[
@@ -50,6 +59,18 @@ export class CommonResultGettersService {
]);
}
private initializeFieldHandlers() {
this.fieldHandlers = new Map<
FieldMetadataType,
QueryResultGetterHandlerInterface
>([
[
FieldMetadataType.FILES,
new FilesFieldQueryResultGetterHandler(this.filesFieldService),
],
]);
}
public async processRecordArray(
recordArray: ObjectRecord[],
flatObjectMetadata: FlatObjectMetadata,
@@ -85,8 +106,6 @@ export class CommonResultGettersService {
workspaceId: string,
fieldMapsForObject?: FieldMapsForObject,
): Promise<ObjectRecord> {
const handler = this.getHandler(flatObjectMetadata.nameSingular);
const fieldMaps =
fieldMapsForObject ??
buildFieldMapsFromFlatObjectMetadata(
@@ -96,6 +115,18 @@ export class CommonResultGettersService {
const { fieldIdByName } = fieldMaps;
const handlers = [
this.getObjectHandler(flatObjectMetadata.nameSingular),
...Object.keys(record)
.map(
(recordFieldName) =>
flatFieldMetadataMaps.byId[fieldIdByName[recordFieldName]],
)
.filter(isDefined)
.map((fieldMetadata) => this.fieldHandlers.get(fieldMetadata.type))
.filter(isDefined),
];
const relationFields = Object.keys(record)
.map(
(recordFieldName) =>
@@ -146,10 +177,20 @@ export class CommonResultGettersService {
);
}
const objectRecordProcessedWithoutRelationFields = await handler.handle(
record,
workspaceId,
);
const fieldMetadata = Object.keys(record)
.map(
(recordFieldName) =>
flatFieldMetadataMaps.byId[fieldIdByName[recordFieldName]],
)
.filter(isDefined);
const objectRecordProcessedWithoutRelationFields =
await this.processObjectRecordWithoutRelationFields(
record,
workspaceId,
handlers,
fieldMetadata,
);
const processedRecord = {
...objectRecordProcessedWithoutRelationFields,
@@ -159,9 +200,33 @@ export class CommonResultGettersService {
return processedRecord;
}
private getHandler(objectType: string): QueryResultGetterHandlerInterface {
private async processObjectRecordWithoutRelationFields(
record: ObjectRecord,
workspaceId: string,
handlers: QueryResultGetterHandlerInterface[],
fieldMetadata: FlatFieldMetadata[],
): Promise<ObjectRecord> {
let processedRecord = record;
for (const handler of handlers) {
processedRecord = await handler.handle(
processedRecord,
workspaceId,
fieldMetadata,
);
}
return processedRecord;
}
private getObjectHandler(
objectType: string,
): QueryResultGetterHandlerInterface {
return (
this.handlers.get(objectType) || {
(this.objectHandlers.get(objectType) || {
handle: (result: ObjectRecord): Promise<ObjectRecord> =>
Promise.resolve(result),
}) ?? {
handle: (result: ObjectRecord): Promise<ObjectRecord> =>
Promise.resolve(result),
}
@@ -0,0 +1,54 @@
import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types';
import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface';
import { isFileOutputArray } from 'src/engine/api/common/common-args-processors/data-arg-processor/types/file-item.guard';
import type { SignedFileOutput } from 'src/engine/api/common/common-args-processors/data-arg-processor/types/file-item.type';
import { type FilesFieldService } from 'src/engine/core-modules/file/files-field/files-field.service';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
export class FilesFieldQueryResultGetterHandler
implements QueryResultGetterHandlerInterface
{
constructor(private readonly filesFieldService: FilesFieldService) {}
async handle(
record: ObjectRecord,
workspaceId: string,
flatFieldMetadata: FlatFieldMetadata[],
): Promise<ObjectRecord> {
const filesFields = flatFieldMetadata.filter(
(field) => field.type === FieldMetadataType.FILES,
);
if (filesFields.length === 0) {
return record;
}
for (const field of filesFields) {
const filesFieldValue = record[field.name];
if (!isFileOutputArray(filesFieldValue)) {
continue;
}
const signedFilesFieldValue: SignedFileOutput[] = [];
for (const file of filesFieldValue) {
const url = this.filesFieldService.signFileUrl({
fileId: file.fileId,
workspaceId,
});
signedFilesFieldValue.push({
...file,
url,
});
}
record[field.name] = signedFilesFieldValue;
}
return record;
}
}