Fix record creation (#8664)

## Context
Now that each operation has its own resolver, we need to make sure they
all map to query arg getters. CreateOne was not properly mapped to the
position getter which made record creation fail because "position:
first" was not properly converted to a float.
Also fixing queries with custom object where we were wrongly using the
table name instead of entity name
This commit is contained in:
Weiko 2024-11-21 22:53:22 +01:00 committed by GitHub
parent 1c04b2b0b7
commit 04c359a5dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 150 additions and 64 deletions

View File

@ -14,7 +14,6 @@ import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-que
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResolverService<
@ -31,14 +30,9 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol
objectMetadataItemWithFieldMaps.nameSingular,
);
const tableName = computeTableName(
objectMetadataItemWithFieldMaps.nameSingular,
objectMetadataItemWithFieldMaps.isCustom,
);
executionArgs.graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
tableName,
objectMetadataItemWithFieldMaps.nameSingular,
executionArgs.args.filter,
);

View File

@ -12,7 +12,6 @@ import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/c
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseResolverService<
@ -29,14 +28,9 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso
objectMetadataItemWithFieldMaps.nameSingular,
);
const tableName = computeTableName(
objectMetadataItemWithFieldMaps.nameSingular,
objectMetadataItemWithFieldMaps.isCustom,
);
executionArgs.graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
tableName,
objectMetadataItemWithFieldMaps.nameSingular,
executionArgs.args.filter,
);

View File

@ -28,7 +28,6 @@ import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/typ
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseResolverService<
@ -111,14 +110,9 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
objectMetadataItemWithFieldMaps.nameSingular,
);
const tableName = computeTableName(
objectMetadataItemWithFieldMaps.nameSingular,
objectMetadataItemWithFieldMaps.isCustom,
);
graphqlQueryParser.applyFilterToBuilder(
duplicateRecordsQueryBuilder,
tableName,
objectMetadataItemWithFieldMaps.nameSingular,
duplicateConditions,
);

View File

@ -30,7 +30,6 @@ import {
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
import { isDefined } from 'src/utils/is-defined';
@Injectable()
@ -57,14 +56,9 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve
let appliedFilters =
executionArgs.args.filter ?? ({} as ObjectRecordFilter);
const tableName = computeTableName(
objectMetadataItemWithFieldMaps.nameSingular,
objectMetadataItemWithFieldMaps.isCustom,
);
executionArgs.graphqlQueryParser.applyFilterToBuilder(
aggregateQueryBuilder,
tableName,
objectMetadataItemWithFieldMaps.nameSingular,
appliedFilters,
);
@ -99,14 +93,14 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve
executionArgs.graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
tableName,
objectMetadataItemWithFieldMaps.nameSingular,
appliedFilters,
);
executionArgs.graphqlQueryParser.applyOrderToBuilder(
queryBuilder,
orderByWithIdCondition,
tableName,
objectMetadataItemWithFieldMaps.nameSingular,
isForwardPagination,
);

View File

@ -23,7 +23,6 @@ import {
WorkspaceQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolverService<
@ -40,14 +39,9 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver
objectMetadataItemWithFieldMaps.nameSingular,
);
const tableName = computeTableName(
objectMetadataItemWithFieldMaps.nameSingular,
objectMetadataItemWithFieldMaps.isCustom,
);
executionArgs.graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
tableName,
objectMetadataItemWithFieldMaps.nameSingular,
executionArgs.args.filter ?? ({} as ObjectRecordFilter),
);

View File

@ -14,7 +14,6 @@ import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-que
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseResolverService<
@ -31,14 +30,9 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso
objectMetadataItemWithFieldMaps.nameSingular,
);
const tableName = computeTableName(
objectMetadataItemWithFieldMaps.nameSingular,
objectMetadataItemWithFieldMaps.isCustom,
);
executionArgs.graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
tableName,
objectMetadataItemWithFieldMaps.nameSingular,
executionArgs.args.filter,
);

View File

@ -20,7 +20,6 @@ import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/g
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
import { isDefined } from 'src/utils/is-defined';
@Injectable()
@ -64,14 +63,9 @@ export class GraphqlQuerySearchResolverService extends GraphqlQueryBaseResolverS
objectMetadataItemWithFieldMaps.nameSingular,
);
const tableName = computeTableName(
objectMetadataItemWithFieldMaps.nameSingular,
objectMetadataItemWithFieldMaps.isCustom,
);
executionArgs.graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
tableName,
objectMetadataItemWithFieldMaps.nameSingular,
executionArgs.args.filter ?? ({} as ObjectRecordFilter),
);

View File

@ -15,7 +15,6 @@ import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResolverService<
@ -32,14 +31,9 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
objectMetadataItemWithFieldMaps.nameSingular,
);
const tableName = computeTableName(
objectMetadataItemWithFieldMaps.nameSingular,
objectMetadataItemWithFieldMaps.isCustom,
);
executionArgs.graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
tableName,
objectMetadataItemWithFieldMaps.nameSingular,
executionArgs.args.filter,
);

View File

@ -7,6 +7,7 @@ import {
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import {
CreateManyResolverArgs,
CreateOneResolverArgs,
FindDuplicatesResolverArgs,
FindManyResolverArgs,
FindOneResolverArgs,
@ -43,6 +44,19 @@ export class QueryRunnerArgsFactory {
);
switch (resolverArgsType) {
case ResolverArgsType.CreateOne:
return {
...args,
data: await this.overrideDataByFieldMetadata(
(args as CreateOneResolverArgs).data,
options,
fieldMetadataMapByNameByName,
{
argIndex: 0,
shouldBackfillPosition,
},
),
} satisfies CreateOneResolverArgs;
case ResolverArgsType.CreateMany:
return {
...args,

View File

@ -3,8 +3,8 @@ import { SCOPE_OPTIONS_METADATA } from '@nestjs/common/constants';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WORKSPACE_QUERY_HOOK_METADATA } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.constants';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { WORKSPACE_QUERY_HOOK_METADATA } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.constants';
export type WorkspaceQueryHookKey =
`${string}.${WorkspaceResolverBuilderMethodNames}`;

View File

@ -1,12 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CreatedByPreQueryHook } from 'src/engine/core-modules/actor/query-hooks/created-by.pre-query-hook';
import { CreatedByCreateManyPreQueryHook } from 'src/engine/core-modules/actor/query-hooks/created-by.create-many.pre-query-hook';
import { CreatedByCreateOnePreQueryHook } from 'src/engine/core-modules/actor/query-hooks/created-by.create-one.pre-query-hook';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@Module({
imports: [TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata')],
providers: [CreatedByPreQueryHook],
exports: [CreatedByPreQueryHook],
providers: [CreatedByCreateManyPreQueryHook, CreatedByCreateOnePreQueryHook],
exports: [CreatedByCreateManyPreQueryHook, CreatedByCreateOnePreQueryHook],
})
export class ActorModule {}

View File

@ -27,8 +27,10 @@ type CustomWorkspaceItem = Omit<
};
@WorkspaceQueryHook(`*.createMany`)
export class CreatedByPreQueryHook implements WorkspaceQueryHookInstance {
private readonly logger = new Logger(CreatedByPreQueryHook.name);
export class CreatedByCreateManyPreQueryHook
implements WorkspaceQueryHookInstance
{
private readonly logger = new Logger(CreatedByCreateManyPreQueryHook.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,

View File

@ -0,0 +1,117 @@
import { Logger } from '@nestjs/common/services/logger.service';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { CreateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { buildCreatedByFromWorkspaceMember } from 'src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import {
ActorMetadata,
FieldActorSource,
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
type CustomWorkspaceItem = Omit<
CustomWorkspaceEntity,
'createdAt' | 'updatedAt'
> & {
createdAt: string;
updatedAt: string;
};
@WorkspaceQueryHook(`*.createOne`)
export class CreatedByCreateOnePreQueryHook
implements WorkspaceQueryHookInstance
{
private readonly logger = new Logger(CreatedByCreateOnePreQueryHook.name);
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
) {}
async execute(
authContext: AuthContext,
objectName: string,
payload: CreateOneResolverArgs<CustomWorkspaceItem>,
): Promise<CreateOneResolverArgs<CustomWorkspaceItem>> {
let createdBy: ActorMetadata | null = null;
// TODO: Once all objects have it, we can remove this check
const createdByFieldMetadata = await this.fieldMetadataRepository.findOne({
where: {
object: {
nameSingular: objectName,
},
name: 'createdBy',
workspaceId: authContext.workspace.id,
},
});
if (!createdByFieldMetadata) {
return payload;
}
// If user is logged in, we use the workspace member
if (authContext.workspaceMemberId && authContext.user) {
createdBy = buildCreatedByFromWorkspaceMember(
authContext.workspaceMemberId,
authContext.user,
);
// TODO: remove that code once we have the workspace member id in all tokens
} else if (authContext.user) {
this.logger.warn("User doesn't have a workspace member id in the token");
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
authContext.workspace.id,
'workspaceMember',
);
const workspaceMember = await workspaceMemberRepository.findOne({
where: {
userId: authContext.user?.id,
},
});
if (!workspaceMember) {
throw new Error(
`Workspace member can't be found for user ${authContext.user.id}`,
);
}
createdBy = {
source: FieldActorSource.MANUAL,
workspaceMemberId: workspaceMember.id,
name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
};
}
if (authContext.apiKey) {
createdBy = {
source: FieldActorSource.API,
name: authContext.apiKey.name,
};
}
// Front-end can fill the source field
if (
createdBy &&
(!payload.data.createdBy || !payload.data.createdBy?.name)
) {
payload.data.createdBy = {
...createdBy,
source: payload.data.createdBy?.source ?? createdBy.source,
};
}
return payload;
}
}