feat: add updatedBy field to track last record modifier (#16807)

<!-- CURSOR_SUMMARY -->
> [!NOTE]
> Implements last-modifier tracking and unified actor injection.
> 
> - New `ActorFromAuthContextService` replaces createdBy-only logic;
pre-query hooks now inject both `createdBy` and `updatedBy` on create
and `updatedBy` on update
> - Add `updatedBy` standard field to core/custom objects (e.g.,
`person`, `company`, `task`, `note`, `attachment`, `dashboard`,
`workflow`, `workflowRun`) with IDs, metadata builders, and ORM entity
fields
> - Record CRUD: `create` now sets both `createdBy` and `updatedBy`;
`update` sets `updatedBy`; workflow actions use a shared workflow actor
builder; REST base handler uses the new actor service
> - Seeder data and snapshots updated to include `updatedBy`; GraphQL
result formatting adds defaults/handling for empty composite fields
(incl. actor)
> - Frontend `useUpdateOneRecord` upserts returned record into the local
store after mutation
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
1f283778e9cb28a35bf84632a1a2997250bb3131. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Abdul Rahman
2025-12-29 14:35:17 +05:30
committed by GitHub
parent 0da5cf29de
commit 01e0502fcf
58 changed files with 1516 additions and 118 deletions
@@ -5,6 +5,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
@@ -165,6 +166,11 @@ export const useUpdateOneRecord = <
const record = data?.[mutationResponseField];
if (!isDefined(record)) return;
const recordToUpsert = getRecordFromRecordNode({
recordNode: record,
});
upsertRecordsInStore({ partialRecords: [recordToUpsert] });
triggerUpdateRecordOptimisticEffect({
cache,
objectMetadataItem,
@@ -20,6 +20,12 @@ const baseNote = {
workspaceMemberId: '1',
context: {},
},
updatedBy: {
name: 'Test',
source: FieldActorSource.MANUAL,
workspaceMemberId: '1',
context: {},
},
createdAt: '2021-01-01',
updatedAt: '2021-01-01',
noteTargets: [],
@@ -15,7 +15,7 @@ import { RestToCommonSelectedFieldsHandler } from 'src/engine/api/rest/core/rest
import { parseCorePath } from 'src/engine/api/rest/input-request-parsers/path-parser-utils/parse-core-path.utils';
import { Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { CreatedByFromAuthContextService } from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service';
import { ActorFromAuthContextService } from 'src/engine/core-modules/actor/services/actor-from-auth-context.service';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
@@ -57,7 +57,7 @@ export abstract class RestApiBaseHandler {
@Inject()
protected readonly workspaceCacheService: WorkspaceCacheService;
@Inject()
protected readonly createdByFromAuthContextService: CreatedByFromAuthContextService;
protected readonly actorFromAuthContextService: ActorFromAuthContextService;
@Inject()
protected readonly workspaceCacheStorageService: WorkspaceCacheStorageService;
@Inject()
@@ -3,10 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm';
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 { UpdatedByUpdateManyPreQueryHook } from 'src/engine/core-modules/actor/query-hooks/updated-by.update-many.pre-query-hook';
import { UpdatedByUpdateOnePreQueryHook } from 'src/engine/core-modules/actor/query-hooks/updated-by.update-one.pre-query-hook';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { CreatedByFromAuthContextService } from './services/created-by-from-auth-context.service';
import { ActorFromAuthContextService } from './services/actor-from-auth-context.service';
@Module({
imports: [
@@ -16,12 +18,16 @@ import { CreatedByFromAuthContextService } from './services/created-by-from-auth
providers: [
CreatedByCreateManyPreQueryHook,
CreatedByCreateOnePreQueryHook,
CreatedByFromAuthContextService,
UpdatedByUpdateManyPreQueryHook,
UpdatedByUpdateOnePreQueryHook,
ActorFromAuthContextService,
],
exports: [
CreatedByCreateManyPreQueryHook,
CreatedByCreateOnePreQueryHook,
CreatedByFromAuthContextService,
UpdatedByUpdateManyPreQueryHook,
UpdatedByUpdateOnePreQueryHook,
ActorFromAuthContextService,
],
})
export class ActorModule {}
@@ -9,9 +9,9 @@ import {
} 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 {
CreatedByFromAuthContextService,
type CreateInput,
} from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service';
ActorFromAuthContextService,
type RecordInput,
} from 'src/engine/core-modules/actor/services/actor-from-auth-context.service';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@WorkspaceQueryHook(`*.createMany`)
@@ -19,14 +19,14 @@ export class CreatedByCreateManyPreQueryHook
implements WorkspacePreQueryHookInstance
{
constructor(
private readonly createdByFromAuthContextService: CreatedByFromAuthContextService,
private readonly actorFromAuthContextService: ActorFromAuthContextService,
) {}
async execute(
authContext: AuthContext,
objectName: string,
payload: CreateManyResolverArgs<CreateInput>,
): Promise<CreateManyResolverArgs<CreateInput>> {
payload: CreateManyResolverArgs<RecordInput>,
): Promise<CreateManyResolverArgs<RecordInput>> {
if (!isDefined(payload.data)) {
throw new GraphqlQueryRunnerException(
'Payload data is required',
@@ -36,11 +36,11 @@ export class CreatedByCreateManyPreQueryHook
return {
...payload,
data: await this.createdByFromAuthContextService.injectCreatedBy(
payload.data,
objectName,
data: await this.actorFromAuthContextService.injectActorFieldsOnCreate({
records: payload.data,
objectMetadataNameSingular: objectName,
authContext,
),
}),
};
}
}
@@ -1,4 +1,4 @@
import { isDefined } from 'class-validator';
import { isDefined } from 'twenty-shared/utils';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type CreateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@@ -9,9 +9,9 @@ import {
} 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 {
CreatedByFromAuthContextService,
type CreateInput,
} from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service';
ActorFromAuthContextService,
type RecordInput,
} from 'src/engine/core-modules/actor/services/actor-from-auth-context.service';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@WorkspaceQueryHook(`*.createOne`)
@@ -19,14 +19,14 @@ export class CreatedByCreateOnePreQueryHook
implements WorkspacePreQueryHookInstance
{
constructor(
private readonly createdByFromAuthContextService: CreatedByFromAuthContextService,
private readonly actorFromAuthContextService: ActorFromAuthContextService,
) {}
async execute(
authContext: AuthContext,
objectName: string,
payload: CreateOneResolverArgs<CreateInput>,
): Promise<CreateOneResolverArgs<CreateInput>> {
payload: CreateOneResolverArgs<RecordInput>,
): Promise<CreateOneResolverArgs<RecordInput>> {
if (!isDefined(payload.data)) {
throw new GraphqlQueryRunnerException(
'Payload data is required',
@@ -35,11 +35,11 @@ export class CreatedByCreateOnePreQueryHook
}
const [recordToCreateData] =
await this.createdByFromAuthContextService.injectCreatedBy(
[payload.data],
objectName,
await this.actorFromAuthContextService.injectActorFieldsOnCreate({
records: [payload.data],
objectMetadataNameSingular: objectName,
authContext,
);
});
return {
...payload,
@@ -0,0 +1,49 @@
import { isDefined } from 'twenty-shared/utils';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type UpdateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
GraphqlQueryRunnerException,
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 {
ActorFromAuthContextService,
type RecordInput,
} from 'src/engine/core-modules/actor/services/actor-from-auth-context.service';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@WorkspaceQueryHook(`*.updateMany`)
export class UpdatedByUpdateManyPreQueryHook
implements WorkspacePreQueryHookInstance
{
constructor(
private readonly actorFromAuthContextService: ActorFromAuthContextService,
) {}
async execute(
authContext: AuthContext,
objectName: string,
payload: UpdateManyResolverArgs<RecordInput>,
): Promise<UpdateManyResolverArgs<RecordInput>> {
if (!isDefined(payload.data)) {
throw new GraphqlQueryRunnerException(
'Payload data is required',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
const [recordToUpdateData] =
await this.actorFromAuthContextService.injectUpdatedBy({
records: [payload.data],
objectMetadataNameSingular: objectName,
authContext,
});
return {
...payload,
data: recordToUpdateData,
};
}
}
@@ -0,0 +1,49 @@
import { isDefined } from 'twenty-shared/utils';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
GraphqlQueryRunnerException,
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 {
ActorFromAuthContextService,
type RecordInput,
} from 'src/engine/core-modules/actor/services/actor-from-auth-context.service';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
@WorkspaceQueryHook(`*.updateOne`)
export class UpdatedByUpdateOnePreQueryHook
implements WorkspacePreQueryHookInstance
{
constructor(
private readonly actorFromAuthContextService: ActorFromAuthContextService,
) {}
async execute(
authContext: AuthContext,
objectName: string,
payload: UpdateOneResolverArgs<RecordInput>,
): Promise<UpdateOneResolverArgs<RecordInput>> {
if (!isDefined(payload.data)) {
throw new GraphqlQueryRunnerException(
'Payload data is required',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
const [recordToUpdateData] =
await this.actorFromAuthContextService.injectUpdatedBy({
records: [payload.data],
objectMetadataNameSingular: objectName,
authContext,
});
return {
...payload,
data: recordToUpdateData,
};
}
}
@@ -7,7 +7,7 @@ import {
type FullNameMetadata,
} from 'twenty-shared/types';
import { CreatedByFromAuthContextService } from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service';
import { ActorFromAuthContextService } from 'src/engine/core-modules/actor/services/actor-from-auth-context.service';
import { type ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { type UserEntity } from 'src/engine/core-modules/user/user.entity';
@@ -31,8 +31,8 @@ const fromFullNameMetadataToName = ({
lastName,
}: FullNameMetadata) => `${firstName} ${lastName}`;
describe('CreatedByFromAuthContextService', () => {
let service: CreatedByFromAuthContextService;
describe('ActorFromAuthContextService', () => {
let service: ActorFromAuthContextService;
const mockWorkspaceMemberRepository = {
findOneOrFail: jest.fn(),
};
@@ -51,7 +51,7 @@ describe('CreatedByFromAuthContextService', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CreatedByFromAuthContextService,
ActorFromAuthContextService,
{
provide: GlobalWorkspaceOrmManager,
useValue: globalWorkspaceOrmManager,
@@ -93,8 +93,8 @@ describe('CreatedByFromAuthContextService', () => {
],
}).compile();
service = module.get<CreatedByFromAuthContextService>(
CreatedByFromAuthContextService,
service = module.get<ActorFromAuthContextService>(
ActorFromAuthContextService,
);
});
@@ -129,11 +129,11 @@ describe('CreatedByFromAuthContextService', () => {
mockedWorkspaceMember,
);
const result = await service.injectCreatedBy(
[{}],
'person',
authContext as AuthContext,
);
const result = await service.injectCreatedBy({
records: [{}],
objectMetadataNameSingular: 'person',
authContext: authContext as AuthContext,
});
expect(result).toEqual<ExpectedResult>([
{
@@ -169,11 +169,11 @@ describe('CreatedByFromAuthContextService', () => {
mockedWorkspaceMember,
);
const result = await service.injectCreatedBy(
[{}],
'person',
authContext as AuthContext,
);
const result = await service.injectCreatedBy({
records: [{}],
objectMetadataNameSingular: 'person',
authContext: authContext as AuthContext,
});
expect(result).toEqual<ExpectedResult>([
{
@@ -196,11 +196,11 @@ describe('CreatedByFromAuthContextService', () => {
workspace: { id: '20202020-bdec-497f-847a-1bb334fefe58' },
} as const satisfies TestingAuthContext;
const result = await service.injectCreatedBy(
[{}],
'person',
authContext as AuthContext,
);
const result = await service.injectCreatedBy({
records: [{}],
objectMetadataNameSingular: 'person',
authContext: authContext as AuthContext,
});
expect(result).toEqual<ExpectedResult>([
{
@@ -220,9 +220,13 @@ describe('CreatedByFromAuthContextService', () => {
} as const satisfies TestingAuthContext;
await expect(
service.injectCreatedBy([{}], 'person', authContext as AuthContext),
service.injectCreatedBy({
records: [{}],
objectMetadataNameSingular: 'person',
authContext: authContext as AuthContext,
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unable to build createdBy metadata - no valid actor information found in auth context"`,
`"Unable to build actor metadata - no valid actor information found in auth context"`,
);
});
});
@@ -6,6 +6,7 @@ import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { buildCreatedByFromApiKey } from 'src/engine/core-modules/actor/utils/build-created-by-from-api-key.util';
import { buildCreatedByFromApplication } from 'src/engine/core-modules/actor/utils/build-created-by-from-application.util';
import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
@@ -14,25 +15,78 @@ import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-module
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { buildCreatedByFromApplication } from 'src/engine/core-modules/actor/utils/build-created-by-from-application.util';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type CreateInput = Record<string, any>;
export type RecordInput = Record<string, unknown>;
export type InjectActorParams = {
records: RecordInput[];
objectMetadataNameSingular: string;
authContext: AuthContext;
};
@Injectable()
export class CreatedByFromAuthContextService {
private readonly logger = new Logger(CreatedByFromAuthContextService.name);
export class ActorFromAuthContextService {
private readonly logger = new Logger(ActorFromAuthContextService.name);
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
async injectCreatedBy(
records: CreateInput[],
objectMetadataNameSingular: string,
authContext: AuthContext,
): Promise<CreateInput[]> {
async injectCreatedBy({
records,
objectMetadataNameSingular,
authContext,
}: InjectActorParams): Promise<RecordInput[]> {
return this.injectActorField({
records,
objectMetadataNameSingular,
authContext,
fieldName: 'createdBy',
});
}
async injectActorFieldsOnCreate({
records,
objectMetadataNameSingular,
authContext,
}: InjectActorParams): Promise<RecordInput[]> {
const recordsWithCreatedBy = await this.injectActorField({
records,
objectMetadataNameSingular,
authContext,
fieldName: 'createdBy',
});
return await this.injectActorField({
records: recordsWithCreatedBy,
objectMetadataNameSingular,
authContext,
fieldName: 'updatedBy',
});
}
async injectUpdatedBy({
records,
objectMetadataNameSingular,
authContext,
}: InjectActorParams): Promise<RecordInput[]> {
return this.injectActorField({
records,
objectMetadataNameSingular,
authContext,
fieldName: 'updatedBy',
});
}
private async injectActorField({
records,
objectMetadataNameSingular,
authContext,
fieldName,
}: InjectActorParams & { fieldName: 'createdBy' | 'updatedBy' }): Promise<
RecordInput[]
> {
const workspace = authContext.workspace;
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
@@ -46,7 +100,7 @@ export class CreatedByFromAuthContextService {
);
this.logger.log(
`Injecting createdBy from auth context for object ${objectMetadataNameSingular} and workspace ${workspace.id}`,
`Injecting ${fieldName} from auth context for object ${objectMetadataNameSingular} and workspace ${workspace.id}`,
);
const { idByNameSingular } = buildObjectIdByNameMaps(
@@ -68,9 +122,9 @@ export class CreatedByFromAuthContextService {
`Object metadata found with fields: ${Object.keys(fieldIdByName)}`,
);
if (!isDefined(fieldIdByName['createdBy'])) {
if (!isDefined(fieldIdByName[fieldName])) {
this.logger.log(
`CreatedBy field not found in object metadata, skipping injection`,
`${fieldName} field not found in object metadata, skipping injection`,
);
return records;
@@ -78,33 +132,35 @@ export class CreatedByFromAuthContextService {
const clonedRecords = structuredClone(records);
const createdBy = await this.buildCreatedBy(authContext);
const actorMetadata = await this.buildActorMetadata(authContext);
if (Array.isArray(clonedRecords)) {
for (const datum of clonedRecords) {
this.injectCreatedByToRecord(createdBy, datum);
}
} else {
this.injectCreatedByToRecord(createdBy, clonedRecords);
for (const record of clonedRecords) {
this.injectActorToRecord(actorMetadata, record, fieldName);
}
return clonedRecords;
}
private injectCreatedByToRecord(
createdBy: ActorMetadata,
record: CreateInput,
private injectActorToRecord(
actorMetadata: ActorMetadata,
record: RecordInput,
fieldName: 'createdBy' | 'updatedBy',
) {
// Front-end can fill the source field
if (createdBy && (!record.createdBy || !record.createdBy?.name)) {
record.createdBy = {
...createdBy,
source: record.createdBy?.source ?? createdBy.source,
};
const existingValue = record[fieldName] as ActorMetadata | undefined;
if (fieldName === 'createdBy') {
if (actorMetadata && (!existingValue || !existingValue.name)) {
record[fieldName] = {
...actorMetadata,
source: existingValue?.source ?? actorMetadata.source,
};
}
} else {
record[fieldName] = actorMetadata;
}
}
private async buildCreatedBy(
private async buildActorMetadata(
authContext: AuthContext,
): Promise<ActorMetadata> {
const { workspace, user, apiKey, application } = authContext;
@@ -151,7 +207,7 @@ export class CreatedByFromAuthContextService {
}
throw new Error(
'Unable to build createdBy metadata - no valid actor information found in auth context',
'Unable to build actor metadata - no valid actor information found in auth context',
);
}
}
@@ -124,14 +124,17 @@ export class CreateRecordService {
flatFieldMetadataMaps,
);
const actorMetadata = params.createdBy ?? {
source: FieldActorSource.WORKFLOW,
name: 'Workflow',
};
const insertResult = await repository.insert(
{
...transformedObjectRecord,
position,
createdBy: params.createdBy ?? {
source: FieldActorSource.WORKFLOW,
name: 'Workflow',
},
createdBy: actorMetadata,
updatedBy: actorMetadata,
},
undefined,
selectedColumns,
@@ -1,6 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import deepEqual from 'deep-equal';
import { FieldActorSource } from 'twenty-shared/types';
import { isDefined, isValidUuid } from 'twenty-shared/utils';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
@@ -34,6 +35,7 @@ export class UpdateRecordService {
fieldsToUpdate,
workspaceId,
rolePermissionConfig,
updatedBy,
} = params;
if (!workspaceId) {
@@ -161,6 +163,12 @@ export class UpdateRecordService {
objectRecordId,
{
...transformedObjectRecord,
updatedBy: updatedBy ?? {
source: FieldActorSource.WORKFLOW,
name: 'Workflow',
workspaceMemberId: null,
context: {},
},
},
undefined,
selectedColumns,
@@ -0,0 +1,7 @@
import { type ActorMetadata } from 'twenty-shared/types';
import { type RecordCrudExecutionContext } from './record-crud-execution-context.type';
export type CreateRecordExecutionContext = RecordCrudExecutionContext & {
createdBy?: ActorMetadata;
};
@@ -1,7 +1,5 @@
import {
type CreateRecordExecutionContext,
type RecordCrudExecutionContext,
} from './execution-context.type';
import { type CreateRecordExecutionContext } from './create-record-execution-context.type';
import { type RecordCrudExecutionContext } from './record-crud-execution-context.type';
import { type CreateRecordInput } from './record-crud-input.type';
export type CreateRecordParams = CreateRecordInput &
@@ -1,4 +1,4 @@
import { type RecordCrudExecutionContext } from './execution-context.type';
import { type RecordCrudExecutionContext } from './record-crud-execution-context.type';
import { type DeleteRecordInput } from './record-crud-input.type';
export type DeleteRecordParams = DeleteRecordInput &
@@ -3,7 +3,7 @@ import {
type ObjectRecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type RecordCrudExecutionContext } from './execution-context.type';
import { type RecordCrudExecutionContext } from './record-crud-execution-context.type';
import { type FindRecordsInput } from './record-crud-input.type';
export type FindRecordsParams = FindRecordsInput &
@@ -1,12 +1,6 @@
import { type ActorMetadata } from 'twenty-shared/types';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
export type RecordCrudExecutionContext = {
workspaceId: string;
rolePermissionConfig?: RolePermissionConfig;
};
export type CreateRecordExecutionContext = RecordCrudExecutionContext & {
createdBy?: ActorMetadata;
};
@@ -0,0 +1,7 @@
import { type ActorMetadata } from 'twenty-shared/types';
import { type RecordCrudExecutionContext } from './record-crud-execution-context.type';
export type UpdateRecordExecutionContext = RecordCrudExecutionContext & {
updatedBy?: ActorMetadata;
};
@@ -1,4 +1,5 @@
import { type RecordCrudExecutionContext } from './execution-context.type';
import { type UpdateRecordInput } from './record-crud-input.type';
import { type UpdateRecordExecutionContext } from './update-record-execution-context.type';
export type UpdateRecordParams = UpdateRecordInput & RecordCrudExecutionContext;
export type UpdateRecordParams = UpdateRecordInput &
UpdateRecordExecutionContext;
@@ -1,4 +1,4 @@
import { type RecordCrudExecutionContext } from './execution-context.type';
import { type RecordCrudExecutionContext } from './record-crud-execution-context.type';
import { type UpsertRecordInput } from './record-crud-input.type';
export type UpsertRecordParams = UpsertRecordInput & RecordCrudExecutionContext;
@@ -244,6 +244,42 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
applicationId: applicationId ?? null,
};
const updatedByFieldId = v4();
const updatedByField: FlatFieldMetadata<FieldMetadataType.ACTOR> = {
type: FieldMetadataType.ACTOR,
id: updatedByFieldId,
viewFieldIds: [],
mainGroupByFieldMetadataViewIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
universalIdentifier: updatedByFieldId,
workspaceId,
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.updatedBy,
name: 'updatedBy',
label: 'Updated by',
icon: 'IconUserCircle',
description: 'The workspace member who last updated the record',
isNullable: false,
isActive: true,
isCustom: false,
isSystem: false,
isUIReadOnly: true,
defaultValue: { name: "''", source: "'MANUAL'" },
viewFilterIds: [],
createdAt,
updatedAt: createdAt,
options: null,
standardOverrides: null,
relationTargetFieldMetadataId: null,
relationTargetObjectMetadataId: null,
settings: null,
morphId: null,
applicationId: applicationId ?? null,
};
const positionFieldId = v4();
const positionField: FlatFieldMetadata<FieldMetadataType.POSITION> = {
type: FieldMetadataType.POSITION,
@@ -327,6 +363,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
nameField,
createdAtField,
updatedAtField,
updatedByField,
deletedAtField,
createdByField,
positionField,
@@ -69,6 +69,16 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
@WorkspaceField({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.updatedBy,
type: FieldMetadataType.ACTOR,
label: msg`Updated by`,
icon: 'IconUserCircle',
description: msg`The workspace member who last updated the record`,
})
@WorkspaceIsFieldUIReadOnly()
updatedBy: ActorMetadata;
@WorkspaceRelation({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.noteTargets,
label: msg`Notes`,
@@ -2,6 +2,7 @@ import { isPlainObject } from '@nestjs/common/utils/shared.utils';
import { isNull } from '@sniptt/guards';
import {
FieldActorSource,
FieldMetadataType,
compositeTypeDefinitions,
} from 'twenty-shared/types';
@@ -157,6 +158,13 @@ export function formatResult<T>(
: value;
}
// After assembling composite fields, handle those with missing required subfields
handleEmptyCompositeFields(
newData,
flatObjectMetadata,
flatFieldMetadataMaps,
);
const fieldMetadataItemsOfTypeDateOnly = getFlatFieldsFromFlatObjectMetadata(
flatObjectMetadata,
flatFieldMetadataMaps,
@@ -250,3 +258,85 @@ function transformCompositeFieldNullValue(
] ?? value
);
}
/**
* Handles composite fields with missing required subfields.
* - For nullable fields: sets to null if all required subfields are null
* - For non-nullable fields: provides a default value to prevent GraphQL errors
*
* This handles existing records that were created before the field was added
* or records with incomplete data.
*/
function handleEmptyCompositeFields(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: Record<string, any>,
flatObjectMetadata: FlatObjectMetadata,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) {
const compositeFieldMetadataCollection = getCompositeFieldMetadataCollection(
flatObjectMetadata,
flatFieldMetadataMaps,
);
for (const fieldMetadata of compositeFieldMetadataCollection) {
const fieldValue = data[fieldMetadata.name];
if (!isDefined(fieldValue) || !isPlainObject(fieldValue)) {
continue;
}
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
if (!compositeType) {
continue;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const typedFieldValue = fieldValue as Record<string, any>;
// Check if all required properties are null/undefined
const requiredProperties = compositeType.properties.filter(
(prop) => prop.isRequired,
);
const allRequiredPropertiesAreNull = requiredProperties.every(
(prop) =>
!isDefined(typedFieldValue[prop.name]) ||
isNull(typedFieldValue[prop.name]),
);
if (allRequiredPropertiesAreNull && requiredProperties.length > 0) {
if (fieldMetadata.isNullable) {
// Field is nullable, set to null
data[fieldMetadata.name] = null;
} else {
// Field is non-nullable, provide a default value
data[fieldMetadata.name] = getDefaultCompositeFieldValue(
fieldMetadata.type,
);
}
}
}
}
/**
* Returns a default value for non-nullable composite fields.
*/
function getDefaultCompositeFieldValue(
fieldType: FieldMetadataType,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): Record<string, any> | null {
switch (fieldType) {
case FieldMetadataType.ACTOR:
return {
source: FieldActorSource.MANUAL,
name: '',
workspaceMemberId: null,
context: {},
};
default:
// For other composite types, return null and let GraphQL handle the error
// This should be extended as needed for other non-nullable composite fields
return null;
}
}
@@ -15,6 +15,9 @@ type AttachmentDataSeed = {
createdBySource: string;
createdByWorkspaceMemberId: string;
createdByName: string;
updatedBySource: string;
updatedByWorkspaceMemberId: string;
updatedByName: string;
personId: string | null;
companyId: string | null;
noteId: string | null;
@@ -30,6 +33,9 @@ export const ATTACHMENT_DATA_SEED_COLUMNS: (keyof AttachmentDataSeed)[] = [
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
'personId',
'companyId',
'noteId',
@@ -251,6 +257,9 @@ const GENERATE_ATTACHMENT_SEEDS = (): AttachmentDataSeed[] => {
createdBySource: FieldActorSource.MANUAL,
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
createdByName: 'Tim A',
updatedBySource: FieldActorSource.MANUAL,
updatedByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
updatedByName: 'Tim A',
personId,
companyId,
noteId,
@@ -11,6 +11,9 @@ type CompanyDataSeed = {
createdByWorkspaceMemberId: string;
createdByName: string;
accountOwnerId: string;
updatedBySource: string;
updatedByWorkspaceMemberId: string;
updatedByName: string;
position: number;
};
@@ -25,6 +28,9 @@ export const COMPANY_DATA_SEED_COLUMNS: (keyof CompanyDataSeed)[] = [
'createdByWorkspaceMemberId',
'createdByName',
'accountOwnerId',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
'position',
];
@@ -8524,6 +8530,9 @@ const COMPANY_DATA_SEEDS_RAW = [
export const COMPANY_DATA_SEEDS: CompanyDataSeed[] = COMPANY_DATA_SEEDS_RAW.map(
(company, index) => ({
...company,
updatedBySource: company.createdBySource,
updatedByWorkspaceMemberId: company.createdByWorkspaceMemberId,
updatedByName: company.createdByName,
position: index + 1,
}),
);
@@ -9,6 +9,9 @@ type DashboardDataSeed = {
createdBySource: string;
createdByWorkspaceMemberId: string;
createdByName: string;
updatedBySource: string;
updatedByWorkspaceMemberId: string;
updatedByName: string;
position: number;
};
@@ -19,6 +22,9 @@ export const DASHBOARD_DATA_SEED_COLUMNS: (keyof DashboardDataSeed)[] = [
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
'position',
];
@@ -41,6 +47,9 @@ export const getDashboardDataSeeds = (
createdBySource: 'MANUAL',
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
createdByName: 'Tim Apple',
updatedBySource: 'MANUAL',
updatedByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
updatedByName: 'Tim Apple',
position: 0,
},
{
@@ -53,6 +62,9 @@ export const getDashboardDataSeeds = (
createdBySource: 'MANUAL',
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
createdByName: 'Jony Ive',
updatedBySource: 'MANUAL',
updatedByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
updatedByName: 'Jony Ive',
position: 1,
},
{
@@ -62,6 +74,9 @@ export const getDashboardDataSeeds = (
createdBySource: 'MANUAL',
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.PHIL,
createdByName: 'Phil Schiller',
updatedBySource: 'MANUAL',
updatedByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.PHIL,
updatedByName: 'Phil Schiller',
position: 2,
},
];
@@ -10,6 +10,9 @@ type NoteDataSeed = {
createdByWorkspaceMemberId: string;
createdByName: string;
createdByContext: string | null;
updatedBySource: string;
updatedByWorkspaceMemberId: string;
updatedByName: string;
};
export const NOTE_DATA_SEED_COLUMNS: (keyof NoteDataSeed)[] = [
@@ -22,6 +25,9 @@ export const NOTE_DATA_SEED_COLUMNS: (keyof NoteDataSeed)[] = [
'createdByWorkspaceMemberId',
'createdByName',
'createdByContext',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
];
const GENERATE_NOTE_IDS = (): Record<string, string> => {
@@ -161,6 +167,9 @@ const GENERATE_NOTE_SEEDS = (): NoteDataSeed[] => {
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
createdByName: 'Tim A',
createdByContext: null,
updatedBySource: 'MANUAL',
updatedByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
updatedByName: 'Tim A',
});
}
@@ -191,6 +200,9 @@ const GENERATE_NOTE_SEEDS = (): NoteDataSeed[] => {
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
createdByName: 'Tim A',
createdByContext: null,
updatedBySource: 'MANUAL',
updatedByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
updatedByName: 'Tim A',
});
}
@@ -15,6 +15,9 @@ type OpportunityDataSeed = {
createdBySource: string;
createdByWorkspaceMemberId: string;
createdByName: string;
updatedBySource: string;
updatedByWorkspaceMemberId: string;
updatedByName: string;
};
export const OPPORTUNITY_DATA_SEED_COLUMNS: (keyof OpportunityDataSeed)[] = [
@@ -30,6 +33,9 @@ export const OPPORTUNITY_DATA_SEED_COLUMNS: (keyof OpportunityDataSeed)[] = [
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
];
const GENERATE_OPPORTUNITY_IDS = (): Record<string, string> => {
@@ -188,6 +194,9 @@ const GENERATE_OPPORTUNITY_SEEDS = (): OpportunityDataSeed[] => {
createdBySource: 'MANUAL',
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
createdByName: 'Tim Cook',
updatedBySource: 'MANUAL',
updatedByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
updatedByName: 'Tim Cook',
});
}
@@ -17,6 +17,9 @@ type PersonDataSeed = {
phonesPrimaryPhoneNumber: string;
phonesPrimaryPhoneCountryCode: string;
phonesPrimaryPhoneCallingCode: string;
updatedBySource: string;
updatedByWorkspaceMemberId: string;
updatedByName: string;
position: number;
};
@@ -36,6 +39,9 @@ export const PERSON_DATA_SEED_COLUMNS: (keyof PersonDataSeed)[] = [
'phonesPrimaryPhoneNumber',
'phonesPrimaryPhoneCountryCode',
'phonesPrimaryPhoneCallingCode',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
'position',
];
@@ -21650,6 +21656,9 @@ const PERSON_DATA_SEEDS_RAW = [
export const PERSON_DATA_SEEDS: PersonDataSeed[] = PERSON_DATA_SEEDS_RAW.map(
(person, index) => ({
...person,
updatedBySource: person.createdBySource,
updatedByWorkspaceMemberId: person.createdByWorkspaceMemberId,
updatedByName: person.createdByName,
position: index + 1,
}),
);
@@ -12,6 +12,9 @@ type TaskDataSeed = {
createdBySource: string;
createdByWorkspaceMemberId: string;
createdByName: string;
updatedBySource: string;
updatedByWorkspaceMemberId: string;
updatedByName: string;
};
export const TASK_DATA_SEED_COLUMNS: (keyof TaskDataSeed)[] = [
@@ -26,6 +29,9 @@ export const TASK_DATA_SEED_COLUMNS: (keyof TaskDataSeed)[] = [
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
];
// Generate all task IDs
@@ -210,6 +216,9 @@ const GENERATE_TASK_SEEDS = (): TaskDataSeed[] => {
createdBySource: 'MANUAL',
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
createdByName: 'Tim A',
updatedBySource: 'MANUAL',
updatedByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
updatedByName: 'Tim A',
});
}
@@ -242,6 +251,9 @@ const GENERATE_TASK_SEEDS = (): TaskDataSeed[] => {
createdBySource: 'MANUAL',
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
createdByName: 'Tim A',
updatedBySource: 'MANUAL',
updatedByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
updatedByName: 'Tim A',
});
}
@@ -29,6 +29,9 @@ export const prefillCompanies = async (
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
])
.orIgnore()
.values([
@@ -47,6 +50,9 @@ export const prefillCompanies = async (
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
{
id: ANTHROPIC_ID,
@@ -63,6 +69,9 @@ export const prefillCompanies = async (
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
{
id: STRIPE_ID,
@@ -79,6 +88,9 @@ export const prefillCompanies = async (
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
{
id: FIGMA_ID,
@@ -95,6 +107,9 @@ export const prefillCompanies = async (
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
{
id: NOTION_ID,
@@ -111,6 +126,9 @@ export const prefillCompanies = async (
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
])
.returning('*')
@@ -26,6 +26,9 @@ export const prefillPeople = async (
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
'phonesPrimaryPhoneNumber',
'phonesPrimaryPhoneCallingCode',
'companyId',
@@ -43,6 +46,9 @@ export const prefillPeople = async (
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
phonesPrimaryPhoneNumber: '123456789',
phonesPrimaryPhoneCallingCode: '+1',
companyId: AIRBNB_ID,
@@ -58,6 +64,9 @@ export const prefillPeople = async (
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
phonesPrimaryPhoneNumber: '555123456',
phonesPrimaryPhoneCallingCode: '+1',
companyId: ANTHROPIC_ID,
@@ -73,6 +82,9 @@ export const prefillPeople = async (
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
phonesPrimaryPhoneNumber: '987625341',
phonesPrimaryPhoneCallingCode: '+1',
companyId: STRIPE_ID,
@@ -88,6 +100,9 @@ export const prefillPeople = async (
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
phonesPrimaryPhoneNumber: '098822619',
phonesPrimaryPhoneCallingCode: '+1',
companyId: FIGMA_ID,
@@ -103,6 +118,9 @@ export const prefillPeople = async (
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
phonesPrimaryPhoneNumber: '882261739',
phonesPrimaryPhoneCallingCode: '+1',
companyId: NOTION_ID,
@@ -54,6 +54,9 @@ export const prefillWorkflows = async (
'createdByWorkspaceMemberId',
'createdByName',
'createdByContext',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
])
.orIgnore()
.values([
@@ -67,6 +70,9 @@ export const prefillWorkflows = async (
createdByWorkspaceMemberId: null,
createdByName: 'System',
createdByContext: {},
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
])
.returning('*')
@@ -54,6 +54,9 @@ export const STANDARD_OBJECTS = {
createdBy: {
universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.createdBy,
},
updatedBy: {
universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.updatedBy,
},
task: { universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.task },
note: { universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.note },
person: { universalIdentifier: ATTACHMENT_STANDARD_FIELD_IDS.person },
@@ -397,6 +400,7 @@ export const STANDARD_OBJECTS = {
},
position: { universalIdentifier: COMPANY_STANDARD_FIELD_IDS.position },
createdBy: { universalIdentifier: COMPANY_STANDARD_FIELD_IDS.createdBy },
updatedBy: { universalIdentifier: COMPANY_STANDARD_FIELD_IDS.updatedBy },
people: { universalIdentifier: COMPANY_STANDARD_FIELD_IDS.people },
accountOwner: {
universalIdentifier: COMPANY_STANDARD_FIELD_IDS.accountOwner,
@@ -549,6 +553,9 @@ export const STANDARD_OBJECTS = {
createdBy: {
universalIdentifier: DASHBOARD_STANDARD_FIELD_IDS.createdBy,
},
updatedBy: {
universalIdentifier: DASHBOARD_STANDARD_FIELD_IDS.updatedBy,
},
timelineActivities: {
universalIdentifier: DASHBOARD_STANDARD_FIELD_IDS.timelineActivities,
},
@@ -1029,6 +1036,7 @@ export const STANDARD_OBJECTS = {
title: { universalIdentifier: NOTE_STANDARD_FIELD_IDS.title },
bodyV2: { universalIdentifier: NOTE_STANDARD_FIELD_IDS.bodyV2 },
createdBy: { universalIdentifier: NOTE_STANDARD_FIELD_IDS.createdBy },
updatedBy: { universalIdentifier: NOTE_STANDARD_FIELD_IDS.updatedBy },
noteTargets: { universalIdentifier: NOTE_STANDARD_FIELD_IDS.noteTargets },
attachments: { universalIdentifier: NOTE_STANDARD_FIELD_IDS.attachments },
timelineActivities: {
@@ -1127,6 +1135,9 @@ export const STANDARD_OBJECTS = {
createdBy: {
universalIdentifier: OPPORTUNITY_STANDARD_FIELD_IDS.createdBy,
},
updatedBy: {
universalIdentifier: OPPORTUNITY_STANDARD_FIELD_IDS.updatedBy,
},
pointOfContact: {
universalIdentifier: OPPORTUNITY_STANDARD_FIELD_IDS.pointOfContact,
},
@@ -1255,6 +1266,7 @@ export const STANDARD_OBJECTS = {
avatarUrl: { universalIdentifier: PERSON_STANDARD_FIELD_IDS.avatarUrl },
position: { universalIdentifier: PERSON_STANDARD_FIELD_IDS.position },
createdBy: { universalIdentifier: PERSON_STANDARD_FIELD_IDS.createdBy },
updatedBy: { universalIdentifier: PERSON_STANDARD_FIELD_IDS.updatedBy },
company: { universalIdentifier: PERSON_STANDARD_FIELD_IDS.company },
pointOfContactForOpportunities: {
universalIdentifier:
@@ -1352,6 +1364,7 @@ export const STANDARD_OBJECTS = {
dueAt: { universalIdentifier: TASK_STANDARD_FIELD_IDS.dueAt },
status: { universalIdentifier: TASK_STANDARD_FIELD_IDS.status },
createdBy: { universalIdentifier: TASK_STANDARD_FIELD_IDS.createdBy },
updatedBy: { universalIdentifier: TASK_STANDARD_FIELD_IDS.updatedBy },
taskTargets: { universalIdentifier: TASK_STANDARD_FIELD_IDS.taskTargets },
attachments: { universalIdentifier: TASK_STANDARD_FIELD_IDS.attachments },
assignee: { universalIdentifier: TASK_STANDARD_FIELD_IDS.assignee },
@@ -1670,6 +1683,7 @@ export const STANDARD_OBJECTS = {
universalIdentifier: WORKFLOW_STANDARD_FIELD_IDS.attachments,
},
createdBy: { universalIdentifier: WORKFLOW_STANDARD_FIELD_IDS.createdBy },
updatedBy: { universalIdentifier: WORKFLOW_STANDARD_FIELD_IDS.updatedBy },
searchVector: {
universalIdentifier: WORKFLOW_STANDARD_FIELD_IDS.searchVector,
},
@@ -1772,6 +1786,9 @@ export const STANDARD_OBJECTS = {
createdBy: {
universalIdentifier: WORKFLOW_RUN_STANDARD_FIELD_IDS.createdBy,
},
updatedBy: {
universalIdentifier: WORKFLOW_RUN_STANDARD_FIELD_IDS.updatedBy,
},
output: { universalIdentifier: WORKFLOW_RUN_STANDARD_FIELD_IDS.output },
context: { universalIdentifier: WORKFLOW_RUN_STANDARD_FIELD_IDS.context },
state: { universalIdentifier: WORKFLOW_RUN_STANDARD_FIELD_IDS.state },
@@ -207,6 +207,28 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
updatedBy: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'updatedBy',
type: FieldMetadataType.ACTOR,
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isUIReadOnly: true,
isNullable: false,
defaultValue: {
source: "'MANUAL'",
name: "'System'",
workspaceMemberId: null,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
// Relation fields
task: createStandardRelationFieldFlatMetadata({
@@ -286,6 +286,28 @@ export const buildCompanyStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
updatedBy: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'updatedBy',
type: FieldMetadataType.ACTOR,
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isUIReadOnly: true,
isNullable: false,
defaultValue: {
source: "'MANUAL'",
name: "'System'",
workspaceMemberId: null,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
searchVector: createStandardFieldFlatMetadata({
objectName,
workspaceId,
@@ -176,6 +176,28 @@ export const buildDashboardStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
updatedBy: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'updatedBy',
type: FieldMetadataType.ACTOR,
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isUIReadOnly: true,
isNullable: false,
defaultValue: {
source: "'MANUAL'",
name: "'System'",
workspaceMemberId: null,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
searchVector: createStandardFieldFlatMetadata({
objectName,
workspaceId,
@@ -181,6 +181,28 @@ export const buildNoteStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
updatedBy: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'updatedBy',
type: FieldMetadataType.ACTOR,
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isUIReadOnly: true,
isNullable: false,
defaultValue: {
source: "'MANUAL'",
name: "'System'",
workspaceMemberId: null,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
searchVector: createStandardFieldFlatMetadata({
objectName,
workspaceId,
@@ -232,6 +232,28 @@ export const buildOpportunityStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
updatedBy: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'updatedBy',
type: FieldMetadataType.ACTOR,
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isUIReadOnly: true,
isNullable: false,
defaultValue: {
source: "'MANUAL'",
name: "'System'",
workspaceMemberId: null,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
searchVector: createStandardFieldFlatMetadata({
objectName,
workspaceId,
@@ -286,6 +286,28 @@ export const buildPersonStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
updatedBy: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'updatedBy',
type: FieldMetadataType.ACTOR,
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isUIReadOnly: true,
isNullable: false,
defaultValue: {
source: "'MANUAL'",
name: "'System'",
workspaceMemberId: null,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
// Relation fields
company: createStandardRelationFieldFlatMetadata({
@@ -225,6 +225,28 @@ export const buildTaskStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
updatedBy: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'updatedBy',
type: FieldMetadataType.ACTOR,
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isUIReadOnly: true,
isNullable: false,
defaultValue: {
source: "'MANUAL'",
name: "'System'",
workspaceMemberId: null,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
searchVector: createStandardFieldFlatMetadata({
objectName,
workspaceId,
@@ -223,6 +223,28 @@ export const buildWorkflowRunStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
updatedBy: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'updatedBy',
type: FieldMetadataType.ACTOR,
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isUIReadOnly: true,
isNullable: false,
defaultValue: {
source: "'MANUAL'",
name: "'System'",
workspaceMemberId: null,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
state: createStandardFieldFlatMetadata({
objectName,
workspaceId,
@@ -200,6 +200,28 @@ export const buildWorkflowStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
updatedBy: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'updatedBy',
type: FieldMetadataType.ACTOR,
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isUIReadOnly: true,
isNullable: false,
defaultValue: {
source: "'MANUAL'",
name: "'System'",
workspaceMemberId: null,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
searchVector: createStandardFieldFlatMetadata({
objectName,
workspaceId,
@@ -35,6 +35,7 @@ export const ATTACHMENT_STANDARD_FIELD_IDS = {
type: '20202020-a417-49b8-a40b-f6a7874caa0d',
fileCategory: '20202020-8c3f-4d9e-9a1b-2e5f7a8c9d0e',
createdBy: '395be3bd-a5c9-463d-aafe-9bc3bbec3f15',
updatedBy: '376239d1-3e65-4cb6-b5d8-e0917d43cc93',
author: '20202020-6501-4ac5-a4ef-b2f8522ef6cd',
activity: '20202020-b569-481b-a13f-9b94e47e54fe',
task: '20202020-51e5-4621-9cf8-215487951c4b',
@@ -128,6 +129,7 @@ export const COMPANY_STANDARD_FIELD_IDS = {
idealCustomerProfile: '20202020-ba6b-438a-8213-2c5ba28d76a2',
position: '20202020-9b4e-462b-991d-a0ee33326454',
createdBy: '20202020-fabc-451d-ab7d-412170916baa',
updatedBy: '7444022e-b38f-4d4f-801b-cd664abc4834',
people: '20202020-3213-4ddf-9494-6422bcff8d7c',
accountOwner: '20202020-95b8-4e10-9881-edb5d4765f9d',
// TODO: check if activityTargets field can be deleted
@@ -280,6 +282,7 @@ export const NOTE_STANDARD_FIELD_IDS = {
body: '20202020-e63d-4e70-95be-a78cd9abe7ef',
bodyV2: '20202020-a7bb-4d94-be51-8f25181502c8',
createdBy: '20202020-0d79-4e21-ab77-5a394eff97be',
updatedBy: '9b446e89-2484-4044-a3b5-420f6b578c0c',
noteTargets: '20202020-1f25-43fe-8b00-af212fdde823',
attachments: '20202020-4986-4c92-bf19-39934b149b16',
timelineActivities: '20202020-7030-42f8-929c-1a57b25d6bce',
@@ -303,6 +306,7 @@ export const OPPORTUNITY_STANDARD_FIELD_IDS = {
stage: '20202020-6f76-477d-8551-28cd65b2b4b9',
position: '20202020-806d-493a-bbc6-6313e62958e2',
createdBy: '20202020-a63e-4a62-8e63-42a51828f831',
updatedBy: '3c8a6095-3f64-4e81-a59e-66c2bd181e11',
pointOfContact: '20202020-8dfb-42fc-92b6-01afb759ed16',
company: '20202020-cbac-457e-b565-adece5fc815f',
favorites: '20202020-a1c2-4500-aaae-83ba8a0e827a',
@@ -328,6 +332,7 @@ export const PERSON_STANDARD_FIELD_IDS = {
avatarUrl: '20202020-b8a6-40df-961c-373dc5d2ec21',
position: '20202020-fcd5-4231-aff5-fff583eaa0b1',
createdBy: '20202020-f6ab-4d98-af24-a3d5b664148a',
updatedBy: 'e9e0dd35-184c-4742-84da-afadf45ce59a',
company: '20202020-e2f3-448e-b34c-2d625f0025fd',
pointOfContactForOpportunities: '20202020-911b-4a7d-b67b-918aa9a5b33a',
// TODO: check if activityTargets field can be deleted
@@ -350,6 +355,7 @@ export const TASK_STANDARD_FIELD_IDS = {
dueAt: '20202020-fd99-40da-951b-4cb9a352fce3',
status: '20202020-70bc-48f9-89c5-6aa730b151e0',
createdBy: '20202020-1a04-48ab-a567-576965ae5387',
updatedBy: '9e8bf518-f4ab-433e-9674-efb75fba2802',
taskTargets: '20202020-de9c-4d0e-a452-713d4a3e5fc7',
attachments: '20202020-794d-4783-a8ff-cecdb15be139',
assignee: '20202020-065a-4f42-a906-e20422c1753f',
@@ -449,6 +455,7 @@ export const WORKFLOW_STANDARD_FIELD_IDS = {
timelineActivities: '20202020-906e-486a-a798-131a5f081faf',
attachments: '20202020-4a8c-4e2d-9b1c-7e5f3a2b4c6d',
createdBy: '20202020-6007-401a-8aa5-e6f48581a6f3',
updatedBy: '3559831e-caf2-4eb5-9db1-b47bf968c774',
searchVector: '20202020-535d-4ffa-b7f3-4fa0d5da1b7a',
} as const;
@@ -462,6 +469,7 @@ export const WORKFLOW_RUN_STANDARD_FIELD_IDS = {
status: '20202020-6b3e-4f9c-8c2b-2e5b8e6d6f3b',
position: '20202020-7802-4c40-ae89-1f506fe3365c',
createdBy: '20202020-6007-401a-8aa5-e6f38581a6f3',
updatedBy: '730dc1c9-34f5-4c22-84a6-bcb55b7604e2',
output: '20202020-7be4-4db2-8ac6-3ff0d740843d',
context: '20202020-189c-478a-b867-d72feaf5926a',
state: '20202020-611f-45f3-9cde-d64927e8ec57',
@@ -516,6 +524,7 @@ export const CUSTOM_OBJECT_STANDARD_FIELD_IDS = {
name: '20202020-ba07-4ffd-ba63-009491f5749c',
position: '20202020-c2bd-4e16-bb9a-c8b0411bf49d',
createdBy: '20202020-be0e-4971-865b-32ca87cbb315',
updatedBy: 'd047ccb4-9469-4514-a982-29c4ae49317d',
// TODO: check if activityTargets field can be deleted
activityTargets: '20202020-7f42-40ae-b96c-c8a61acc83bf',
noteTargets: '20202020-01fd-4f37-99dc-9427a444018a',
@@ -531,6 +540,7 @@ export const DASHBOARD_STANDARD_FIELD_IDS = {
position: '20202020-38af-409b-95f0-7f08aa5f420f',
pageLayoutId: '20202020-bb53-4648-aa36-1d9d54e6f7f2',
createdBy: '20202020-ff32-4fa1-b7ad-407cc6aa0734',
updatedBy: '53ee42e7-f157-42b5-b278-a5fa9b378307',
timelineActivities: '20202020-9b0c-5d6e-7f8a-9b0c1d2e3f4a',
favorites: '20202020-f032-478f-88fa-6426ff6f1e4c',
attachments: '20202020-bf6f-4220-8c55-2764f1175870',
@@ -148,6 +148,16 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
@WorkspaceField({
standardId: ATTACHMENT_STANDARD_FIELD_IDS.updatedBy,
type: FieldMetadataType.ACTOR,
label: msg`Updated by`,
icon: 'IconUserCircle',
description: msg`The workspace member who last updated the record`,
})
@WorkspaceIsFieldUIReadOnly()
updatedBy: ActorMetadata;
@WorkspaceRelation({
standardId: ATTACHMENT_STANDARD_FIELD_IDS.author,
type: RelationType.MANY_TO_ONE,
@@ -168,6 +168,16 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
@WorkspaceField({
standardId: COMPANY_STANDARD_FIELD_IDS.updatedBy,
type: FieldMetadataType.ACTOR,
label: msg`Updated by`,
icon: 'IconUserCircle',
description: msg`The workspace member who last updated the record`,
})
@WorkspaceIsFieldUIReadOnly()
updatedBy: ActorMetadata;
// Relations
@WorkspaceRelation({
standardId: COMPANY_STANDARD_FIELD_IDS.people,
@@ -91,6 +91,16 @@ export class DashboardWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
@WorkspaceField({
standardId: DASHBOARD_STANDARD_FIELD_IDS.updatedBy,
type: FieldMetadataType.ACTOR,
label: msg`Updated by`,
icon: 'IconUserCircle',
description: msg`The workspace member who last updated the record`,
})
@WorkspaceIsFieldUIReadOnly()
updatedBy: ActorMetadata;
@WorkspaceRelation({
standardId: DASHBOARD_STANDARD_FIELD_IDS.timelineActivities,
type: RelationType.ONE_TO_MANY,
@@ -94,6 +94,16 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
@WorkspaceField({
standardId: NOTE_STANDARD_FIELD_IDS.updatedBy,
type: FieldMetadataType.ACTOR,
label: msg`Updated by`,
icon: 'IconUserCircle',
description: msg`The workspace member who last updated the record`,
})
@WorkspaceIsFieldUIReadOnly()
updatedBy: ActorMetadata;
@WorkspaceRelation({
standardId: NOTE_STANDARD_FIELD_IDS.noteTargets,
label: msg`Relations`,
@@ -130,6 +130,16 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
@WorkspaceField({
standardId: OPPORTUNITY_STANDARD_FIELD_IDS.updatedBy,
type: FieldMetadataType.ACTOR,
label: msg`Updated by`,
icon: 'IconUserCircle',
description: msg`The workspace member who last updated the record`,
})
@WorkspaceIsFieldUIReadOnly()
updatedBy: ActorMetadata;
@WorkspaceRelation({
standardId: OPPORTUNITY_STANDARD_FIELD_IDS.pointOfContact,
type: RelationType.MANY_TO_ONE,
@@ -195,6 +195,16 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
@WorkspaceField({
standardId: PERSON_STANDARD_FIELD_IDS.updatedBy,
type: FieldMetadataType.ACTOR,
label: msg`Updated by`,
icon: 'IconUserCircle',
description: msg`The workspace member who last updated the record`,
})
@WorkspaceIsFieldUIReadOnly()
updatedBy: ActorMetadata;
// Relations
@WorkspaceRelation({
standardId: PERSON_STANDARD_FIELD_IDS.company,
@@ -133,6 +133,16 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
@WorkspaceField({
standardId: TASK_STANDARD_FIELD_IDS.updatedBy,
type: FieldMetadataType.ACTOR,
label: msg`Updated by`,
icon: 'IconUserCircle',
description: msg`The workspace member who last updated the record`,
})
@WorkspaceIsFieldUIReadOnly()
updatedBy: ActorMetadata;
@WorkspaceRelation({
standardId: TASK_STANDARD_FIELD_IDS.taskTargets,
label: msg`Relations`,
@@ -204,6 +204,16 @@ export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
@WorkspaceField({
standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.updatedBy,
type: FieldMetadataType.ACTOR,
label: msg`Updated by`,
icon: 'IconUserCircle',
description: msg`The workspace member who last updated the record`,
})
@WorkspaceIsFieldUIReadOnly()
updatedBy: ActorMetadata;
@WorkspaceField({
standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.state,
type: FieldMetadataType.RAW_JSON,
@@ -220,4 +220,14 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity {
})
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
@WorkspaceField({
standardId: WORKFLOW_STANDARD_FIELD_IDS.updatedBy,
type: FieldMetadataType.ACTOR,
label: msg`Updated by`,
icon: 'IconUserCircle',
description: msg`The workspace member who last updated the record`,
})
@WorkspaceIsFieldUIReadOnly()
updatedBy: ActorMetadata;
}
@@ -0,0 +1,18 @@
import { type ActorMetadata, FieldActorSource } from 'twenty-shared/types';
import { type WorkflowExecutionContext } from 'src/modules/workflow/workflow-executor/types/workflow-execution-context.type';
export const buildWorkflowActorMetadata = (
executionContext: WorkflowExecutionContext,
): ActorMetadata => {
if (executionContext.isActingOnBehalfOfUser) {
return executionContext.initiator;
}
return {
source: FieldActorSource.WORKFLOW,
name: 'Workflow',
workspaceMemberId: null,
context: {},
};
};
@@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common';
import { type ActorMetadata, FieldActorSource } from 'twenty-shared/types';
import { resolveInput } from 'twenty-shared/utils';
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
@@ -14,7 +13,7 @@ import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/work
import { WorkflowExecutionContextService } from 'src/modules/workflow/workflow-executor/services/workflow-execution-context.service';
import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
import { type WorkflowExecutionContext } from 'src/modules/workflow/workflow-executor/types/workflow-execution-context.type';
import { buildWorkflowActorMetadata } from 'src/modules/workflow/workflow-executor/utils/build-workflow-actor-metadata.util';
import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util';
import { resolveRichTextFieldsInRecord } from 'src/modules/workflow/workflow-executor/utils/resolve-rich-text-fields-in-record.util';
import { type WorkflowCreateRecordActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type';
@@ -65,7 +64,7 @@ export class CreateRecordWorkflowAction implements WorkflowAction {
const executionContext =
await this.workflowExecutionContextService.getExecutionContext(runInfo);
const createdBy = this.buildCreatedByActor(executionContext);
const createdBy = buildWorkflowActorMetadata(executionContext);
const toolOutput = await this.createRecordService.execute({
objectName: workflowActionInput.objectName,
@@ -86,19 +85,4 @@ export class CreateRecordWorkflowAction implements WorkflowAction {
result: toolOutput.result,
};
}
private buildCreatedByActor(
executionContext: WorkflowExecutionContext,
): ActorMetadata {
if (executionContext.isActingOnBehalfOfUser) {
return executionContext.initiator;
}
return {
source: FieldActorSource.WORKFLOW,
name: 'Workflow',
workspaceMemberId: null,
context: {},
};
}
}
@@ -17,6 +17,7 @@ import {
import { WorkflowExecutionContextService } from 'src/modules/workflow/workflow-executor/services/workflow-execution-context.service';
import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
import { buildWorkflowActorMetadata } from 'src/modules/workflow/workflow-executor/utils/build-workflow-actor-metadata.util';
import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util';
import { resolveRichTextFieldsInRecord } from 'src/modules/workflow/workflow-executor/utils/resolve-rich-text-fields-in-record.util';
import { isWorkflowUpdateRecordAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/guards/is-workflow-update-record-action.guard';
@@ -86,12 +87,15 @@ export class UpdateRecordWorkflowAction implements WorkflowAction {
const executionContext =
await this.workflowExecutionContextService.getExecutionContext(runInfo);
const updatedBy = buildWorkflowActorMetadata(executionContext);
const toolOutput = await this.updateRecordService.execute({
objectName: workflowActionInput.objectName,
objectRecordId: workflowActionInput.objectRecordId,
objectRecord: workflowActionInput.objectRecord,
fieldsToUpdate: workflowActionInput.fieldsToUpdate,
workspaceId,
updatedBy,
rolePermissionConfig: executionContext.rolePermissionConfig,
});
@@ -73,6 +73,22 @@ exports[`Object metadata creation should fail v2 when labelPlural contains only
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -594,6 +610,27 @@ exports[`Object metadata creation should fail v2 when labelPlural contains only
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -699,6 +736,22 @@ exports[`Object metadata creation should fail v2 when labelPlural exceeds maximu
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -1220,6 +1273,27 @@ exports[`Object metadata creation should fail v2 when labelPlural exceeds maximu
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -1336,6 +1410,22 @@ exports[`Object metadata creation should fail v2 when labelSingular contains onl
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -1857,6 +1947,27 @@ exports[`Object metadata creation should fail v2 when labelSingular contains onl
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -1962,6 +2073,22 @@ exports[`Object metadata creation should fail v2 when labelSingular exceeds maxi
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -2483,6 +2610,27 @@ exports[`Object metadata creation should fail v2 when labelSingular exceeds maxi
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -2599,6 +2747,22 @@ exports[`Object metadata creation should fail v2 when labels are identical 1`] =
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -3120,6 +3284,27 @@ exports[`Object metadata creation should fail v2 when labels are identical 1`] =
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -3225,6 +3410,22 @@ exports[`Object metadata creation should fail v2 when labels with whitespaces re
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -3746,6 +3947,27 @@ exports[`Object metadata creation should fail v2 when labels with whitespaces re
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -3851,6 +4073,22 @@ exports[`Object metadata creation should fail v2 when name exceeds maximum lengt
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -4402,6 +4640,27 @@ exports[`Object metadata creation should fail v2 when name exceeds maximum lengt
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -4507,6 +4766,22 @@ exports[`Object metadata creation should fail v2 when namePlural has invalid cha
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -5028,6 +5303,27 @@ exports[`Object metadata creation should fail v2 when namePlural has invalid cha
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -5133,6 +5429,22 @@ exports[`Object metadata creation should fail v2 when namePlural is a reserved k
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -5654,6 +5966,27 @@ exports[`Object metadata creation should fail v2 when namePlural is a reserved k
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -5770,6 +6103,22 @@ exports[`Object metadata creation should fail v2 when namePlural is not camelCas
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -6297,6 +6646,27 @@ exports[`Object metadata creation should fail v2 when namePlural is not camelCas
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -6402,6 +6772,22 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -6989,6 +7375,27 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -7094,6 +7501,22 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -7669,6 +8092,27 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -7774,6 +8218,22 @@ exports[`Object metadata creation should fail v2 when nameSingular has invalid c
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -8331,6 +8791,27 @@ exports[`Object metadata creation should fail v2 when nameSingular has invalid c
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -8436,6 +8917,22 @@ exports[`Object metadata creation should fail v2 when nameSingular is a reserved
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -8981,6 +9478,27 @@ exports[`Object metadata creation should fail v2 when nameSingular is a reserved
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -9097,6 +9615,22 @@ exports[`Object metadata creation should fail v2 when nameSingular is not camelC
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -9684,6 +10218,27 @@ exports[`Object metadata creation should fail v2 when nameSingular is not camelC
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -9789,6 +10344,22 @@ exports[`Object metadata creation should fail v2 when names are identical 1`] =
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -10310,6 +10881,27 @@ exports[`Object metadata creation should fail v2 when names are identical 1`] =
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -10415,6 +11007,22 @@ exports[`Object metadata creation should fail v2 when names with whitespaces res
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "updatedBy",
"objectMetadataId": Any<String>,
},
"status": "fail",
"type": "create_field",
},
{
"errors": [
{
@@ -10936,6 +11544,27 @@ exports[`Object metadata creation should fail v2 when names with whitespaces res
"status": "fail",
"type": "create_view_field",
},
{
"errors": [
{
"code": "INVALID_VIEW_DATA",
"message": "Field metadata not found",
"userFriendlyMessage": "Field metadata not found",
},
{
"code": "INVALID_VIEW_DATA",
"message": "View not found",
"userFriendlyMessage": "View not found",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataId": Any<String>,
"id": Any<String>,
"viewId": Any<String>,
},
"status": "fail",
"type": "create_view_field",
},
],
"viewFilter": [],
"viewGroup": [],
@@ -93,6 +93,6 @@ describe('View side effect on object creation', () => {
expectToFail: false,
});
expect(createdViewFields.length).toBe(11);
expect(createdViewFields.length).toBe(12);
});
});