[BUG] Refactor actor composite type (#10232)
fixes #10200 The FieldActor Zod schema was updated to correctly handle null context. --------- Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
committed by
GitHub
parent
927b8c717e
commit
94c0d0f8d2
@ -1,13 +1,12 @@
|
||||
import { ConnectedAccountProvider } from 'twenty-shared';
|
||||
import { ThemeColor } from 'twenty-ui';
|
||||
|
||||
import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues';
|
||||
import { ZodHelperLiteral } from '@/object-record/record-field/types/ZodHelperLiteral';
|
||||
import { RecordForSelect } from '@/object-record/relation-picker/types/RecordForSelect';
|
||||
|
||||
import { ConnectedAccountProvider } from 'twenty-shared';
|
||||
import * as z from 'zod';
|
||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||
import { CurrencyCode } from './CurrencyCode';
|
||||
|
||||
export type FieldUuidMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
fieldName: string;
|
||||
@ -279,23 +278,25 @@ export type FieldRichTextV2Value = {
|
||||
|
||||
export type FieldRichTextValue = null | string;
|
||||
|
||||
type FieldActorSource =
|
||||
| 'API'
|
||||
| 'IMPORT'
|
||||
| 'EMAIL'
|
||||
| 'CALENDAR'
|
||||
| 'MANUAL'
|
||||
| 'SYSTEM'
|
||||
| 'WORKFLOW';
|
||||
const FieldActorSourceSchema = z.union([
|
||||
z.literal('API'),
|
||||
z.literal('IMPORT'),
|
||||
z.literal('EMAIL'),
|
||||
z.literal('CALENDAR'),
|
||||
z.literal('MANUAL'),
|
||||
z.literal('SYSTEM'),
|
||||
z.literal('WORKFLOW'),
|
||||
]);
|
||||
|
||||
export type FieldActorValue = {
|
||||
source: FieldActorSource;
|
||||
workspaceMemberId: string | null;
|
||||
name: string;
|
||||
context: {
|
||||
provider?: ConnectedAccountProvider;
|
||||
} | null;
|
||||
};
|
||||
export const FieldActorValueSchema = z.object({
|
||||
source: FieldActorSourceSchema,
|
||||
workspaceMemberId: z.string().nullable(),
|
||||
name: z.string(),
|
||||
context: z.object({
|
||||
provider: z.nativeEnum(ConnectedAccountProvider).optional(),
|
||||
}),
|
||||
});
|
||||
export type FieldActorValue = z.infer<typeof FieldActorValueSchema>;
|
||||
|
||||
export type FieldActorForInputValue = Pick<
|
||||
FieldActorValue,
|
||||
|
||||
@ -1,20 +1,9 @@
|
||||
import { ConnectedAccountProvider } from 'twenty-shared';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FieldActorValue } from '../FieldMetadata';
|
||||
|
||||
const actorSchema = z.object({
|
||||
source: z.string(),
|
||||
workspaceMemberId: z.optional(z.string().nullable()),
|
||||
name: z.string(),
|
||||
context: z.optional(
|
||||
z.object({
|
||||
provider: z.optional(z.nativeEnum(ConnectedAccountProvider)),
|
||||
}),
|
||||
),
|
||||
});
|
||||
import {
|
||||
FieldActorValue,
|
||||
FieldActorValueSchema,
|
||||
} from '@/object-record/record-field/types/FieldMetadata';
|
||||
|
||||
export const isFieldActorValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldActorValue => actorSchema.safeParse(fieldValue).success;
|
||||
): fieldValue is FieldActorValue =>
|
||||
FieldActorValueSchema.safeParse(fieldValue).success;
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
const jestConfig = {
|
||||
// For more information please have a look to official docs https://jestjs.io/docs/configuration/#prettierpath-string
|
||||
// Prettier v3 will should be supported in jest v30 https://github.com/jestjs/jest/releases/tag/v30.0.0-alpha.1
|
||||
prettierPath: null,
|
||||
// to enable logs, comment out the following line
|
||||
silent: true,
|
||||
clearMocks: true,
|
||||
|
||||
@ -0,0 +1,114 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { FieldMetadataType } from 'twenty-shared';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { CommandLogger } from 'src/database/commands/logger';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.42:standardization-of-actor-composite-context-type',
|
||||
description: 'Add context to actor composite type.',
|
||||
})
|
||||
export class StandardizationOfActorCompositeContextTypeCommand extends ActiveWorkspacesCommandRunner {
|
||||
protected readonly logger;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
this.logger = new CommandLogger({
|
||||
constructorName: this.constructor.name,
|
||||
verbose: false,
|
||||
});
|
||||
this.logger.setVerbose(false);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log(`Running add-context-to-actor-composite-type command`);
|
||||
|
||||
if (options?.dryRun) {
|
||||
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
|
||||
}
|
||||
|
||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
||||
try {
|
||||
await this.execute(workspaceId, options?.dryRun);
|
||||
this.logger.verbose(
|
||||
`[${index + 1}/${workspaceIds.length}] Added for workspace: ${workspaceId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error for workspace: ${workspaceId}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async execute(workspaceId: string, dryRun = false): Promise<void> {
|
||||
this.logger.verbose(`Adding for workspace: ${workspaceId}`);
|
||||
const actorFields = await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
type: FieldMetadataType.ACTOR,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['object'],
|
||||
});
|
||||
|
||||
for (const field of actorFields) {
|
||||
if (!field || !field.object) {
|
||||
this.logger.verbose(
|
||||
'field.objectMetadata is null',
|
||||
workspaceId,
|
||||
field.id,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
field.object.nameSingular,
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
const rowsToUpdate = await fieldRepository.update(
|
||||
{
|
||||
[field.name + 'Context']: IsNull(),
|
||||
},
|
||||
{
|
||||
[field.name + 'Context']: {},
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.verbose(
|
||||
`updated ${rowsToUpdate ? rowsToUpdate.affected : 0} rows`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
this.twentyORMGlobalManager.destroyDataSourceForWorkspace(workspaceId);
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ import { BaseCommandOptions } from 'src/database/commands/base.command';
|
||||
import { FixBodyV2ViewFieldPositionCommand } from 'src/database/commands/upgrade-version/0-42/0-42-fix-body-v2-view-field-position.command';
|
||||
import { LimitAmountOfViewFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-limit-amount-of-view-field';
|
||||
import { MigrateRichTextFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-migrate-rich-text-field.command';
|
||||
import { StandardizationOfActorCompositeContextTypeCommand } from 'src/database/commands/upgrade-version/0-42/0-42-standardization-of-actor-composite-context-type';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
|
||||
@ -28,6 +29,7 @@ export class UpgradeTo0_42Command extends ActiveWorkspacesCommandRunner {
|
||||
private readonly fixBodyV2ViewFieldPositionCommand: FixBodyV2ViewFieldPositionCommand,
|
||||
private readonly limitAmountOfViewFieldCommand: LimitAmountOfViewFieldCommand,
|
||||
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
private readonly standardizationOfActorCompositeContextType: StandardizationOfActorCompositeContextTypeCommand,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
@ -75,5 +77,11 @@ export class UpgradeTo0_42Command extends ActiveWorkspacesCommandRunner {
|
||||
},
|
||||
workspaceIds,
|
||||
);
|
||||
|
||||
await this.standardizationOfActorCompositeContextType.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { FixBodyV2ViewFieldPositionCommand } from 'src/database/commands/upgrade-version/0-42/0-42-fix-body-v2-view-field-position.command';
|
||||
import { LimitAmountOfViewFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-limit-amount-of-view-field';
|
||||
import { MigrateRichTextFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-migrate-rich-text-field.command';
|
||||
import { StandardizationOfActorCompositeContextTypeCommand } from 'src/database/commands/upgrade-version/0-42/0-42-standardization-of-actor-composite-context-type';
|
||||
import { UpgradeTo0_42Command } from 'src/database/commands/upgrade-version/0-42/0-42-upgrade-version.command';
|
||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
@ -35,6 +36,7 @@ import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manage
|
||||
MigrateRichTextFieldCommand,
|
||||
FixBodyV2ViewFieldPositionCommand,
|
||||
LimitAmountOfViewFieldCommand,
|
||||
StandardizationOfActorCompositeContextTypeCommand,
|
||||
],
|
||||
})
|
||||
export class UpgradeTo0_42CommandModule {}
|
||||
|
||||
@ -5,6 +5,7 @@ import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
|
||||
import { BaseCommandOptions } from 'src/database/commands/base.command';
|
||||
import { StandardizationOfActorCompositeContextTypeCommand } from 'src/database/commands/upgrade-version/0-42/0-42-standardization-of-actor-composite-context-type';
|
||||
import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-version/0-43/0-43-add-tasks-assigned-to-me-view.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@ -17,6 +18,7 @@ export class UpgradeTo0_43Command extends ActiveWorkspacesCommandRunner {
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly addTasksAssignedToMeViewCommand: AddTasksAssignedToMeViewCommand,
|
||||
private readonly standardizationOfActorCompositeContextTypeCommand: StandardizationOfActorCompositeContextTypeCommand,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
@ -33,5 +35,12 @@ export class UpgradeTo0_43Command extends ActiveWorkspacesCommandRunner {
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
|
||||
// Note: Introduced in 0.42, ran manually on prod. Introduced to self-host globally on 0.43
|
||||
await this.standardizationOfActorCompositeContextTypeCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { StandardizationOfActorCompositeContextTypeCommand } from 'src/database/commands/upgrade-version/0-42/0-42-standardization-of-actor-composite-context-type';
|
||||
import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-version/0-43/0-43-add-tasks-assigned-to-me-view.command';
|
||||
import { MigrateSearchVectorOnNoteAndTaskEntitiesCommand } from 'src/database/commands/upgrade-version/0-43/0-43-migrate-search-vector-on-note-and-task-entities.command';
|
||||
import { UpgradeTo0_43Command } from 'src/database/commands/upgrade-version/0-43/0-43-upgrade-version.command';
|
||||
@ -29,6 +30,7 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor
|
||||
UpgradeTo0_43Command,
|
||||
AddTasksAssignedToMeViewCommand,
|
||||
MigrateSearchVectorOnNoteAndTaskEntitiesCommand,
|
||||
StandardizationOfActorCompositeContextTypeCommand,
|
||||
],
|
||||
})
|
||||
export class UpgradeTo0_43CommandModule {}
|
||||
|
||||
@ -5,9 +5,19 @@ import { CreatedByCreateManyPreQueryHook } from 'src/engine/core-modules/actor/q
|
||||
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';
|
||||
|
||||
import { CreatedByFromAuthContextService } from './services/created-by-from-auth-context.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata')],
|
||||
providers: [CreatedByCreateManyPreQueryHook, CreatedByCreateOnePreQueryHook],
|
||||
exports: [CreatedByCreateManyPreQueryHook, CreatedByCreateOnePreQueryHook],
|
||||
providers: [
|
||||
CreatedByCreateManyPreQueryHook,
|
||||
CreatedByCreateOnePreQueryHook,
|
||||
CreatedByFromAuthContextService,
|
||||
],
|
||||
exports: [
|
||||
CreatedByCreateManyPreQueryHook,
|
||||
CreatedByCreateOnePreQueryHook,
|
||||
CreatedByFromAuthContextService,
|
||||
],
|
||||
})
|
||||
export class ActorModule {}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'class-validator';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
@ -12,16 +11,10 @@ import {
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
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 { CreatedByFromAuthContextService } from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service';
|
||||
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,
|
||||
@ -35,12 +28,10 @@ type CustomWorkspaceItem = Omit<
|
||||
export class CreatedByCreateManyPreQueryHook
|
||||
implements WorkspaceQueryHookInstance
|
||||
{
|
||||
private readonly logger = new Logger(CreatedByCreateManyPreQueryHook.name);
|
||||
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
private readonly createdByFromAuthContextService: CreatedByFromAuthContextService,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
@ -48,8 +39,6 @@ export class CreatedByCreateManyPreQueryHook
|
||||
objectName: string,
|
||||
payload: CreateManyResolverArgs<CustomWorkspaceItem>,
|
||||
): Promise<CreateManyResolverArgs<CustomWorkspaceItem>> {
|
||||
let createdBy: ActorMetadata | null = null;
|
||||
|
||||
if (!isDefined(payload.data)) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Payload data is required',
|
||||
@ -72,46 +61,8 @@ export class CreatedByCreateManyPreQueryHook
|
||||
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,
|
||||
};
|
||||
}
|
||||
const createdBy =
|
||||
await this.createdByFromAuthContextService.buildCreatedBy(authContext);
|
||||
|
||||
for (const datum of payload.data) {
|
||||
// Front-end can fill the source field
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'class-validator';
|
||||
@ -12,16 +11,10 @@ import {
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
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 { CreatedByFromAuthContextService } from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service';
|
||||
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,
|
||||
@ -35,12 +28,10 @@ type CustomWorkspaceItem = Omit<
|
||||
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>,
|
||||
private readonly createdByFromAuthContextService: CreatedByFromAuthContextService,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
@ -48,8 +39,6 @@ export class CreatedByCreateOnePreQueryHook
|
||||
objectName: string,
|
||||
payload: CreateOneResolverArgs<CustomWorkspaceItem>,
|
||||
): Promise<CreateOneResolverArgs<CustomWorkspaceItem>> {
|
||||
let createdBy: ActorMetadata | null = null;
|
||||
|
||||
if (!isDefined(payload.data)) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Payload data is required',
|
||||
@ -72,46 +61,8 @@ export class CreatedByCreateOnePreQueryHook
|
||||
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,
|
||||
};
|
||||
}
|
||||
const createdBy =
|
||||
await this.createdByFromAuthContextService.buildCreatedBy(authContext);
|
||||
|
||||
// Front-end can fill the source field
|
||||
if (
|
||||
|
||||
@ -0,0 +1,148 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { CreatedByFromAuthContextService } from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import {
|
||||
ActorMetadata,
|
||||
FieldActorSource,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
type TestingAuthContext = Omit<AuthContext, 'workspace' | 'apiKey' | 'user'> & {
|
||||
workspace: Partial<Workspace>;
|
||||
apiKey?: Partial<ApiKeyWorkspaceEntity>;
|
||||
user?: Partial<User>;
|
||||
};
|
||||
// TODO create util
|
||||
const fromFullNameMetadataToName = ({
|
||||
firstName,
|
||||
lastName,
|
||||
}: FullNameMetadata) => `${firstName} ${lastName}`;
|
||||
|
||||
describe('CreatedByFromAuthContextService', () => {
|
||||
let service: CreatedByFromAuthContextService;
|
||||
const mockWorkspaceMemberRepository = {
|
||||
findOneOrFail: jest.fn(),
|
||||
};
|
||||
const twentyORMGlobalManager: jest.Mocked<
|
||||
Pick<TwentyORMGlobalManager, 'getRepositoryForWorkspace'>
|
||||
> = {
|
||||
getRepositoryForWorkspace: jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockWorkspaceMemberRepository),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
CreatedByFromAuthContextService,
|
||||
{
|
||||
provide: TwentyORMGlobalManager,
|
||||
useValue: twentyORMGlobalManager,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<CreatedByFromAuthContextService>(
|
||||
CreatedByFromAuthContextService,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
describe('buildCreatedBy', () => {
|
||||
it('should build metadata from workspaceMemberId and user when both are present', async () => {
|
||||
const authContext = {
|
||||
workspaceMemberId: '20202020-0b5c-4178-bed7-d371f6411eaa',
|
||||
user: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
id: '20202020-9aae-49a8-bafc-ac44bae62d6d',
|
||||
},
|
||||
workspace: { id: '20202020-bdec-497f-847a-1bb334fefe58' },
|
||||
} as const satisfies TestingAuthContext;
|
||||
|
||||
const result = await service.buildCreatedBy(authContext as AuthContext);
|
||||
|
||||
expect(result).toEqual<ActorMetadata>({
|
||||
context: {},
|
||||
name: fromFullNameMetadataToName(authContext.user),
|
||||
workspaceMemberId: authContext.workspaceMemberId,
|
||||
source: FieldActorSource.MANUAL,
|
||||
});
|
||||
});
|
||||
|
||||
it('should build metadata from user when workspaceMemberId is missing', async () => {
|
||||
const authContext = {
|
||||
user: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
id: '20202020-9aae-49a8-bafc-ac44bae62d6d',
|
||||
},
|
||||
workspace: { id: '20202020-bdec-497f-847a-1bb334fefe58' },
|
||||
} as const satisfies TestingAuthContext;
|
||||
|
||||
const mockedWorkspaceMember = {
|
||||
id: '20202020-78a3-4520-ba74-b0e1b534a501',
|
||||
name: {
|
||||
firstName: 'Pepito',
|
||||
lastName: 'Dubois',
|
||||
},
|
||||
} as const satisfies Partial<WorkspaceMemberWorkspaceEntity>;
|
||||
|
||||
mockWorkspaceMemberRepository.findOneOrFail.mockResolvedValueOnce(
|
||||
mockedWorkspaceMember,
|
||||
);
|
||||
|
||||
const result = await service.buildCreatedBy(authContext as AuthContext);
|
||||
|
||||
expect(result).toEqual<ActorMetadata>({
|
||||
context: {},
|
||||
name: fromFullNameMetadataToName(mockedWorkspaceMember.name),
|
||||
workspaceMemberId: mockedWorkspaceMember.id,
|
||||
source: FieldActorSource.MANUAL,
|
||||
});
|
||||
});
|
||||
|
||||
it('should build metadata from apiKey when only apiKey is present', async () => {
|
||||
const authContext = {
|
||||
apiKey: {
|
||||
id: '20202020-56c2-471b-925d-31ed3ecd0951',
|
||||
name: 'API Key Name',
|
||||
},
|
||||
workspace: { id: '20202020-bdec-497f-847a-1bb334fefe58' },
|
||||
} as const satisfies TestingAuthContext;
|
||||
|
||||
const result = await service.buildCreatedBy(authContext as AuthContext);
|
||||
|
||||
expect(result).toEqual<ActorMetadata>({
|
||||
source: FieldActorSource.API,
|
||||
workspaceMemberId: null,
|
||||
name: authContext.apiKey.name,
|
||||
context: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when no valid actor information is found', async () => {
|
||||
const authContext = {
|
||||
workspace: { id: 'workspace-id' },
|
||||
} as const satisfies TestingAuthContext;
|
||||
|
||||
await expect(
|
||||
service.buildCreatedBy(authContext as AuthContext),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unable to build createdBy metadata - no valid actor information found in auth context"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,67 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
import { buildCreatedByFromApiKey } from 'src/engine/core-modules/actor/utils/build-created-by-from-api-key.util';
|
||||
import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class CreatedByFromAuthContextService {
|
||||
private readonly logger = new Logger(CreatedByFromAuthContextService.name);
|
||||
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
public async buildCreatedBy(
|
||||
authContext: AuthContext,
|
||||
): Promise<ActorMetadata> {
|
||||
const { workspace, workspaceMemberId, user, apiKey } = authContext;
|
||||
|
||||
// TODO: remove that code once we have the workspace member id in all tokens
|
||||
if (isDefined(workspaceMemberId) && isDefined(user)) {
|
||||
return buildCreatedByFromFullNameMetadata({
|
||||
fullNameMetadata: {
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
},
|
||||
workspaceMemberId,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(user)) {
|
||||
this.logger.warn("User doesn't have a workspace member id in the token");
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
|
||||
workspace.id,
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
return buildCreatedByFromFullNameMetadata({
|
||||
fullNameMetadata: workspaceMember.name,
|
||||
workspaceMemberId: workspaceMember.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(apiKey)) {
|
||||
return buildCreatedByFromApiKey({
|
||||
apiKey,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Unable to build createdBy metadata - no valid actor information found in auth context',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import {
|
||||
ActorMetadata,
|
||||
FieldActorSource,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity';
|
||||
|
||||
type BuildCreatedByFromApiKeyArgs = {
|
||||
apiKey: ApiKeyWorkspaceEntity;
|
||||
};
|
||||
export const buildCreatedByFromApiKey = ({
|
||||
apiKey,
|
||||
}: BuildCreatedByFromApiKeyArgs): ActorMetadata => ({
|
||||
source: FieldActorSource.API,
|
||||
name: apiKey.name,
|
||||
workspaceMemberId: null,
|
||||
context: {},
|
||||
});
|
||||
@ -0,0 +1,19 @@
|
||||
import {
|
||||
ActorMetadata,
|
||||
FieldActorSource,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
|
||||
|
||||
type BuildCreatedByFromFullNameMetadataArgs = {
|
||||
workspaceMemberId: string;
|
||||
fullNameMetadata: FullNameMetadata;
|
||||
};
|
||||
export const buildCreatedByFromFullNameMetadata = ({
|
||||
fullNameMetadata,
|
||||
workspaceMemberId,
|
||||
}: BuildCreatedByFromFullNameMetadataArgs): ActorMetadata => ({
|
||||
workspaceMemberId,
|
||||
source: FieldActorSource.MANUAL,
|
||||
name: `${fullNameMetadata.firstName} ${fullNameMetadata.lastName}`,
|
||||
context: {},
|
||||
});
|
||||
@ -1,11 +0,0 @@
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
|
||||
export const buildCreatedByFromWorkspaceMember = (
|
||||
workspaceMemberId: string,
|
||||
user: User,
|
||||
) => ({
|
||||
workspaceMemberId,
|
||||
source: FieldActorSource.MANUAL,
|
||||
name: `${user.firstName} ${user.lastName}`,
|
||||
});
|
||||
@ -56,9 +56,9 @@ export const actorCompositeType: CompositeType = {
|
||||
|
||||
export type ActorMetadata = {
|
||||
source: FieldActorSource;
|
||||
workspaceMemberId?: string;
|
||||
workspaceMemberId: string | null;
|
||||
name: string;
|
||||
context?: {
|
||||
context: {
|
||||
provider?: ConnectedAccountProvider;
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { Scope } from '@nestjs/common';
|
||||
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import {
|
||||
@ -15,8 +17,6 @@ import {
|
||||
WorkflowTriggerException,
|
||||
WorkflowTriggerExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
|
||||
export type WorkflowTriggerJobData = {
|
||||
workspaceId: string;
|
||||
@ -75,6 +75,8 @@ export class WorkflowTriggerJob {
|
||||
{
|
||||
source: FieldActorSource.WORKFLOW,
|
||||
name: workflow.name,
|
||||
context: {},
|
||||
workspaceMemberId: null,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
|
||||
@ -4,7 +4,10 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { EntityManager, Repository } from 'typeorm';
|
||||
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { buildCreatedByFromWorkspaceMember } from 'src/engine/core-modules/actor/utils/build-created-by-from-workspace-member.util';
|
||||
import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
@ -26,17 +29,14 @@ import {
|
||||
WorkflowTriggerException,
|
||||
WorkflowTriggerExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
|
||||
import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
||||
import { assertVersionCanBeActivated } from 'src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util';
|
||||
import { assertNever } from 'src/utils/assert';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import {
|
||||
WorkflowTriggerJob,
|
||||
WorkflowTriggerJobData,
|
||||
} from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job';
|
||||
import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
||||
import { assertVersionCanBeActivated } from 'src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util';
|
||||
import { computeCronPatternFromSchedule } from 'src/modules/workflow/workflow-trigger/utils/compute-cron-pattern-from-schedule';
|
||||
import { assertNever } from 'src/utils/assert';
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowTriggerWorkspaceService {
|
||||
@ -70,7 +70,7 @@ export class WorkflowTriggerWorkspaceService {
|
||||
workflowVersionId: string,
|
||||
payload: object,
|
||||
workspaceMemberId: string,
|
||||
user: User,
|
||||
{ firstName, lastName }: User,
|
||||
) {
|
||||
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail(
|
||||
workflowVersionId,
|
||||
@ -80,7 +80,10 @@ export class WorkflowTriggerWorkspaceService {
|
||||
this.getWorkspaceId(),
|
||||
workflowVersionId,
|
||||
payload,
|
||||
buildCreatedByFromWorkspaceMember(workspaceMemberId, user),
|
||||
buildCreatedByFromFullNameMetadata({
|
||||
fullNameMetadata: { firstName, lastName },
|
||||
workspaceMemberId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user