Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7f4a4cce8 | ||
|
|
eac6723d78 |
@@ -72,13 +72,11 @@ export const AgentChatFilePreview = ({
|
||||
);
|
||||
|
||||
const rightComponent = onRemove ? (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<AvatarOrIcon
|
||||
Icon={IconX}
|
||||
IconColor={theme.font.color.secondary}
|
||||
onClick={onRemove}
|
||||
/>
|
||||
</div>
|
||||
<AvatarOrIcon
|
||||
Icon={IconX}
|
||||
IconColor={theme.font.color.secondary}
|
||||
onClick={onRemove}
|
||||
/>
|
||||
) : undefined;
|
||||
|
||||
const hasRightDivider = isDefined(onRemove);
|
||||
|
||||
+2
-3
@@ -7,7 +7,6 @@ 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';
|
||||
|
||||
@@ -42,8 +41,8 @@ export const RecordTitleCellSingleTextDisplayMode = ({
|
||||
|
||||
const recordStore = useAtomFamilyStateValue(recordStoreFamilyState, recordId);
|
||||
|
||||
const fieldValue = recordStore?.[fieldDefinition.metadata.fieldName];
|
||||
const isEmpty = !isDefined(fieldValue) || fieldValue.trim() === '';
|
||||
const isEmpty =
|
||||
recordStore?.[fieldDefinition.metadata.fieldName]?.trim() === '';
|
||||
|
||||
const { openRecordTitleCell } = useRecordTitleCell();
|
||||
|
||||
|
||||
-88
@@ -519,94 +519,6 @@ 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 = {};
|
||||
|
||||
|
||||
+1
-1
@@ -202,7 +202,7 @@ export const buildRecordFromImportedStructuredRow = ({
|
||||
},
|
||||
|
||||
[FieldMetadataType.EMAILS]: {
|
||||
primaryEmail: (value: unknown) => castToString(value).toLowerCase(),
|
||||
primaryEmail: castToString,
|
||||
additionalEmails: stringArrayJSONSchema.parse,
|
||||
},
|
||||
[FieldMetadataType.FULL_NAME]: {
|
||||
|
||||
+1
-123
@@ -1,6 +1,6 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
|
||||
import { FieldMetadataType } 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,118 +116,6 @@ 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,
|
||||
@@ -258,11 +146,6 @@ describe('DataArgProcessorService', () => {
|
||||
authContext: createMockAuthContext(),
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
},
|
||||
}),
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
@@ -300,11 +183,6 @@ describe('DataArgProcessorService', () => {
|
||||
authContext: createMockAuthContext(),
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
|
||||
+4
-114
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isNull, isObject, isUndefined } from '@sniptt/guards';
|
||||
import { isNull, isUndefined } from '@sniptt/guards';
|
||||
import {
|
||||
FieldMetadataSettingsMapping,
|
||||
FieldMetadataType,
|
||||
@@ -15,6 +15,7 @@ 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';
|
||||
@@ -22,7 +23,6 @@ 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 { 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 { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
@Injectable()
|
||||
@@ -73,14 +73,12 @@ 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)) {
|
||||
@@ -160,8 +158,6 @@ export class DataArgProcessorService {
|
||||
fieldMetadata,
|
||||
key,
|
||||
value,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
);
|
||||
}
|
||||
processedRecords.push(processedRecord);
|
||||
@@ -174,8 +170,6 @@ export class DataArgProcessorService {
|
||||
fieldMetadata: FlatFieldMetadata,
|
||||
key: string,
|
||||
value: unknown,
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
|
||||
): Promise<unknown> {
|
||||
switch (fieldMetadata.type) {
|
||||
case FieldMetadataType.POSITION:
|
||||
@@ -266,29 +260,6 @@ 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: {
|
||||
@@ -354,85 +325,4 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-7
@@ -145,12 +145,8 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
args: CommonInput<CreateManyQueryArgs>,
|
||||
queryRunnerContext: CommonBaseQueryRunnerContext,
|
||||
): Promise<CommonInput<CreateManyQueryArgs>> {
|
||||
const {
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
} = queryRunnerContext;
|
||||
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
|
||||
queryRunnerContext;
|
||||
|
||||
return {
|
||||
...args,
|
||||
@@ -159,7 +155,6 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
+3
-8
@@ -2,6 +2,7 @@ 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';
|
||||
@@ -14,7 +15,6 @@ 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,19 +52,14 @@ export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerServ
|
||||
args: CommonInput<CreateOneQueryArgs>,
|
||||
queryRunnerContext: CommonBaseQueryRunnerContext,
|
||||
): Promise<CommonInput<CreateOneQueryArgs>> {
|
||||
const {
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
} = queryRunnerContext;
|
||||
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
|
||||
queryRunnerContext;
|
||||
|
||||
const coercedData = await this.dataArgProcessor.process({
|
||||
partialRecordInputs: [args.data],
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
+2
-7
@@ -176,12 +176,8 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne
|
||||
args: CommonInput<FindDuplicatesQueryArgs>,
|
||||
queryRunnerContext: CommonBaseQueryRunnerContext,
|
||||
): Promise<CommonInput<FindDuplicatesQueryArgs>> {
|
||||
const {
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
} = queryRunnerContext;
|
||||
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
|
||||
queryRunnerContext;
|
||||
|
||||
const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata(
|
||||
flatFieldMetadataMaps,
|
||||
@@ -206,7 +202,6 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
shouldBackfillPositionIfUndefined: false,
|
||||
}),
|
||||
};
|
||||
|
||||
+2
-7
@@ -100,12 +100,8 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
args: CommonInput<UpdateManyQueryArgs>,
|
||||
queryRunnerContext: CommonBaseQueryRunnerContext,
|
||||
): Promise<CommonInput<UpdateManyQueryArgs>> {
|
||||
const {
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
} = queryRunnerContext;
|
||||
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
|
||||
queryRunnerContext;
|
||||
|
||||
return {
|
||||
...args,
|
||||
@@ -120,7 +116,6 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
shouldBackfillPositionIfUndefined: false,
|
||||
})
|
||||
)[0],
|
||||
|
||||
+2
-7
@@ -65,12 +65,8 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ
|
||||
args: CommonInput<UpdateOneQueryArgs>,
|
||||
queryRunnerContext: CommonBaseQueryRunnerContext,
|
||||
): Promise<CommonInput<UpdateOneQueryArgs>> {
|
||||
const {
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
} = queryRunnerContext;
|
||||
const { authContext, flatObjectMetadata, flatFieldMetadataMaps } =
|
||||
queryRunnerContext;
|
||||
|
||||
return {
|
||||
...args,
|
||||
@@ -80,7 +76,6 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ
|
||||
authContext,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
shouldBackfillPositionIfUndefined: false,
|
||||
})
|
||||
)[0],
|
||||
|
||||
-51
@@ -1,5 +1,3 @@
|
||||
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';
|
||||
@@ -148,53 +146,4 @@ 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');
|
||||
});
|
||||
});
|
||||
|
||||
+1
-2
@@ -56,8 +56,7 @@ export const objectRecordChangedValues = (
|
||||
if (
|
||||
key === 'updatedAt' ||
|
||||
key === 'searchVector' ||
|
||||
field?.type === FieldMetadataType.RELATION ||
|
||||
field?.type === FieldMetadataType.POSITION
|
||||
field?.type === FieldMetadataType.RELATION
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
+10
-1
@@ -29,7 +29,16 @@ export class ResolverValidationPipe implements PipeTransform {
|
||||
return value;
|
||||
}
|
||||
|
||||
const object = plainToInstance(metatype, value);
|
||||
let object: object;
|
||||
|
||||
try {
|
||||
object = plainToInstance(metatype, value);
|
||||
} catch (error) {
|
||||
throw new UserInputError(
|
||||
`Invalid input: ${error instanceof Error ? error.message : 'transformation failed'}`,
|
||||
);
|
||||
}
|
||||
|
||||
const errors = await safeClassValidatorValidateWrapper(object);
|
||||
|
||||
if (errors.length === 0) {
|
||||
|
||||
+4
-5
@@ -128,11 +128,10 @@ export class CommonApiContextBuilderService {
|
||||
authContext.apiKey.id,
|
||||
workspaceId,
|
||||
);
|
||||
} else if (isApplicationAuthContext(authContext)) {
|
||||
if (!isDefined(authContext.application.defaultRoleId)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
} else if (
|
||||
isApplicationAuthContext(authContext) &&
|
||||
isDefined(authContext.application.defaultRoleId)
|
||||
) {
|
||||
roleId = authContext.application.defaultRoleId;
|
||||
} else if (isUserAuthContext(authContext)) {
|
||||
const userWorkspaceRoleId =
|
||||
|
||||
+1
-4
@@ -33,7 +33,6 @@ import {
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
||||
import { FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto';
|
||||
import { type FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
import { transformEnumValue } from 'src/engine/utils/transform-enum-value';
|
||||
|
||||
@@ -132,9 +131,7 @@ export class FieldMetadataDTO<T extends FieldMetadataType = FieldMetadataType> {
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
defaultValue?: FieldMetadataDefaultValue<T>;
|
||||
|
||||
@Transform(({ value }) =>
|
||||
transformEnumValue(value as FieldMetadataDefaultOption[]),
|
||||
)
|
||||
@Transform(({ value }) => transformEnumValue(value))
|
||||
@IsOptional()
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
options?: FieldMetadataOptions<T>;
|
||||
|
||||
+1
-14
@@ -15,7 +15,6 @@ 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,
|
||||
@@ -337,25 +336,13 @@ 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: flatEntitiesToDelete,
|
||||
flatEntityToDelete: [flatNavigationMenuItemToDelete],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { transformEnumValue } from 'src/engine/utils/transform-enum-value';
|
||||
|
||||
describe('transformEnumValue', () => {
|
||||
it('should return undefined when options is undefined', () => {
|
||||
expect(transformEnumValue(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should prefix option values starting with a digit', () => {
|
||||
const options = [
|
||||
{ value: '1_OPTION', label: 'Option 1', color: 'green' as const },
|
||||
{ value: 'OPTION_2', label: 'Option 2', color: 'blue' as const },
|
||||
];
|
||||
|
||||
const result = transformEnumValue(options);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ value: '_1_OPTION', label: 'Option 1', color: 'green' },
|
||||
{ value: 'OPTION_2', label: 'Option 2', color: 'blue' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse and transform options passed as a JSON string', () => {
|
||||
const options = JSON.stringify([
|
||||
{ value: 'STARTUP', label: 'Startup', color: 'green' },
|
||||
{ value: 'SME', label: 'SME', color: 'turquoise' },
|
||||
]);
|
||||
|
||||
const result = transformEnumValue(options);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ value: 'STARTUP', label: 'Startup', color: 'green' },
|
||||
{ value: 'SME', label: 'SME', color: 'turquoise' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should prefix digit-starting values in JSON string options', () => {
|
||||
const options = JSON.stringify([
|
||||
{ value: '3RD_PARTY', label: '3rd Party', color: 'red' },
|
||||
]);
|
||||
|
||||
const result = transformEnumValue(options);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ value: '_3RD_PARTY', label: '3rd Party', color: 'red' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return options unchanged when no values start with a digit', () => {
|
||||
const options = [
|
||||
{ value: 'STARTUP', label: 'Startup', color: 'green' as const },
|
||||
];
|
||||
|
||||
const result = transformEnumValue(options);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ value: 'STARTUP', label: 'Startup', color: 'green' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,20 @@
|
||||
import { type FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
|
||||
export function transformEnumValue(options?: FieldMetadataDefaultOption[]) {
|
||||
return options?.map((option) => {
|
||||
export function transformEnumValue(
|
||||
options?: FieldMetadataDefaultOption[] | string,
|
||||
) {
|
||||
if (!options) {
|
||||
return options;
|
||||
}
|
||||
|
||||
const parsedOptions =
|
||||
typeof options === 'string' ? JSON.parse(options) : options;
|
||||
|
||||
if (!Array.isArray(parsedOptions)) {
|
||||
return parsedOptions;
|
||||
}
|
||||
|
||||
return parsedOptions.map((option: FieldMetadataDefaultOption) => {
|
||||
if (/^\d/.test(option.value)) {
|
||||
return {
|
||||
...option,
|
||||
|
||||
Reference in New Issue
Block a user