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:
co-authored by
Félix Malfait
Félix Malfait
parent
0da5cf29de
commit
01e0502fcf
@@ -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 {}
|
||||
|
||||
+10
-10
@@ -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,
|
||||
),
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -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,
|
||||
|
||||
+49
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+49
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+27
-23
@@ -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"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
+87
-31
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
+7
-4
@@ -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,
|
||||
|
||||
+8
@@ -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,
|
||||
|
||||
+7
@@ -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;
|
||||
};
|
||||
+2
-4
@@ -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
-1
@@ -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 &
|
||||
|
||||
+1
-1
@@ -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 &
|
||||
|
||||
-6
@@ -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;
|
||||
};
|
||||
+7
@@ -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;
|
||||
};
|
||||
+3
-2
@@ -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
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user