Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 6289e11874 fix: handle null defaultRoleId in CommonApiContextBuilderService for application auth
https://sonarly.com/issue/16711?type=bug

Workflow record actions (Find, Create, Update, Delete) fail with "Invalid auth context" for workspaces upgraded from versions before the `defaultRoleId` column was added to `core.application`, because the permission resolver has no fallback for application contexts with a NULL `defaultRoleId`.

Fix: **Changed:** `CommonApiContextBuilderService.getObjectsPermissions()` in `common-api-context-builder.service.ts`

**What:** Separated the `isApplicationAuthContext` check from the `isDefined(defaultRoleId)` check. Previously, these were combined in a single `else if` condition:

```ts
} else if (
  isApplicationAuthContext(authContext) &&
  isDefined(authContext.application.defaultRoleId)
) {
```

When `isApplicationAuthContext` was true but `defaultRoleId` was null, neither this branch nor the `isUserAuthContext` branch matched, causing the code to fall through to the error: `"Invalid auth context - no authentication mechanism found"`.

**Fix:** The application auth context is now always matched first. If `defaultRoleId` is null, the method returns `{}` (empty permissions object = no restrictions). This is consistent with the `shouldBypassPermissionChecks: true` fallback already used in `WorkflowExecutionContextService.buildApplicationExecutionContext()` for the same scenario.

When `defaultRoleId` is present, normal role-based permission resolution continues as before.

**Why this approach:** The root cause is a missing backfill of `defaultRoleId` on the `core.application` table for pre-existing workspaces. While a database migration to backfill would be the deepest fix, the code must also be defensive against this state. Returning unrestricted permissions for applications without a role is the correct behavior — the Twenty Standard Application (the only application that runs workflow actions) should have full access.
2026-03-19 21:44:05 +00:00
26139ee463 fix: stop event propagation when removing file in AI chat preview (#18779)
## Summary

When clicking the ✕ button on an uploaded file in the AI chatbot context
preview, the click event bubbled up to the `StyledClickableContainer`
parent, which triggered `handleClick` (opening the file preview modal)
instead of — or in addition to — calling `onRemove`.

**Root cause:** `StyledClickableContainer` has `onClick={handleClick}`
at the div level. The `AvatarOrIcon` (X button) `onClick={onRemove}`
callback didn't stop propagation, so the event continued to bubble and
opened the preview.

**Fix:** Wrap `onRemove` in a `handleRemove` callback that calls
`e.stopPropagation()` before invoking the original handler.

Fixes #18298

---------

Co-authored-by: victorjzq <zhiqiangjia@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-19 19:07:19 +00:00
8005b35b56 Fix relation connect where failing on mixed-case email and URL (#18605)
Related to issue #17711 
Follow-up to PR #17774  which fixed the frontend link normalization only
## Summary
- Normalize `primaryEmail` (lowercase) and `primaryLinkUrl` in relation
`connect.where` composite values
- Applied in both frontend spreadsheet import and backend
`DataArgProcessorService` to cover all entry points (UI import, GraphQL
API)

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-03-19 17:56:26 +00:00
Abdul RahmanandGitHub 02bfebccc2 Fix: cascade delete favorite folder children on backend (#18765) 2026-03-19 17:55:28 +00:00
Thomas TrompetteandGitHub b86e6189c0 Remove postition from Timeline Activities + fix workflow title placeholder (#18777)
- Position were not properly displayed because we never implemented a
display for this
- Untitled placeholder was not displayed anymore
<img width="359" height="117" alt="Capture d’écran 2026-03-19 à 17 11
25"
src="https://github.com/user-attachments/assets/64c90d81-8262-4176-ae25-804748e36b1e"
/>
2026-03-19 17:25:35 +00:00
15 changed files with 444 additions and 30 deletions
@@ -72,11 +72,13 @@ export const AgentChatFilePreview = ({
);
const rightComponent = onRemove ? (
<AvatarOrIcon
Icon={IconX}
IconColor={theme.font.color.secondary}
onClick={onRemove}
/>
<div onClick={(e) => e.stopPropagation()}>
<AvatarOrIcon
Icon={IconX}
IconColor={theme.font.color.secondary}
onClick={onRemove}
/>
</div>
) : undefined;
const hasRightDivider = isDefined(onRemove);
@@ -7,6 +7,7 @@ import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFi
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -41,8 +42,8 @@ export const RecordTitleCellSingleTextDisplayMode = ({
const recordStore = useAtomFamilyStateValue(recordStoreFamilyState, recordId);
const isEmpty =
recordStore?.[fieldDefinition.metadata.fieldName]?.trim() === '';
const fieldValue = recordStore?.[fieldDefinition.metadata.fieldName];
const isEmpty = !isDefined(fieldValue) || fieldValue.trim() === '';
const { openRecordTitleCell } = useRecordTitleCell();
@@ -519,6 +519,94 @@ describe('buildRecordFromImportedStructuredRow', () => {
});
});
it('should lowercase relation email composite subfield', () => {
const importedStructuredRow: ImportedStructuredRow = {
'emailField (relationField)': 'John.Doe@Example.COM',
};
const spreadsheetImportFields = [
{
fieldMetadataItemId: '6',
isNestedField: false,
isRelationConnectField: true,
label: 'Relation Field / Email Field',
key: 'emailField (relationField)',
fieldMetadataType: FieldMetadataType.RELATION,
uniqueFieldMetadataItem: {
name: 'emailField',
type: FieldMetadataType.EMAILS,
},
compositeSubFieldKey: 'primaryEmail',
},
] as SpreadsheetImportField[];
const result = buildRecordFromImportedStructuredRow({
importedStructuredRow,
fieldMetadataItems: fields,
spreadsheetImportFields,
});
expect(result).toEqual({
relationField: {
connect: {
where: {
emailField: {
primaryEmail: 'john.doe@example.com',
},
},
},
},
createdBy: {
source: 'IMPORT',
context: {},
},
});
});
it('should normalize relation links composite subfield', () => {
const importedStructuredRow: ImportedStructuredRow = {
'domainNameField (relationField)': 'HTTPS://Example.COM/path/',
};
const spreadsheetImportFields = [
{
fieldMetadataItemId: '6',
isNestedField: false,
isRelationConnectField: true,
label: 'Relation Field / Domain Name Field',
key: 'domainNameField (relationField)',
fieldMetadataType: FieldMetadataType.RELATION,
uniqueFieldMetadataItem: {
name: 'linksField',
type: FieldMetadataType.LINKS,
},
compositeSubFieldKey: 'primaryLinkUrl',
},
] as SpreadsheetImportField[];
const result = buildRecordFromImportedStructuredRow({
importedStructuredRow,
fieldMetadataItems: fields,
spreadsheetImportFields,
});
expect(result).toEqual({
relationField: {
connect: {
where: {
linksField: {
primaryLinkUrl: 'https://example.com/path',
},
},
},
},
createdBy: {
source: 'IMPORT',
context: {},
},
});
});
it('should return empty record for empty imported row', () => {
const importedStructuredRow: ImportedStructuredRow = {};
@@ -202,7 +202,7 @@ export const buildRecordFromImportedStructuredRow = ({
},
[FieldMetadataType.EMAILS]: {
primaryEmail: castToString,
primaryEmail: (value: unknown) => castToString(value).toLowerCase(),
additionalEmails: stringArrayJSONSchema.parse,
},
[FieldMetadataType.FULL_NAME]: {
@@ -1,6 +1,6 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
import { DataArgProcessorService } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service';
import { type SystemWorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
@@ -116,6 +116,118 @@ describe('DataArgProcessorService', () => {
expect(dataArgProcessorService).toBeDefined();
});
it('should normalize relation connect where composite values', async () => {
const flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata> = {
byUniversalIdentifier: {
'company-universal-id': {
id: 'company-id',
name: 'company',
type: FieldMetadataType.RELATION,
isNullable: true,
objectMetadataId: 'object-id',
universalIdentifier: 'company-universal-id',
relationTargetObjectMetadataId: 'target-company-object-id',
settings: {
relationType: RelationType.MANY_TO_ONE,
joinColumnName: 'companyId',
},
} as FlatFieldMetadata,
'emails-universal-id': {
id: 'emails-id',
name: 'emails',
type: FieldMetadataType.EMAILS,
isNullable: true,
objectMetadataId: 'target-company-object-id',
universalIdentifier: 'emails-universal-id',
} as FlatFieldMetadata,
'domainName-universal-id': {
id: 'domainName-id',
name: 'domainName',
type: FieldMetadataType.LINKS,
isNullable: true,
objectMetadataId: 'target-company-object-id',
universalIdentifier: 'domainName-universal-id',
} as FlatFieldMetadata,
},
universalIdentifierById: {
'company-id': 'company-universal-id',
'emails-id': 'emails-universal-id',
'domainName-id': 'domainName-universal-id',
},
universalIdentifiersByApplicationId: {},
};
const flatObjectMetadata = {
id: 'object-id',
nameSingular: 'testObject',
namePlural: 'testObjects',
isCustom: false,
fieldIds: ['company-id'],
universalIdentifier: 'test-object-universal-id',
labelIdentifierFieldMetadataUniversalIdentifier: null,
imageIdentifierFieldMetadataUniversalIdentifier: null,
} as FlatObjectMetadata;
const flatObjectMetadataMaps = {
byUniversalIdentifier: {
'target-company-universal-id': {
id: 'target-company-object-id',
nameSingular: 'company',
namePlural: 'companies',
isCustom: false,
fieldIds: ['emails-id', 'domainName-id'],
universalIdentifier: 'target-company-universal-id',
labelIdentifierFieldMetadataUniversalIdentifier: null,
imageIdentifierFieldMetadataUniversalIdentifier: null,
} as FlatObjectMetadata,
},
universalIdentifierById: {
'target-company-object-id': 'target-company-universal-id',
},
universalIdentifiersByApplicationId: {},
};
const result = await dataArgProcessorService.process({
partialRecordInputs: [
{
company: {
connect: {
where: {
emails: {
primaryEmail: 'User@Example.COM',
},
domainName: {
primaryLinkUrl: 'HTTPS://Example.COM/path/',
},
},
},
},
},
],
authContext: createMockAuthContext(),
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
});
expect(result).toEqual([
{
company: {
connect: {
where: {
emails: {
primaryEmail: 'user@example.com',
},
domainName: {
primaryLinkUrl: 'https://example.com/path',
},
},
},
},
},
]);
});
describe('failing inputs validation', () => {
const fieldMetadataTypesToTest = Object.keys(
failingInputsByFieldMetadataType,
@@ -146,6 +258,11 @@ describe('DataArgProcessorService', () => {
authContext: createMockAuthContext(),
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps: {
byUniversalIdentifier: {},
universalIdentifierById: {},
universalIdentifiersByApplicationId: {},
},
}),
).rejects.toThrowErrorMatchingSnapshot();
});
@@ -183,6 +300,11 @@ describe('DataArgProcessorService', () => {
authContext: createMockAuthContext(),
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps: {
byUniversalIdentifier: {},
universalIdentifierById: {},
universalIdentifiersByApplicationId: {},
},
});
expect(result).toBeDefined();
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { isNull, isUndefined } from '@sniptt/guards';
import { isNull, isObject, isUndefined } from '@sniptt/guards';
import {
FieldMetadataSettingsMapping,
FieldMetadataType,
@@ -15,7 +15,6 @@ import {
} from 'twenty-shared/utils';
import { transformActorField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-actor-field.util';
import { isRelationNestedOperation } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-relation-nested-operation.util';
import { transformAddressField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-address-field.util';
import { transformArrayField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-array-field.util';
import { transformCurrencyField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-currency-field.util';
@@ -23,6 +22,7 @@ import { transformFullNameField } from 'src/engine/api/common/common-args-proces
import { transformNumericField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-numeric-field.util';
import { transformRawJsonField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-raw-json-field.util';
import { transformTextField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-text-field.util';
import { isRelationNestedOperation } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-relation-nested-operation.util';
import { validateActorFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-actor-field-or-throw.util';
import { validateAddressFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-address-field-or-throw.util';
import { validateArrayFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util';
@@ -56,12 +56,12 @@ import { transformLinksValue } from 'src/engine/core-modules/record-transformer/
import { transformPhonesValue } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util';
import { transformRichTextValue } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text.util';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@Injectable()
@@ -73,12 +73,14 @@ export class DataArgProcessorService {
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
shouldBackfillPositionIfUndefined = true,
}: {
partialRecordInputs: Partial<ObjectRecord>[] | undefined;
authContext: WorkspaceAuthContext;
flatObjectMetadata: FlatObjectMetadata;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
shouldBackfillPositionIfUndefined?: boolean;
}): Promise<Partial<ObjectRecord>[]> {
if (!isDefined(partialRecordInputs)) {
@@ -158,6 +160,8 @@ export class DataArgProcessorService {
fieldMetadata,
key,
value,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
);
}
processedRecords.push(processedRecord);
@@ -170,6 +174,8 @@ export class DataArgProcessorService {
fieldMetadata: FlatFieldMetadata,
key: string,
value: unknown,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
): Promise<unknown> {
switch (fieldMetadata.type) {
case FieldMetadataType.POSITION:
@@ -260,6 +266,29 @@ export class DataArgProcessorService {
);
}
const connectOperation = value as Record<
string,
Record<string, unknown>
>;
const connectWhere = connectOperation.connect?.where;
if (isObject(connectWhere)) {
const processedWhere = await this.processConnectWhere(
connectWhere as Record<string, unknown>,
fieldMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
);
return {
...connectOperation,
connect: {
...connectOperation.connect,
where: processedWhere,
},
};
}
return value;
}
case FieldMetadataType.PHONES: {
@@ -325,4 +354,85 @@ export class DataArgProcessorService {
);
}
}
private async processConnectWhere(
connectWhere: Record<string, unknown>,
relationFieldMetadata: FlatFieldMetadata,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
): Promise<Record<string, unknown>> {
if (!isDefined(relationFieldMetadata.relationTargetObjectMetadataId)) {
throw new CommonQueryRunnerException(
`Relation target object metadata id not found for field ${relationFieldMetadata.name}`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
const targetObjectMetadata =
findFlatEntityByIdInFlatEntityMaps<FlatObjectMetadata>({
flatEntityId: relationFieldMetadata.relationTargetObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
if (!isDefined(targetObjectMetadata)) {
throw new CommonQueryRunnerException(
`Relation target object metadata not found for field ${relationFieldMetadata.name}`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
targetObjectMetadata,
);
const processedWhere: Record<string, unknown> = {};
for (const [whereKey, whereValue] of Object.entries(connectWhere)) {
const fieldId = fieldIdByName[whereKey];
if (!isDefined(fieldId)) {
processedWhere[whereKey] = whereValue;
continue;
}
const whereFieldMetadata =
findFlatEntityByIdInFlatEntityMaps<FlatFieldMetadata>({
flatEntityId: fieldId,
flatEntityMaps: flatFieldMetadataMaps,
});
if (!isDefined(whereFieldMetadata)) {
processedWhere[whereKey] = whereValue;
continue;
}
try {
const processedValue = await this.processField(
whereFieldMetadata,
whereKey,
whereValue,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
);
// Only keep original keys — processField may add null subfields that alter WHERE semantics
if (isObject(whereValue) && isObject(processedValue)) {
const originalKeys = new Set(Object.keys(whereValue));
processedWhere[whereKey] = Object.fromEntries(
Object.entries(processedValue).filter(([k]) => originalKeys.has(k)),
);
} else {
processedWhere[whereKey] = processedValue;
}
} catch {
processedWhere[whereKey] = whereValue;
}
}
return processedWhere;
}
}
@@ -145,8 +145,12 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
args: CommonInput<CreateManyQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<CreateManyQueryArgs>> {
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
queryRunnerContext;
const {
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
} = queryRunnerContext;
return {
...args,
@@ -155,6 +159,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
}),
};
}
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
import { type ObjectRecord } from 'twenty-shared/types';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service';
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
@@ -15,6 +14,7 @@ import {
CreateOneQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@@ -52,14 +52,19 @@ export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerServ
args: CommonInput<CreateOneQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<CreateOneQueryArgs>> {
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
queryRunnerContext;
const {
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
} = queryRunnerContext;
const coercedData = await this.dataArgProcessor.process({
partialRecordInputs: [args.data],
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
});
return {
@@ -176,8 +176,12 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne
args: CommonInput<FindDuplicatesQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<FindDuplicatesQueryArgs>> {
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
queryRunnerContext;
const {
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
} = queryRunnerContext;
const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
@@ -202,6 +206,7 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
shouldBackfillPositionIfUndefined: false,
}),
};
@@ -100,8 +100,12 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer
args: CommonInput<UpdateManyQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<UpdateManyQueryArgs>> {
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
queryRunnerContext;
const {
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
} = queryRunnerContext;
return {
...args,
@@ -116,6 +120,7 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
shouldBackfillPositionIfUndefined: false,
})
)[0],
@@ -65,8 +65,12 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ
args: CommonInput<UpdateOneQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<UpdateOneQueryArgs>> {
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
queryRunnerContext;
const {
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
} = queryRunnerContext;
return {
...args,
@@ -76,6 +80,7 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ
authContext,
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
shouldBackfillPositionIfUndefined: false,
})
)[0],
@@ -1,3 +1,5 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
@@ -146,4 +148,53 @@ describe('objectRecordChangedValues', () => {
expect(result).toEqual(expectedChanges);
});
it('ignores changes to POSITION fields', () => {
const positionFieldId = 'position-field-id';
const positionUniversalId = 'position-universal-id';
const objectMetadataWithPosition: FlatObjectMetadata = {
...mockObjectMetadata,
fieldIds: [positionFieldId],
};
const flatFieldMetadataMapsWithPosition: FlatEntityMaps<FlatFieldMetadata> =
{
byUniversalIdentifier: {
[positionUniversalId]: {
id: positionFieldId,
name: 'position',
type: FieldMetadataType.POSITION,
universalIdentifier: positionUniversalId,
} as FlatFieldMetadata,
},
universalIdentifierById: {
[positionFieldId]: positionUniversalId,
},
universalIdentifiersByApplicationId: {},
};
const oldRecord = {
id: '74316f58-29b0-4a6a-b8fa-d2b506d5516n',
position: 1,
name: 'Original',
};
const newRecord = {
id: '74316f58-29b0-4a6a-b8fa-d2b506d5516n',
position: 5,
name: 'Updated',
};
const result = objectRecordChangedValues(
oldRecord,
newRecord,
objectMetadataWithPosition,
flatFieldMetadataMapsWithPosition,
);
expect(result).toEqual({
name: { before: 'Original', after: 'Updated' },
});
expect(result).not.toHaveProperty('position');
});
});
@@ -56,7 +56,8 @@ export const objectRecordChangedValues = (
if (
key === 'updatedAt' ||
key === 'searchVector' ||
field?.type === FieldMetadataType.RELATION
field?.type === FieldMetadataType.RELATION ||
field?.type === FieldMetadataType.POSITION
) {
return acc;
}
@@ -128,10 +128,11 @@ export class CommonApiContextBuilderService {
authContext.apiKey.id,
workspaceId,
);
} else if (
isApplicationAuthContext(authContext) &&
isDefined(authContext.application.defaultRoleId)
) {
} else if (isApplicationAuthContext(authContext)) {
if (!isDefined(authContext.application.defaultRoleId)) {
return {};
}
roleId = authContext.application.defaultRoleId;
} else if (isUserAuthContext(authContext)) {
const userWorkspaceRoleId =
@@ -15,6 +15,7 @@ import { type CreateNavigationMenuItemInput } from 'src/engine/metadata-modules/
import { type NavigationMenuItemDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/navigation-menu-item.dto';
import { RecordIdentifierDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/record-identifier.dto';
import { type UpdateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/update-navigation-menu-item.input';
import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum';
import {
NavigationMenuItemException,
NavigationMenuItemExceptionCode,
@@ -336,13 +337,25 @@ export class NavigationMenuItemService {
existingUserWorkspaceId: flatNavigationMenuItemToDelete.userWorkspaceId,
});
const flatEntitiesToDelete = [flatNavigationMenuItemToDelete];
if (flatNavigationMenuItemToDelete.type === NavigationMenuItemType.FOLDER) {
const userWorkspaceIdKey =
flatNavigationMenuItemToDelete.userWorkspaceId ?? 'null';
const folderChildren =
existingFlatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[
userWorkspaceIdKey
]?.[id] ?? [];
flatEntitiesToDelete.unshift(...folderChildren);
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
navigationMenuItem: {
flatEntityToCreate: [],
flatEntityToDelete: [flatNavigationMenuItemToDelete],
flatEntityToDelete: flatEntitiesToDelete,
flatEntityToUpdate: [],
},
},