diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts index 6bb5acee4f..bd992d9777 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts @@ -20,6 +20,7 @@ import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/g import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; @@ -195,7 +196,24 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol queryBuilder.orWhere(condition); }); - return await queryBuilder.withDeleted().getMany(); + const restrictedFields = + executionArgs.repository.objectRecordsPermissions?.[ + objectMetadataItemWithFieldMaps.id + ]?.restrictedFields; + + const selectOptions = getAllSelectableFields({ + restrictedFields: restrictedFields ?? {}, + objectMetadata: { + objectMetadataMapItem: objectMetadataItemWithFieldMaps, + }, + }); + + return await queryBuilder + .withDeleted() + .setFindOptions({ + select: selectOptions, + }) + .getMany(); } private getValueFromPath( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util.ts new file mode 100644 index 0000000000..59aafe54f2 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util.ts @@ -0,0 +1,19 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; + +export const extractGraphQLRelationFieldNames = ( + fieldMetadata: FieldMetadataEntity< + FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION + >, +) => { + const joinColumnName = fieldMetadata.settings?.joinColumnName; + + if (!joinColumnName) { + throw new Error('Join column name is not defined'); + } + + const fieldMetadataName = fieldMetadata.name; + + return { joinColumnName, fieldMetadataName }; +}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.util.ts index b098a64644..a36531e36d 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.util.ts @@ -13,6 +13,7 @@ import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfa import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory'; import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory'; import { formatRelationConnectInputTarget } from 'src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory'; +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 { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; @@ -172,11 +173,8 @@ const generateRelationField = < return relationField; } - const joinColumnName = fieldMetadata.settings?.joinColumnName; - - if (!joinColumnName) { - throw new Error('Join column name is not defined'); - } + const { joinColumnName, fieldMetadataName } = + extractGraphQLRelationFieldNames(fieldMetadata); const target = getTarget(fieldMetadata); const typeFactoryOptions = getTypeFactoryOptions(fieldMetadata, kind); @@ -220,7 +218,7 @@ const generateRelationField = < } // @ts-expect-error legacy noImplicitAny - relationField[fieldMetadata.name] = { + relationField[fieldMetadataName] = { type: type, description: fieldMetadata.description, }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils.ts index 3ff49ee0ef..b9aaf2acd9 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils.ts @@ -5,7 +5,11 @@ import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata- export const isFieldMetadataRelationOrMorphRelation = ( fieldMetadata: FieldMetadataEntity, -) => { +): fieldMetadata is FieldMetadataEntity & + ( + | FieldMetadataEntity + | FieldMetadataEntity + ) => { return ( isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION) || isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.MORPH_RELATION) diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts index 094c79e308..88bdcf737d 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts @@ -5,6 +5,7 @@ import { Request } from 'express'; import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler'; import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; +import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils'; @Injectable() export class RestApiDeleteOneHandler extends RestApiBaseHandler { @@ -18,7 +19,7 @@ export class RestApiDeleteOneHandler extends RestApiBaseHandler { const { objectMetadata, repository, restrictedFields } = await this.getRepositoryAndMetadataOrFail(request); - const selectOptions = this.getAllSelectableFields({ + const selectOptions = getAllSelectableFields({ restrictedFields, objectMetadata, }); diff --git a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts index 936670fe4f..077cc002fd 100644 --- a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts @@ -24,6 +24,7 @@ import { MAX_DEPTH, } from 'src/engine/api/rest/input-factories/depth-input.factory'; import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils'; +import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils'; import { CreatedByFromAuthContextService } from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service'; import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; @@ -44,7 +45,6 @@ import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/wo import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { formatResult as formatGetManyData } from 'src/engine/twenty-orm/utils/format-result.util'; -import { getFieldMetadataIdToColumnNamesMap } from 'src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; export interface PageInfo { @@ -310,7 +310,7 @@ export abstract class RestApiBaseHandler { let selectOptions = undefined; if (!isEmpty(restrictedFields)) { - selectOptions = this.getAllSelectableFields({ + selectOptions = getAllSelectableFields({ restrictedFields, objectMetadata, }); @@ -355,36 +355,6 @@ export abstract class RestApiBaseHandler { }; } - public getAllSelectableFields({ - restrictedFields, - objectMetadata, - }: { - restrictedFields: RestrictedFields; - objectMetadata: { objectMetadataMapItem: ObjectMetadataItemWithFieldMaps }; - }) { - const restrictedFieldsIds = Object.entries(restrictedFields) - .filter(([_, value]) => value.canRead === false) - .map(([key]) => key); - - const fieldMetadataIdToColumnNamesMap = getFieldMetadataIdToColumnNamesMap( - objectMetadata.objectMetadataMapItem, - ); - - const restrictedFieldsColumnNames: string[] = restrictedFieldsIds - .map((fieldId) => fieldMetadataIdToColumnNamesMap.get(fieldId)) - .filter(isDefined) - .flat(); - - const allColumnNames = [...fieldMetadataIdToColumnNamesMap.values()].flat(); - - return Object.fromEntries( - allColumnNames.map((columnName) => [ - columnName, - !restrictedFieldsColumnNames.includes(columnName), - ]), - ); - } - public formatResult({ operation, objectNameSingular, diff --git a/packages/twenty-server/src/engine/api/utils/get-all-selectable-fields.utils.ts b/packages/twenty-server/src/engine/api/utils/get-all-selectable-fields.utils.ts new file mode 100644 index 0000000000..00cd564b1b --- /dev/null +++ b/packages/twenty-server/src/engine/api/utils/get-all-selectable-fields.utils.ts @@ -0,0 +1,37 @@ +import { RestrictedFields } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { getFieldMetadataIdToColumnNamesMap } from 'src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util'; + +export const getAllSelectableFields = ({ + restrictedFields, + objectMetadata, +}: { + restrictedFields: RestrictedFields; + objectMetadata: { objectMetadataMapItem: ObjectMetadataItemWithFieldMaps }; +}) => { + const restrictedFieldsIds = Object.entries(restrictedFields) + .filter(([_, value]) => value.canRead === false) + .map(([key]) => key); + + const fieldMetadataIdToColumnNamesMap = getFieldMetadataIdToColumnNamesMap( + objectMetadata.objectMetadataMapItem, + ); + + const restrictedFieldsColumnNames: string[] = restrictedFieldsIds + .map((fieldId) => fieldMetadataIdToColumnNamesMap.get(fieldId)) + .filter(isDefined) + .flat(); + + const allColumnNames = [...fieldMetadataIdToColumnNamesMap.values()].flat(); + + const restrictedFieldsColumnNamesSet = new Set(restrictedFieldsColumnNames); + + return Object.fromEntries( + allColumnNames.map((columnName) => [ + columnName, + !restrictedFieldsColumnNamesSet.has(columnName), + ]), + ); +}; diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts index a3b429ea16..dac524c004 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts @@ -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; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-column-name-to-field-metadata-id.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-column-name-to-field-metadata-id.util.spec.ts index 11ccf3cb79..f5df6cec13 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-column-name-to-field-metadata-id.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-column-name-to-field-metadata-id.util.spec.ts @@ -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', () => { diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util.ts index de623cf9a2..571d09a5b6 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util.ts @@ -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, diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util.ts index 2e34c44a94..3ffc1b98f1 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util.ts @@ -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, diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util.ts index ecddb75ea4..937a8bda29 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util.ts @@ -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);