Common API - Add tests on position field validation at creation (#16630)

Co-authored-by: guillim <guigloo@msn.com>
This commit is contained in:
Etienne
2025-12-17 17:02:37 +01:00
committed by GitHub
parent 0e6a8c04c4
commit 59d3a14922
13 changed files with 353 additions and 55 deletions
@@ -33,6 +33,7 @@ import { validateLinksFieldOrThrow } from 'src/engine/api/common/common-args-pro
import { validateMultiSelectFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-multi-select-field-or-throw.util';
import { validateNumberFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util';
import { validateNumericFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-numeric-field-or-throw.util';
import { validateOverriddenPositionFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-overridden-position-field-or-throw.util';
import { validatePhonesFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-phones-field-or-throw.util';
import { validateRatingAndSelectFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rating-and-select-field-or-throw.util';
import { validateRawJsonFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util';
@@ -44,7 +45,6 @@ import {
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { transformEmailsValue } from 'src/engine/core-modules/record-transformer/utils/transform-emails-value.util';
import { transformLinksValue } from 'src/engine/core-modules/record-transformer/utils/transform-links-value.util';
@@ -58,10 +58,7 @@ import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-meta
@Injectable()
export class DataArgProcessor {
constructor(
private readonly recordPositionService: RecordPositionService,
private readonly featureFlagService: FeatureFlagService,
) {}
constructor(private readonly recordPositionService: RecordPositionService) {}
async process({
partialRecordInputs,
@@ -90,9 +87,21 @@ export class DataArgProcessor {
flatObjectMetadata,
);
const overriddenPositionRecords =
await this.recordPositionService.overridePositionOnRecords({
partialRecordInputs: partialRecordInputs,
workspaceId: workspace.id,
objectMetadata: {
isCustom: flatObjectMetadata.isCustom,
nameSingular: flatObjectMetadata.nameSingular,
fieldIdByName,
},
shouldBackfillPositionIfUndefined,
});
const processedRecords: Partial<ObjectRecord>[] = [];
for (const record of partialRecordInputs) {
for (const record of overriddenPositionRecords) {
const processedRecord: Partial<ObjectRecord> = {};
for (const [key, value] of Object.entries(record)) {
@@ -139,19 +148,7 @@ export class DataArgProcessor {
processedRecords.push(processedRecord);
}
const overriddenPositionRecords =
await this.recordPositionService.overridePositionOnRecords({
partialRecordInputs: processedRecords,
workspaceId: workspace.id,
objectMetadata: {
isCustom: flatObjectMetadata.isCustom,
nameSingular: flatObjectMetadata.nameSingular,
fieldIdByName,
},
shouldBackfillPositionIfUndefined,
});
return overriddenPositionRecords;
return processedRecords;
}
private async processField(
@@ -161,7 +158,7 @@ export class DataArgProcessor {
): Promise<unknown> {
switch (fieldMetadata.type) {
case FieldMetadataType.POSITION:
return value;
return validateOverriddenPositionFieldOrThrow(value, key);
case FieldMetadataType.NUMERIC: {
const validatedValue = validateNumericFieldOrThrow(value, key);
@@ -0,0 +1,87 @@
import { validateOverriddenPositionFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-overridden-position-field-or-throw.util';
describe('validateOverriddenPositionFieldOrThrow', () => {
describe('valid inputs', () => {
it('should return the position when value is a positive integer', () => {
const result = validateOverriddenPositionFieldOrThrow(123, 'testField');
expect(result).toBe(123);
});
it('should return the position when value is zero', () => {
const result = validateOverriddenPositionFieldOrThrow(0, 'testField');
expect(result).toBe(0);
});
it('should return the position when value is negative integer', () => {
const result = validateOverriddenPositionFieldOrThrow(-3, 'testField');
expect(result).toBe(-3);
});
it('should return the position when value is negative float', () => {
const result = validateOverriddenPositionFieldOrThrow(-0.5, 'testField');
expect(result).toBe(-0.5);
});
});
describe('invalid inputs', () => {
it('should throw when value is null', () => {
expect(() =>
validateOverriddenPositionFieldOrThrow(null, 'testField'),
).toThrow('Invalid position value null for field "testField"');
});
it('should throw when value is undefined', () => {
expect(() =>
validateOverriddenPositionFieldOrThrow(undefined, 'testField'),
).toThrow('Invalid position value undefined for field "testField"');
});
it('should throw when value is a string with a number', () => {
expect(() =>
validateOverriddenPositionFieldOrThrow('123', 'testField'),
).toThrow('Invalid position value \'123\' for field "testField"');
});
it('should throw when value is an empty string', () => {
expect(() =>
validateOverriddenPositionFieldOrThrow('', 'testField'),
).toThrow('Invalid position value \'\' for field "testField"');
});
it('should throw when value is a boolean (true)', () => {
expect(() =>
validateOverriddenPositionFieldOrThrow(true, 'testField'),
).toThrow('Invalid position value true for field "testField"');
});
it('should throw when value is a boolean (false)', () => {
expect(() =>
validateOverriddenPositionFieldOrThrow(false, 'testField'),
).toThrow('Invalid position value false for field "testField"');
});
it('should throw when value is an array', () => {
expect(() =>
validateOverriddenPositionFieldOrThrow([1, 2, 3], 'testField'),
).toThrow('Invalid position value [ 1, 2, 3 ] for field "testField"');
});
it('should throw when value is an object', () => {
expect(() =>
validateOverriddenPositionFieldOrThrow({ key: 'value' }, 'testField'),
).toThrow(
'Invalid position value { key: \'value\' } for field "testField"',
);
});
it('should throw when value is NaN', () => {
expect(() =>
validateOverriddenPositionFieldOrThrow(NaN, 'testField'),
).toThrow('Invalid position value NaN for field "testField"');
});
});
});
@@ -0,0 +1,23 @@
import { inspect } from 'util';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateOverriddenPositionFieldOrThrow = (
value: unknown,
fieldName: string,
): number | null => {
if (
typeof value !== 'number' ||
(typeof value === 'number' &&
(isNaN(value) || value === Infinity || value === -Infinity))
)
throw new CommonQueryRunnerException(
`Invalid position value ${inspect(value)} for field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
return value;
};
@@ -48,8 +48,8 @@ import {
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import type { RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
@Injectable()
export abstract class CommonBaseQueryRunnerService<