[permissions] Adapt field permissions to connect + createMany (#13655)

In this PR we adapt field permission to two things

1. Connect: the recent insertion of the connect feature introduces the
possibility to have "connect" objects in typeORM's expressionMap's
valueSet, where we previously only had column names (if i followed
correctly). For instance for a person object that a N - 1 relationship
to company, person will have both companyId and company as possible
valueSet keys for the upsert. We need to reflect that in our
`getColumnNameToFieldMetadataIdMap` util that returns a map containing
every possible value we could encounter in valueSet. In an attempt to
tie this to the schema generation where this is introduced, I created
the shallow util extractGraphQLRelationFieldNames (probably ill-named -
im willing to update the name if @etiennejouan has a better idea?) to
remind us that these two are linked.

2. CreateMany: When calling query builders methods directly on custom
object (like we do in graphql-create-many-resolver), we need to be
careful to call them with a selection of readable fields. We had a
debate on whether we should compute this selection containing all
readable fields by default under the hood when no selected fields are
indicated. I am still not 100% convinced as I think it should remain the
caller's responsibility, but this case reminds us that it could easily
be forgotten by developers - although it is all the more the case as we
don't have seeds yet that help us realize that ([PR on the
way](https://github.com/twentyhq/twenty/pull/13646)).
This commit is contained in:
Marie
2025-08-06 10:31:52 +02:00
committed by GitHub
parent a804d65210
commit 9957057d1f
12 changed files with 114 additions and 60 deletions
@@ -40,7 +40,7 @@ export class WorkspaceRepository<
private readonly internalContext: WorkspaceInternalContext;
private shouldBypassPermissionChecks: boolean;
private featureFlagMap: FeatureFlagMap;
private objectRecordsPermissions?: ObjectRecordsPermissions;
public readonly objectRecordsPermissions?: ObjectRecordsPermissions;
private authContext?: AuthContext;
declare manager: WorkspaceEntityManager;
@@ -226,8 +226,9 @@ describe('getColumnNameToFieldMetadataIdMap', () => {
);
expect(result['companyId']).toBe('field-1');
expect(result['company']).toBe('field-1');
expect(result['name']).toBe('field-2');
expect(Object.keys(result)).toHaveLength(2);
expect(Object.keys(result)).toHaveLength(3);
});
it('should skip ONE_TO_MANY relation field types', () => {
@@ -1,3 +1,5 @@
import { isDefined } from 'twenty-shared/utils';
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@@ -34,12 +36,17 @@ export function getColumnNameToFieldMetadataIdMap(
},
processRelationField: ({
fieldMetadataId,
columnName,
joinColumnName,
connectFieldName,
}: {
fieldMetadataId: string;
columnName: string;
joinColumnName: string;
connectFieldName?: string;
}) => {
columnNameToFieldMetadataIdMap[columnName] = fieldMetadataId;
columnNameToFieldMetadataIdMap[joinColumnName] = fieldMetadataId;
if (isDefined(connectFieldName)) {
columnNameToFieldMetadataIdMap[connectFieldName] = fieldMetadataId;
}
},
processSimpleField: ({
fieldMetadataId,
@@ -40,13 +40,12 @@ export function getFieldMetadataIdToColumnNamesMap(
},
processRelationField: ({
fieldMetadataId,
columnName,
joinColumnName,
}: {
fieldMetadataId: string;
fieldMetadata: FieldMetadataEntity;
columnName: string;
joinColumnName: string;
}) => {
fieldMetadataToColumnNamesMap.set(fieldMetadataId, [columnName]); // TODO test
fieldMetadataToColumnNamesMap.set(fieldMetadataId, [joinColumnName]);
},
processSimpleField: ({
fieldMetadataId,
@@ -2,6 +2,7 @@ import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interf
import { FieldMetadataRelationSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { extractGraphQLRelationFieldNames } from 'src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util';
import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@@ -26,11 +27,13 @@ export type ColumnNameProcessor = {
processRelationField: ({
fieldMetadataId,
fieldMetadata,
columnName,
joinColumnName,
connectFieldName,
}: {
fieldMetadataId: string;
fieldMetadata: FieldMetadataEntity;
columnName: string;
joinColumnName: string;
connectFieldName?: string;
}) => void;
processSimpleField: ({
fieldMetadataId,
@@ -73,18 +76,15 @@ export function processFieldMetadataForColumnNameMapping(
if (fieldMetadataSettings?.relationType === RelationType.ONE_TO_MANY) {
continue;
}
const columnName = fieldMetadataSettings?.joinColumnName;
if (!columnName) {
throw new PermissionsException(
`Join column name is required for relation field metadata ${fieldMetadata.name}`,
PermissionsExceptionCode.JOIN_COLUMN_NAME_REQUIRED,
);
}
const { joinColumnName, fieldMetadataName } =
extractGraphQLRelationFieldNames(fieldMetadata);
processor.processRelationField({
fieldMetadataId,
fieldMetadata,
columnName,
joinColumnName,
connectFieldName: fieldMetadataName,
});
} else {
const columnName = computeColumnName(fieldMetadata);