Files v2 - FILES field update/create in common API (#17400)

## FILES Field update interface

```
mutation {
  updateCompany(
    id: "company-uuid"
    data: {
      filesfield: [
          { fileId: "new-file-uuid", label: "new-document.pdf" }
        ]
    }
  ) {
    id
    filesfield {
      fileId
      label
      extension
    }
  }
}
```
This commit is contained in:
Etienne
2026-01-29 14:33:26 +01:00
committed by GitHub
parent 3dfb5ffc02
commit 09de762285
43 changed files with 1929 additions and 236 deletions
@@ -22,7 +22,8 @@ import {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { RelationNestedQueries } from 'src/engine/twenty-orm/relation-nested-queries/relation-nested-queries';
import { FilesFieldSync } from 'src/engine/twenty-orm/field-operations/files-field-sync/files-field-sync';
import { RelationNestedQueries } from 'src/engine/twenty-orm/field-operations/relation-nested-queries/relation-nested-queries';
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils';
import { type WorkspaceDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-delete-query-builder';
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
@@ -46,6 +47,7 @@ export class WorkspaceInsertQueryBuilder<
private relationNestedConfig:
| [RelationConnectQueryConfig[], RelationDisconnectQueryFieldsByEntityIndex]
| null;
private filesFieldSync: FilesFieldSync;
constructor(
queryBuilder: InsertQueryBuilder<T>,
@@ -64,6 +66,7 @@ export class WorkspaceInsertQueryBuilder<
this.relationNestedQueries = new RelationNestedQueries(
this.internalContext,
);
this.filesFieldSync = new FilesFieldSync(this.internalContext);
}
override clone(): this {
@@ -158,6 +161,38 @@ export class WorkspaceInsertQueryBuilder<
this.internalContext,
);
let filesFieldFileIds = null;
let fileIdToApplicationId = new Map<string, string>();
const entities = Array.isArray(this.expressionMap.valuesSet)
? this.expressionMap.valuesSet
: [this.expressionMap.valuesSet];
const filesFieldDiffByEntityIndex =
this.filesFieldSync.computeFilesFieldDiffBeforeInsert(
entities as QueryDeepPartialEntityWithNestedRelationFields<T>[],
mainAliasTarget,
);
if (isDefined(filesFieldDiffByEntityIndex)) {
const result = await this.filesFieldSync.enrichFilesFields({
entities:
entities as QueryDeepPartialEntityWithNestedRelationFields<T>[],
filesFieldDiffByEntityIndex,
workspaceId: this.internalContext.workspaceId,
target: mainAliasTarget,
});
filesFieldFileIds = result.fileIds;
fileIdToApplicationId = result.fileIdToApplicationId;
this.expressionMap.valuesSet = Array.isArray(
this.expressionMap.valuesSet,
)
? result.entities
: result.entities[0];
}
if (isDefined(this.relationNestedConfig)) {
const nestedRelationQueryBuilder = new WorkspaceSelectQueryBuilder(
this as unknown as WorkspaceSelectQueryBuilder<T>,
@@ -183,6 +218,13 @@ export class WorkspaceInsertQueryBuilder<
this.validateRLSPredicatesForInsert();
const result = await super.execute();
if (isDefined(filesFieldFileIds)) {
await this.filesFieldSync.updateFileEntityRecords(
filesFieldFileIds,
fileIdToApplicationId,
);
}
const eventSelectQueryBuilder = (
this.connection.manager as WorkspaceEntityManager
).createQueryBuilder(
@@ -26,7 +26,8 @@ import {
TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { RelationNestedQueries } from 'src/engine/twenty-orm/relation-nested-queries/relation-nested-queries';
import { FilesFieldSync } from 'src/engine/twenty-orm/field-operations/files-field-sync/files-field-sync';
import { RelationNestedQueries } from 'src/engine/twenty-orm/field-operations/relation-nested-queries/relation-nested-queries';
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils';
import { type WorkspaceDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-delete-query-builder';
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
@@ -57,6 +58,7 @@ export class WorkspaceUpdateQueryBuilder<
criteria: string;
partialEntity: QueryDeepPartialEntity<T>;
}[];
private filesFieldSync: FilesFieldSync;
constructor(
queryBuilder: UpdateQueryBuilder<T>,
@@ -75,6 +77,7 @@ export class WorkspaceUpdateQueryBuilder<
this.relationNestedQueries = new RelationNestedQueries(
this.internalContext,
);
this.filesFieldSync = new FilesFieldSync(this.internalContext);
}
override clone(): this {
@@ -154,6 +157,44 @@ export class WorkspaceUpdateQueryBuilder<
this.featureFlagMap,
);
const formattedBefore = formatResult<T[]>(
before,
objectMetadata,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
let filesFieldDiffByEntityIndex = null;
let filesFieldFileIds = null;
let fileIdToApplicationId = new Map<string, string>();
const updatePayload = Array.isArray(this.expressionMap.valuesSet)
? (this.expressionMap.valuesSet[0] ?? {})
: (this.expressionMap.valuesSet ?? {});
filesFieldDiffByEntityIndex =
this.filesFieldSync.computeFilesFieldDiffBeforeUpdateOne(
updatePayload,
mainAliasTarget,
formattedBefore,
);
if (isDefined(filesFieldDiffByEntityIndex)) {
const entities = formattedBefore.map(() => updatePayload);
const result = await this.filesFieldSync.enrichFilesFields({
entities,
filesFieldDiffByEntityIndex,
workspaceId: this.internalContext.workspaceId,
target: mainAliasTarget,
});
filesFieldFileIds = result.fileIds;
fileIdToApplicationId = result.fileIdToApplicationId;
this.expressionMap.valuesSet = result.entities[0];
}
if (isDefined(this.relationNestedConfig)) {
const updatedValues =
await this.relationNestedQueries.processRelationNestedQueries({
@@ -168,13 +209,6 @@ export class WorkspaceUpdateQueryBuilder<
updatedValues.length === 1 ? updatedValues[0] : updatedValues;
}
const formattedBefore = formatResult<T[]>(
before,
objectMetadata,
this.internalContext.flatObjectMetadataMaps,
this.internalContext.flatFieldMetadataMaps,
);
this.applyRowLevelPermissionPredicates();
const valuesSet = this.expressionMap.valuesSet ?? {};
@@ -194,6 +228,13 @@ export class WorkspaceUpdateQueryBuilder<
const result = await super.execute();
if (isDefined(filesFieldFileIds)) {
await this.filesFieldSync.updateFileEntityRecords(
filesFieldFileIds,
fileIdToApplicationId,
);
}
const after = await eventSelectQueryBuilder.getMany();
const formattedAfter = formatResult<T[]>(
@@ -324,6 +365,36 @@ export class WorkspaceUpdateQueryBuilder<
mainAliasTarget,
);
let filesFieldDiffByEntityIndex = null;
let filesFieldFileIds = null;
let fileIdToApplicationId = null;
const entities = this.manyInputs.map((input) => input.partialEntity);
filesFieldDiffByEntityIndex =
this.filesFieldSync.computeFilesFieldDiffBeforeUpdate(
entities,
mainAliasTarget,
formattedBefore,
);
if (isDefined(filesFieldDiffByEntityIndex)) {
const result = await this.filesFieldSync.enrichFilesFields({
entities,
filesFieldDiffByEntityIndex,
workspaceId: this.internalContext.workspaceId,
target: mainAliasTarget,
});
filesFieldFileIds = result.fileIds;
fileIdToApplicationId = result.fileIdToApplicationId;
this.manyInputs = result.entities.map((updatedEntity, index) => ({
criteria: this.manyInputs[index].criteria,
partialEntity: updatedEntity,
}));
}
if (isDefined(this.relationNestedConfig)) {
const updatedValues =
await this.relationNestedQueries.processRelationNestedQueries({
@@ -373,6 +444,13 @@ export class WorkspaceUpdateQueryBuilder<
results.push(result);
}
if (isDefined(filesFieldFileIds) && isDefined(fileIdToApplicationId)) {
await this.filesFieldSync.updateFileEntityRecords(
filesFieldFileIds,
fileIdToApplicationId,
);
}
const afterRecords = await eventSelectQueryBuilder.getMany();
const formattedAfter = formatResult<T[]>(