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
1f283778e9. 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 09:05:17 +00:00
committed by GitHub
co-authored by Félix Malfait Félix Malfait
parent 0da5cf29de
commit 01e0502fcf
58 changed files with 1516 additions and 118 deletions
@@ -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,
});