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
@@ -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;