Common - Field validation (#15491)

Closes : https://github.com/twentyhq/core-team-issues/issues/1622

To do in other PR : 
- Add migration command for non nullable text, raw_json & array fields
- Add null transformation

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Etienne
2025-11-13 15:45:35 +01:00
committed by GitHub
co-authored by Weiko
parent f6ed1e8295
commit 261cba40ef
114 changed files with 3524 additions and 1189 deletions
@@ -1,3 +0,0 @@
import { CommonSelectedFieldsHandler } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler';
export const CommonArgsHandlers = [CommonSelectedFieldsHandler];
@@ -0,0 +1,4 @@
import { DataArgProcessor } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor';
import { QueryRunnerArgsFactory } from 'src/engine/api/common/common-args-processors/query-runner-args.factory';
export const CommonArgsProcessors = [DataArgProcessor, QueryRunnerArgsFactory]; // TODO: Refacto-common Remove QueryRunnerArgsFactory
@@ -0,0 +1,271 @@
import { Injectable } from '@nestjs/common';
import { isNull, isUndefined } from '@sniptt/guards';
import {
FieldMetadataRelationSettings,
FieldMetadataType,
ObjectRecord,
RelationType,
} from 'twenty-shared/types';
import {
assertIsDefinedOrThrow,
assertUnreachable,
isDefined,
} from 'twenty-shared/utils';
import { transformActorField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-actor-field.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';
import { transformFullNameField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-full-name-field.util';
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 { 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';
import { validateBooleanFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-boolean-field-or-throw.util';
import { validateCurrencyFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-currency-field-or-throw.util';
import { validateDateAndDateTimeFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-and-date-time-field-or-throw.util';
import { validateEmailsFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-emails-field-or-throw.util';
import { validateFullNameFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-full-name-field-or-throw.util';
import { validateLinksFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-links-field-or-throw.util';
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 { 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';
import { validateRichTextV2FieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rich-text-v2-field-or-throw.util';
import { validateTextFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util';
import { validateUUIDFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-uuid-field-or-throw.util';
import {
CommonQueryRunnerException,
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 { 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';
import { transformPhonesValue } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util';
import { transformRichTextV2Value } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
@Injectable()
export class DataArgProcessor {
constructor(private readonly recordPositionService: RecordPositionService) {}
async process({
partialRecordInputs,
authContext,
objectMetadataItemWithFieldMaps,
shouldBackfillPositionIfUndefined = true,
}: {
partialRecordInputs: Partial<ObjectRecord>[] | undefined;
authContext: AuthContext;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
shouldBackfillPositionIfUndefined?: boolean;
}): Promise<Partial<ObjectRecord>[]> {
if (!isDefined(partialRecordInputs)) {
return [];
}
const workspace = authContext.workspace;
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
const processedRecords: Partial<ObjectRecord>[] = [];
for (const record of partialRecordInputs) {
const processedRecord: Partial<ObjectRecord> = {};
for (const [key, value] of Object.entries(record)) {
const fieldMetadataId =
objectMetadataItemWithFieldMaps.fieldIdByName[key] ||
objectMetadataItemWithFieldMaps.fieldIdByJoinColumnName[key];
if (!isDefined(fieldMetadataId)) {
throw new CommonQueryRunnerException(
`Object ${objectMetadataItemWithFieldMaps.nameSingular} doesn't have any "${key}" field.`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
const fieldMetadata =
objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
if (
!isDefined(fieldMetadata.defaultValue) &&
!fieldMetadata.isNullable &&
isNull(value)
) {
throw new CommonQueryRunnerException(
`Field ${key} is not nullable and has no default value.`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
if (isUndefined(value)) {
continue;
}
processedRecord[key] = await this.processField(
fieldMetadata,
key,
value,
);
}
processedRecords.push(processedRecord);
}
const overriddenPositionRecords =
await this.recordPositionService.overridePositionOnRecords({
partialRecordInputs: processedRecords,
workspaceId: workspace.id,
objectMetadata: {
isCustom: objectMetadataItemWithFieldMaps.isCustom,
nameSingular: objectMetadataItemWithFieldMaps.nameSingular,
fieldIdByName: objectMetadataItemWithFieldMaps.fieldIdByName,
},
shouldBackfillPositionIfUndefined,
});
return overriddenPositionRecords;
}
private async processField(
fieldMetadata: FieldMetadataEntity,
key: string,
value: unknown,
): Promise<unknown> {
switch (fieldMetadata.type) {
case FieldMetadataType.POSITION:
return value;
case FieldMetadataType.NUMERIC: {
const validatedValue = validateNumericFieldOrThrow(value, key);
return transformNumericField(validatedValue);
}
case FieldMetadataType.NUMBER: {
return validateNumberFieldOrThrow(value, key);
}
case FieldMetadataType.TEXT: {
const validatedValue = validateTextFieldOrThrow(value, key);
return transformTextField(validatedValue);
}
case FieldMetadataType.DATE_TIME:
case FieldMetadataType.DATE:
return validateDateAndDateTimeFieldOrThrow(value, key);
case FieldMetadataType.BOOLEAN:
return validateBooleanFieldOrThrow(value, key);
case FieldMetadataType.RATING:
case FieldMetadataType.SELECT: {
validateRatingAndSelectFieldOrThrow(
value,
key,
fieldMetadata.options?.map((option) => option.value),
);
return value;
}
case FieldMetadataType.MULTI_SELECT: {
const validatedValue = validateMultiSelectFieldOrThrow(
value,
key,
fieldMetadata.options?.map((option) => option.value),
);
return transformArrayField(validatedValue);
}
case FieldMetadataType.UUID:
return validateUUIDFieldOrThrow(value, key);
case FieldMetadataType.ARRAY: {
const validatedValue = validateArrayFieldOrThrow(value, key);
return transformArrayField(validatedValue);
}
case FieldMetadataType.RAW_JSON: {
const validatedValue = validateRawJsonFieldOrThrow(value, key);
return transformRawJsonField(validatedValue);
}
case FieldMetadataType.RELATION:
case FieldMetadataType.MORPH_RELATION: {
const fieldMetadataRelationSettings =
fieldMetadata.settings as FieldMetadataRelationSettings;
if (
fieldMetadataRelationSettings.relationType ===
RelationType.ONE_TO_MANY
) {
throw new CommonQueryRunnerException(
`One-to-many relation ${key} field does not support write operations.`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
if (key === fieldMetadataRelationSettings.joinColumnName) {
return validateUUIDFieldOrThrow(value, key);
}
return value;
}
case FieldMetadataType.PHONES: {
const validatedValue = validatePhonesFieldOrThrow(value, key);
return transformPhonesValue({ input: validatedValue });
}
case FieldMetadataType.EMAILS: {
const validatedValue = validateEmailsFieldOrThrow(value, key);
return transformEmailsValue(validatedValue);
}
case FieldMetadataType.FULL_NAME: {
const validatedValue = validateFullNameFieldOrThrow(value, key);
return transformFullNameField(validatedValue);
}
case FieldMetadataType.ADDRESS: {
const validatedValue = validateAddressFieldOrThrow(value, key);
return transformAddressField(validatedValue);
}
case FieldMetadataType.CURRENCY: {
const validatedValue = validateCurrencyFieldOrThrow(value, key);
return transformCurrencyField(validatedValue);
}
case FieldMetadataType.ACTOR: {
const validatedValue = validateActorFieldOrThrow(value, key);
return transformActorField(validatedValue);
}
case FieldMetadataType.RICH_TEXT_V2: {
const validatedValue = validateRichTextV2FieldOrThrow(value, key);
return await transformRichTextV2Value(validatedValue);
}
case FieldMetadataType.LINKS: {
const validatedValue = validateLinksFieldOrThrow(value, key);
return transformLinksValue(validatedValue);
}
case FieldMetadataType.RICH_TEXT:
case FieldMetadataType.TS_VECTOR:
throw new CommonQueryRunnerException(
`${key} ${fieldMetadata.type}-typed field does not support write operations`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
default:
assertUnreachable(
fieldMetadata.type,
'Should never occur, add validator for new field type',
);
}
}
}
@@ -0,0 +1,84 @@
import { FieldActorSource } from 'twenty-shared/types';
import { transformActorField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-actor-field.util';
describe('transformActorField', () => {
it('should return null when value is null', () => {
const result = transformActorField(null, true);
expect(result).toBeNull();
});
it('should transform actor with source only', () => {
const result = transformActorField(
{
source: FieldActorSource.EMAIL,
},
true,
);
expect(result).toEqual({
source: FieldActorSource.EMAIL,
});
});
it('should transform actor with source and context', () => {
const result = transformActorField(
{
source: FieldActorSource.WORKFLOW,
context: { workflowId: '123', stepId: 'step-1' },
},
true,
);
expect(result).toEqual({
source: FieldActorSource.WORKFLOW,
context: { workflowId: '123', stepId: 'step-1' },
});
});
it('should transform actor with null source', () => {
const result = transformActorField(
{
source: null,
context: { userId: '456' },
},
true,
);
expect(result).toEqual({
source: null,
context: { userId: '456' },
});
});
it('should transform actor with null context', () => {
const result = transformActorField(
{
source: FieldActorSource.API,
context: null,
},
true,
);
expect(result).toEqual({
source: FieldActorSource.API,
context: null,
});
});
it('should transform empty context object to null', () => {
const result = transformActorField(
{
source: FieldActorSource.EMAIL,
context: {},
},
true,
);
expect(result).toEqual({
source: FieldActorSource.EMAIL,
context: null,
});
});
});
@@ -0,0 +1,45 @@
import { transformAddressField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-address-field.util';
describe('transformAddressField', () => {
it('should return null when value is null', () => {
const result = transformAddressField(null);
expect(result).toBeNull();
});
it('should return an empty object when value is an empty object', () => {
const result = transformAddressField({});
expect(result).toEqual({});
});
it('should preserve undefined for fields that are not provided', () => {
const result = transformAddressField({
addressStreet1: '123 Main St',
addressCity: 'San Francisco',
});
expect(result).toEqual({
addressStreet1: '123 Main St',
addressCity: 'San Francisco',
});
});
it('should handle mixed null, undefined, and valid values', () => {
const result = transformAddressField({
addressStreet1: '123 Main St',
addressStreet2: null,
addressCity: 'San Francisco',
addressLat: 37.7749,
addressLng: null,
});
expect(result).toEqual({
addressStreet1: '123 Main St',
addressStreet2: null,
addressCity: 'San Francisco',
addressLat: 37.7749,
addressLng: null,
});
});
});
@@ -0,0 +1,27 @@
import { transformArrayField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-array-field.util';
describe('transformArrayField', () => {
it('should return null when value is null', () => {
const result = transformArrayField(null, true);
expect(result).toBeNull();
});
it('should return null when value is an empty array', () => {
const result = transformArrayField([], true);
expect(result).toBeNull();
});
it('should return an array when value is a string', () => {
const result = transformArrayField('singleString', true);
expect(result).toEqual(['singleString']);
});
it('should return an array when value is an array of strings', () => {
const result = transformArrayField(['string1', 'string2', 'string3'], true);
expect(result).toEqual(['string1', 'string2', 'string3']);
});
});
@@ -0,0 +1,54 @@
import { transformCurrencyField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-currency-field.util';
describe('transformCurrencyField', () => {
it('should return null when value is null', () => {
const result = transformCurrencyField(null);
expect(result).toBeNull();
});
it('should return empty object when value is empty object', () => {
const result = transformCurrencyField({});
expect(result).toEqual({});
});
it('should transform amountMicros from number', () => {
const result = transformCurrencyField({ amountMicros: 1000 });
expect(result).toEqual({ amountMicros: 1000 });
});
it('should transform amountMicros from string to number', () => {
const result = transformCurrencyField({ amountMicros: '1000' });
expect(result).toEqual({ amountMicros: 1000 });
});
it('should transform currencyCode', () => {
const result = transformCurrencyField({ currencyCode: 'USD' });
expect(result).toEqual({ currencyCode: 'USD' });
});
it('should transform both amountMicros and currencyCode', () => {
const result = transformCurrencyField({
amountMicros: '2500',
currencyCode: 'EUR',
});
expect(result).toEqual({ amountMicros: 2500, currencyCode: 'EUR' });
});
it('should handle null amountMicros', () => {
const result = transformCurrencyField({ amountMicros: null });
expect(result).toEqual({ amountMicros: null });
});
it('should handle null currencyCode', () => {
const result = transformCurrencyField({ currencyCode: null });
expect(result).toEqual({ currencyCode: null });
});
});
@@ -0,0 +1,33 @@
import { transformFullNameField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-full-name-field.util';
describe('transformFullNameField', () => {
it('should return null when value is null', () => {
const result = transformFullNameField(null, true);
expect(result).toBeNull();
});
it('should return full name object with both fields', () => {
const value = {
firstName: 'John',
lastName: 'Doe',
};
const result = transformFullNameField(value, true);
expect(result).toEqual({
firstName: 'John',
lastName: 'Doe',
});
});
it('should return full name object with only lastName', () => {
const value = {
lastName: '',
};
const result = transformFullNameField(value, true);
expect(result).toEqual({
lastName: null,
});
});
});
@@ -0,0 +1,21 @@
import { transformNumericField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-numeric-field.util';
describe('transformNumericField', () => {
it('should return null when value is null', () => {
const result = transformNumericField(null);
expect(result).toBeNull();
});
it('should return the number when value is a float', () => {
const result = transformNumericField(3.14159);
expect(result).toBe(3.14159);
});
it('should transform a numeric string with decimals to a number', () => {
const result = transformNumericField('123.456');
expect(result).toBe(123.456);
});
});
@@ -0,0 +1,21 @@
import { transformRawJsonField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-raw-json-field.util';
describe('transformRawJsonField', () => {
it('should return null when value is null', () => {
const result = transformRawJsonField(null, true);
expect(result).toBeNull();
});
it('should return null when value is empty object', () => {
const result = transformRawJsonField({}, true);
expect(result).toBeNull();
});
it('should return the string when value is empty array', () => {
const result = transformRawJsonField([], true);
expect(result).toBeNull();
});
});
@@ -0,0 +1,21 @@
import { transformTextField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-text-field.util';
describe('transformTextField', () => {
it('should return null when value is null', () => {
const result = transformTextField(null, true);
expect(result).toBeNull();
});
it('should return null when value is empty string', () => {
const result = transformTextField('', true);
expect(result).toBeNull();
});
it('should return the string when value is a non-empty string', () => {
const result = transformTextField('hello world', true);
expect(result).toBe('hello world');
});
});
@@ -0,0 +1,24 @@
import { isNull, isUndefined } from '@sniptt/guards';
import { type FieldActorSource } from 'twenty-shared/types';
import { transformRawJsonField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-raw-json-field.util';
export const transformActorField = (
value: {
source?: FieldActorSource | null;
context?: object | null;
} | null,
isNullEquivalenceEnabled: boolean = false,
): {
source?: FieldActorSource | null;
context?: object | null;
} | null => {
if (isNull(value)) return null;
return {
source: value.source,
context: isUndefined(value.context)
? undefined
: transformRawJsonField(value.context, isNullEquivalenceEnabled),
};
};
@@ -0,0 +1,56 @@
import { isNull, isUndefined } from '@sniptt/guards';
import { transformNumericField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-numeric-field.util';
import { transformTextField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-text-field.util';
export const transformAddressField = (
value: {
addressStreet1?: string | null;
addressStreet2?: string | null;
addressCity?: string | null;
addressState?: string | null;
addressPostcode?: string | null;
addressCountry?: string | null;
addressLat?: number | null;
addressLng?: number | null;
} | null,
isNullEquivalenceEnabled: boolean = false,
): {
addressStreet1?: string | null;
addressStreet2?: string | null;
addressCity?: string | null;
addressState?: string | null;
addressPostcode?: string | null;
addressCountry?: string | null;
addressLat?: number | null;
addressLng?: number | null;
} | null => {
if (isNull(value)) return null;
return {
addressStreet1: isUndefined(value.addressStreet1)
? undefined
: transformTextField(value.addressStreet1, isNullEquivalenceEnabled),
addressStreet2: isUndefined(value.addressStreet2)
? undefined
: transformTextField(value.addressStreet2, isNullEquivalenceEnabled),
addressCity: isUndefined(value.addressCity)
? undefined
: transformTextField(value.addressCity, isNullEquivalenceEnabled),
addressState: isUndefined(value.addressState)
? undefined
: transformTextField(value.addressState, isNullEquivalenceEnabled),
addressPostcode: isUndefined(value.addressPostcode)
? undefined
: transformTextField(value.addressPostcode, isNullEquivalenceEnabled),
addressCountry: isUndefined(value.addressCountry)
? undefined
: transformTextField(value.addressCountry, isNullEquivalenceEnabled),
addressLat: isUndefined(value.addressLat)
? undefined
: transformNumericField(value.addressLat),
addressLng: isUndefined(value.addressLng)
? undefined
: transformNumericField(value.addressLng),
};
};
@@ -0,0 +1,14 @@
import { isNull } from '@sniptt/guards';
export const transformArrayField = (
value: string | string[] | null,
isNullEquivalenceEnabled: boolean = false,
): string[] | null => {
if (typeof value === 'string') return [value];
return isNullEquivalenceEnabled &&
!isNull(value) &&
Object.keys(value).length === 0
? null
: value;
};
@@ -0,0 +1,26 @@
import { isNull, isUndefined } from '@sniptt/guards';
import { transformNumericField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-numeric-field.util';
import { transformTextField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-text-field.util';
export const transformCurrencyField = (
value: {
amountMicros?: number | string | null;
currencyCode?: string | null;
} | null,
isNullEquivalenceEnabled: boolean = false,
): {
amountMicros?: number | null;
currencyCode?: string | null;
} | null => {
if (isNull(value)) return null;
return {
amountMicros: isUndefined(value.amountMicros)
? undefined
: transformNumericField(value.amountMicros),
currencyCode: isUndefined(value.currencyCode)
? undefined
: transformTextField(value.currencyCode, isNullEquivalenceEnabled),
};
};
@@ -0,0 +1,25 @@
import { isNull, isUndefined } from '@sniptt/guards';
import { transformTextField } from 'src/engine/api/common/common-args-processors/data-arg-processor/transformer-utils/transform-text-field.util';
export const transformFullNameField = (
value: {
firstName?: string | null;
lastName?: string | null;
} | null,
isNullEquivalenceEnabled: boolean = false,
): {
firstName?: string | null;
lastName?: string | null;
} | null => {
if (isNull(value)) return null;
return {
firstName: isUndefined(value.firstName)
? undefined
: transformTextField(value.firstName, isNullEquivalenceEnabled),
lastName: isUndefined(value.lastName)
? undefined
: transformTextField(value.lastName, isNullEquivalenceEnabled),
};
};
@@ -0,0 +1,7 @@
import { isNull } from '@sniptt/guards';
export const transformNumericField = (
value: number | string | null,
): number | null => {
return isNull(value) ? null : Number(value);
};
@@ -0,0 +1,12 @@
import { isNull } from '@sniptt/guards';
export const transformRawJsonField = (
value: object | null,
isNullEquivalenceEnabled: boolean = false,
): object | null => {
return isNullEquivalenceEnabled &&
!isNull(value) &&
Object.keys(value).length === 0
? null
: value;
};
@@ -0,0 +1,8 @@
import { isNonEmptyString } from '@sniptt/guards';
export const transformTextField = (
value: string | null,
isNullEquivalenceEnabled: boolean = false,
): string | null => {
return isNullEquivalenceEnabled && !isNonEmptyString(value) ? null : value;
};
@@ -0,0 +1,84 @@
import { FieldActorSource } from 'twenty-shared/types';
import { validateActorFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-actor-field-or-throw.util';
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
describe('validateActorFieldOrThrow', () => {
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateActorFieldOrThrow(null, 'testField');
expect(result).toBeNull();
});
it('should return valid actor object with source and context', () => {
const validActor = {
source: FieldActorSource.EMAIL,
context: { userId: '123', email: 'test@example.com' },
};
const result = validateActorFieldOrThrow(validActor, 'testField');
expect(result).toEqual(validActor);
});
it('should accept empty context object', () => {
const validActor = {
source: FieldActorSource.EMAIL,
context: {},
};
const result = validateActorFieldOrThrow(validActor, 'testField');
expect(result).toEqual(validActor);
});
});
describe('invalid inputs', () => {
it('should throw when value is undefined', () => {
expect(() => validateActorFieldOrThrow(undefined, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when value is a string', () => {
expect(() => validateActorFieldOrThrow('invalid', 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when source is invalid', () => {
const invalidActor = {
source: 'INVALID_SOURCE',
context: {},
};
expect(() =>
validateActorFieldOrThrow(invalidActor, 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when context is a string', () => {
const invalidActor = {
source: FieldActorSource.EMAIL,
context: 'invalid',
};
expect(() =>
validateActorFieldOrThrow(invalidActor, 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when actor has invalid subfield', () => {
const invalidActor = {
source: FieldActorSource.EMAIL,
context: {},
invalidField: 'invalid',
};
expect(() =>
validateActorFieldOrThrow(invalidActor, 'testField'),
).toThrow(CommonQueryRunnerException);
});
});
});
@@ -0,0 +1,159 @@
import { validateAddressFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-address-field-or-throw.util';
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
describe('validateAddressFieldOrThrow', () => {
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateAddressFieldOrThrow(null, 'testField');
expect(result).toBeNull();
});
it('should return valid address object with all text fields', () => {
const value = {
addressStreet1: '123 Main St',
addressStreet2: 'Apt 4B',
addressCity: 'New York',
addressState: 'NY',
addressPostcode: '10001',
addressCountry: 'USA',
};
const result = validateAddressFieldOrThrow(value, 'testField');
expect(result).toEqual(value);
});
it('should return valid address object with coordinates', () => {
const value = {
addressStreet1: '123 Main St',
addressCity: 'New York',
addressLat: 40.7128,
addressLng: -74.006,
};
const result = validateAddressFieldOrThrow(value, 'testField');
expect(result).toEqual(value);
});
it('should return valid address object with null subfields', () => {
const value = {
addressStreet1: '123 Main St',
addressStreet2: null,
addressCity: 'New York',
addressState: null,
addressPostcode: null,
addressCountry: null,
addressLat: null,
addressLng: null,
};
const result = validateAddressFieldOrThrow(value, 'testField');
expect(result).toEqual(value);
});
});
describe('invalid inputs', () => {
it('should throw when value is not an object', () => {
expect(() => validateAddressFieldOrThrow('invalid', 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when value is an array', () => {
expect(() =>
validateAddressFieldOrThrow(['123 Main St'], 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when addressStreet1 is not a string or number', () => {
const value = {
addressStreet1: { invalid: 'object' },
};
expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when addressStreet2 is not a string', () => {
const value = {
addressStreet2: 123,
};
expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when addressCity is not a string', () => {
const value = {
addressCity: true,
};
expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when addressState is not a string', () => {
const value = {
addressState: ['NY'],
};
expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when addressPostcode is not a string', () => {
const value = {
addressPostcode: 10001,
};
expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when addressCountry is not a string', () => {
const value = {
addressCountry: { code: 'US' },
};
expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when addressLat is not a number', () => {
const value = {
addressLat: 'not a number',
};
expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when addressLng is not a number', () => {
const value = {
addressLng: 'not a number',
};
expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when an invalid subfield is provided', () => {
const value = {
addressStreet1: '123 Main St',
invalidSubField: 'invalid',
};
expect(() => validateAddressFieldOrThrow(value, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
});
});
@@ -0,0 +1,55 @@
import { validateArrayFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util';
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
describe('validateArrayFieldOrThrow', () => {
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateArrayFieldOrThrow(null, 'testField');
expect(result).toBeNull();
});
it('should return the string when value is a string', () => {
const result = validateArrayFieldOrThrow('singleString', 'testField');
expect(result).toBe('singleString');
});
it('should return the array when value is an empty array', () => {
const result = validateArrayFieldOrThrow([], 'testField');
expect(result).toEqual([]);
});
it('should return the array when value is an array of strings', () => {
const stringArray = ['string1', 'string2', 'string3'];
const result = validateArrayFieldOrThrow(stringArray, 'testField');
expect(result).toEqual(stringArray);
});
});
describe('invalid inputs', () => {
it('should throw when value is a number', () => {
expect(() => validateArrayFieldOrThrow(123, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when value is an object', () => {
const objectValue = { key: 'value' };
expect(() => validateArrayFieldOrThrow(objectValue, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when value is an array containing objects', () => {
const arrayWithObjects = ['string1', { key: 'value' }, 'string2'];
expect(() =>
validateArrayFieldOrThrow(arrayWithObjects, 'testField'),
).toThrow(CommonQueryRunnerException);
});
});
});
@@ -0,0 +1,70 @@
import { validateBooleanFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-boolean-field-or-throw.util';
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
describe('validateBooleanFieldOrThrow', () => {
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateBooleanFieldOrThrow(null, 'testField');
expect(result).toBeNull();
});
it('should return true when value is true', () => {
const result = validateBooleanFieldOrThrow(true, 'testField');
expect(result).toBe(true);
});
it('should return false when value is false', () => {
const result = validateBooleanFieldOrThrow(false, 'testField');
expect(result).toBe(false);
});
});
describe('invalid inputs', () => {
it('should throw when value is undefined', () => {
expect(() => validateBooleanFieldOrThrow(undefined, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when value is a string "true"', () => {
expect(() => validateBooleanFieldOrThrow('true', 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when value is an empty string', () => {
expect(() => validateBooleanFieldOrThrow('', 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when value is a number 1', () => {
expect(() => validateBooleanFieldOrThrow(1, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when value is a number 0', () => {
expect(() => validateBooleanFieldOrThrow(0, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when value is an object', () => {
expect(() => validateBooleanFieldOrThrow({}, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when value is a function', () => {
const functionValue = () => true;
expect(() =>
validateBooleanFieldOrThrow(functionValue, 'testField'),
).toThrow(CommonQueryRunnerException);
});
});
});
@@ -0,0 +1,73 @@
import { validateCurrencyFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-currency-field-or-throw.util';
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
describe('validateCurrencyFieldOrThrow', () => {
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateCurrencyFieldOrThrow(null, 'testField');
expect(result).toBeNull();
});
it('should return the currency object when both amountMicros and currencyCode are valid', () => {
const currencyValue = {
amountMicros: 1000000,
currencyCode: 'USD',
};
const result = validateCurrencyFieldOrThrow(currencyValue, 'testField');
expect(result).toEqual(currencyValue);
});
it('should return the currency object when only amountMicros is provided', () => {
const currencyValue = {
amountMicros: 5000000,
};
const result = validateCurrencyFieldOrThrow(currencyValue, 'testField');
expect(result).toEqual(currencyValue);
});
});
describe('invalid inputs', () => {
it('should throw when value is not an object', () => {
expect(() =>
validateCurrencyFieldOrThrow('not an object', 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when amountMicros is not a valid numeric value', () => {
const currencyValue = {
amountMicros: 'not a number',
currencyCode: 'USD',
};
expect(() =>
validateCurrencyFieldOrThrow(currencyValue, 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when currencyCode is not a string', () => {
const currencyValue = {
amountMicros: 1000000,
currencyCode: 123,
};
expect(() =>
validateCurrencyFieldOrThrow(currencyValue, 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when an invalid subfield is present', () => {
const currencyValue = {
amountMicros: 1000000,
currencyCode: 'USD',
invalidField: 'invalid',
};
expect(() =>
validateCurrencyFieldOrThrow(currencyValue, 'testField'),
).toThrow(CommonQueryRunnerException);
});
});
});
@@ -0,0 +1,97 @@
import { validateDateAndDateTimeFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-date-and-date-time-field-or-throw.util';
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
describe('validateDateAndDateTimeFieldOrThrow', () => {
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateDateAndDateTimeFieldOrThrow(null, 'testField');
expect(result).toBeNull();
});
it('should return the value when it is a valid ISO date string', () => {
const dateString = '2024-01-15';
const result = validateDateAndDateTimeFieldOrThrow(
dateString,
'testField',
);
expect(result).toBe(dateString);
});
it('should return the value when it is a valid ISO datetime string', () => {
const datetimeString = '2024-01-15T10:30:00Z';
const result = validateDateAndDateTimeFieldOrThrow(
datetimeString,
'testField',
);
expect(result).toBe(datetimeString);
});
it('should return the value when it is a Date object', () => {
const dateObject = new Date('2024-01-15');
const result = validateDateAndDateTimeFieldOrThrow(
dateObject,
'testField',
);
expect(result).toBe(dateObject);
});
it('should return the value when it is a timestamp number', () => {
const timestamp = Date.now();
const result = validateDateAndDateTimeFieldOrThrow(
timestamp,
'testField',
);
expect(result).toBe(timestamp);
});
it('should return the value when it is a Date', () => {
const date = new Date();
const result = validateDateAndDateTimeFieldOrThrow(date, 'testField');
expect(result).toBe(date);
});
});
describe('invalid inputs', () => {
it('should throw when value is an invalid date string', () => {
expect(() =>
validateDateAndDateTimeFieldOrThrow('invalid-date', 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when value is an empty string', () => {
expect(() =>
validateDateAndDateTimeFieldOrThrow('', 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when value is a boolean', () => {
expect(() =>
validateDateAndDateTimeFieldOrThrow(true, 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when value is an array', () => {
expect(() =>
validateDateAndDateTimeFieldOrThrow([], 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when value is an object', () => {
expect(() =>
validateDateAndDateTimeFieldOrThrow({}, 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when value is undefined', () => {
expect(() =>
validateDateAndDateTimeFieldOrThrow(undefined, 'testField'),
).toThrow(CommonQueryRunnerException);
});
});
});
@@ -0,0 +1,87 @@
import { validateEmailsFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-emails-field-or-throw.util';
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
describe('validateEmailsFieldOrThrow', () => {
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateEmailsFieldOrThrow(null, 'testField');
expect(result).toBeNull();
});
it('should return the emails object when all fields are valid', () => {
const emailsValue = {
primaryEmail: 'primary@example.com',
additionalEmails: ['secondary1@example.com', 'secondary2@example.com'],
};
const result = validateEmailsFieldOrThrow(emailsValue, 'testField');
expect(result).toEqual(emailsValue);
});
it('should return the emails object when only primaryEmail is provided', () => {
const emailsValue = {
primaryEmail: 'primary@example.com',
};
const result = validateEmailsFieldOrThrow(emailsValue, 'testField');
expect(result).toEqual(emailsValue);
});
it('should accept empty additionalEmails array', () => {
const emailsValue = {
primaryEmail: 'primary@example.com',
additionalEmails: [],
};
const result = validateEmailsFieldOrThrow(emailsValue, 'testField');
expect(result).toEqual(emailsValue);
});
});
describe('invalid inputs', () => {
it('should throw when value is not an object', () => {
expect(() =>
validateEmailsFieldOrThrow('not an object', 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when value is undefined', () => {
expect(() => validateEmailsFieldOrThrow(undefined, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when primaryEmail is not a string', () => {
const emailsValue = {
primaryEmail: 12345,
};
expect(() =>
validateEmailsFieldOrThrow(emailsValue, 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when additionalEmails is not an array', () => {
const emailsValue = {
additionalEmails: { key: 'not an array' },
};
expect(() =>
validateEmailsFieldOrThrow(emailsValue, 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when invalid subfields are present', () => {
const emailsValue = {
primaryEmail: 'primary@example.com',
invalidField1: 'invalid',
invalidField2: 'invalid',
};
expect(() =>
validateEmailsFieldOrThrow(emailsValue, 'testField'),
).toThrow(CommonQueryRunnerException);
});
});
});
@@ -0,0 +1,70 @@
import { validateFullNameFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-full-name-field-or-throw.util';
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
describe('validateFullNameFieldOrThrow', () => {
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateFullNameFieldOrThrow(null, 'testField');
expect(result).toBeNull();
});
it('should return valid full name object with both fields', () => {
const value = {
firstName: 'John',
lastName: 'Doe',
};
const result = validateFullNameFieldOrThrow(value, 'testField');
expect(result).toEqual(value);
});
it('should return valid full name object with only firstName', () => {
const value = {
firstName: 'John',
};
const result = validateFullNameFieldOrThrow(value, 'testField');
expect(result).toEqual(value);
});
it('should return valid full name object with only lastName', () => {
const value = {
lastName: 'Doe',
};
const result = validateFullNameFieldOrThrow(value, 'testField');
expect(result).toEqual(value);
});
});
describe('invalid inputs', () => {
it('should throw when value is not an object', () => {
expect(() =>
validateFullNameFieldOrThrow('invalid', 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when firstName is not a string', () => {
const value = {
firstName: { invalid: 'object' },
};
expect(() => validateFullNameFieldOrThrow(value, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when an invalid subfield is provided', () => {
const value = {
firstName: 'John',
lastName: 'Doe',
invalidSubField: 'invalid',
};
expect(() => validateFullNameFieldOrThrow(value, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
});
});
@@ -0,0 +1,88 @@
import { validateLinksFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-links-field-or-throw.util';
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
describe('validateLinksFieldOrThrow', () => {
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateLinksFieldOrThrow(null, 'testField');
expect(result).toBeNull();
});
it('should return the links object when all fields are valid', () => {
const linksValue = {
primaryLinkUrl: 'https://example.com',
primaryLinkLabel: 'Example Website',
secondaryLinks: [{ url: 'https://secondary.com', label: 'Secondary' }],
};
const result = validateLinksFieldOrThrow(linksValue, 'testField');
expect(result).toEqual(linksValue);
});
it('should return the links object when only primaryLinkUrl is provided', () => {
const linksValue = {
primaryLinkUrl: 'https://example.com',
};
const result = validateLinksFieldOrThrow(linksValue, 'testField');
expect(result).toEqual(linksValue);
});
});
describe('invalid inputs', () => {
it('should throw when value is not an object', () => {
expect(() =>
validateLinksFieldOrThrow('not an object', 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when value is undefined', () => {
expect(() => validateLinksFieldOrThrow(undefined, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when primaryLinkUrl is not a string', () => {
const linksValue = {
primaryLinkUrl: 12345,
};
expect(() => validateLinksFieldOrThrow(linksValue, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when primaryLinkLabel is not a string', () => {
const linksValue = {
primaryLinkLabel: ['not', 'a', 'string'],
};
expect(() => validateLinksFieldOrThrow(linksValue, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when secondaryLinks is not an object or null', () => {
const linksValue = {
secondaryLinks: 'not an object',
};
expect(() => validateLinksFieldOrThrow(linksValue, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when invalid subfields are present', () => {
const linksValue = {
primaryLinkUrl: 'https://example.com',
invalidField1: 'invalid',
invalidField2: 'invalid',
};
expect(() => validateLinksFieldOrThrow(linksValue, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
});
});
@@ -0,0 +1,73 @@
import { validateMultiSelectFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-multi-select-field-or-throw.util';
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
describe('validateMultiSelectFieldOrThrow', () => {
const validOptions = ['option1', 'option2', 'option3'];
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateMultiSelectFieldOrThrow(
null,
'testField',
validOptions,
);
expect(result).toBeNull();
});
it('should return array when all values are in options', () => {
const value = ['option1', 'option2'];
const result = validateMultiSelectFieldOrThrow(
value,
'testField',
validOptions,
);
expect(result).toEqual(value);
});
it('should return string when value is a single string in options', () => {
const value = 'option1';
const result = validateMultiSelectFieldOrThrow(
value,
'testField',
validOptions,
);
expect(result).toEqual(value);
});
it('should return empty array when value is empty array', () => {
const value: string[] = [];
const result = validateMultiSelectFieldOrThrow(
value,
'testField',
validOptions,
);
expect(result).toEqual(value);
});
});
describe('invalid inputs', () => {
it('should throw when options are undefined', () => {
expect(() =>
validateMultiSelectFieldOrThrow(['option1'], 'testField', undefined),
).toThrow(CommonQueryRunnerException);
});
it('should throw when options are not defined', () => {
expect(() =>
validateMultiSelectFieldOrThrow(['option1'], 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when value contains option not in the options list', () => {
const value = ['option1', 'invalidOption'];
expect(() =>
validateMultiSelectFieldOrThrow(value, 'testField', validOptions),
).toThrow(CommonQueryRunnerException);
});
});
});
@@ -0,0 +1,87 @@
import { validateNumberFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util';
describe('validateNumberFieldOrThrow', () => {
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateNumberFieldOrThrow(null, 'testField');
expect(result).toBeNull();
});
it('should return the number when value is a positive integer', () => {
const result = validateNumberFieldOrThrow(123, 'testField');
expect(result).toBe(123);
});
it('should return the number when value is a negative integer', () => {
const result = validateNumberFieldOrThrow(-456, 'testField');
expect(result).toBe(-456);
});
it('should return the number when value is a positive float', () => {
const result = validateNumberFieldOrThrow(123.45, 'testField');
expect(result).toBe(123.45);
});
it('should return the number when value is zero', () => {
const result = validateNumberFieldOrThrow(0, 'testField');
expect(result).toBe(0);
});
});
describe('invalid inputs', () => {
it('should throw when value is undefined', () => {
expect(() => validateNumberFieldOrThrow(undefined, 'testField')).toThrow(
'Invalid number value undefined for field "testField"',
);
});
it('should throw when value is a string with a number', () => {
expect(() => validateNumberFieldOrThrow('123', 'testField')).toThrow(
'Invalid number value \'123\' for field "testField"',
);
});
it('should throw when value is an empty string', () => {
expect(() => validateNumberFieldOrThrow('', 'testField')).toThrow(
'Invalid number value \'\' for field "testField"',
);
});
it('should throw when value is a boolean (true)', () => {
expect(() => validateNumberFieldOrThrow(true, 'testField')).toThrow(
'Invalid number value true for field "testField"',
);
});
it('should throw when value is a boolean (false)', () => {
expect(() => validateNumberFieldOrThrow(false, 'testField')).toThrow(
'Invalid number value false for field "testField"',
);
});
it('should throw when value is an array', () => {
expect(() => validateNumberFieldOrThrow([1, 2, 3], 'testField')).toThrow(
'Invalid number value [ 1, 2, 3 ] for field "testField"',
);
});
it('should throw when value is an object', () => {
expect(() =>
validateNumberFieldOrThrow({ key: 'value' }, 'testField'),
).toThrow(
'Invalid number value { key: \'value\' } for field "testField"',
);
});
it('should throw when value is NaN', () => {
expect(() => validateNumberFieldOrThrow(NaN, 'testField')).toThrow(
'Invalid number value NaN for field "testField"',
);
});
});
});
@@ -0,0 +1,49 @@
import { validateNumericFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-numeric-field-or-throw.util';
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
describe('validateNumericFieldOrThrow', () => {
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateNumericFieldOrThrow(null, 'testField');
expect(result).toBeNull();
});
it('should return null when value is an empty string', () => {
const result = validateNumericFieldOrThrow('', 'testField');
expect(result).toBeNull();
});
it('should return the number when value is a float', () => {
const result = validateNumericFieldOrThrow(3.14159, 'testField');
expect(result).toBe(3.14159);
});
});
describe('invalid inputs', () => {
it('should throw when value is NaN', () => {
expect(() => validateNumericFieldOrThrow(NaN, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when value is a non-numeric string', () => {
expect(() =>
validateNumericFieldOrThrow('not a number', 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when value is undefined', () => {
expect(() => validateNumericFieldOrThrow(undefined, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when value is an array', () => {
expect(() => validateNumericFieldOrThrow([1, 2, 3], 'testField')).toThrow(
CommonQueryRunnerException,
);
});
});
});
@@ -0,0 +1,88 @@
import { validatePhonesFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-phones-field-or-throw.util';
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
describe('validatePhonesFieldOrThrow', () => {
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validatePhonesFieldOrThrow(null, 'testField');
expect(result).toBeNull();
});
it('should return the phones object when all fields are valid', () => {
const phonesValue = {
primaryPhoneNumber: '+1234567890',
primaryPhoneCountryCode: 'US',
primaryPhoneCallingCode: '+1',
additionalPhones: null,
};
const result = validatePhonesFieldOrThrow(phonesValue, 'testField');
expect(result).toEqual(phonesValue);
});
it('should return the phones object when only primaryPhoneNumber is provided', () => {
const phonesValue = {
primaryPhoneNumber: '+1234567890',
};
const result = validatePhonesFieldOrThrow(phonesValue, 'testField');
expect(result).toEqual(phonesValue);
});
});
describe('invalid inputs', () => {
it('should throw when value is not an object', () => {
expect(() =>
validatePhonesFieldOrThrow('not an object', 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when value is undefined', () => {
expect(() => validatePhonesFieldOrThrow(undefined, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when primaryPhoneNumber is not a string', () => {
const phonesValue = {
primaryPhoneNumber: 123456,
};
expect(() =>
validatePhonesFieldOrThrow(phonesValue, 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when primaryPhoneCountryCode is not a string', () => {
const phonesValue = {
primaryPhoneCountryCode: 123,
};
expect(() =>
validatePhonesFieldOrThrow(phonesValue, 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when primaryPhoneCallingCode is not a string', () => {
const phonesValue = {
primaryPhoneCallingCode: 1,
};
expect(() =>
validatePhonesFieldOrThrow(phonesValue, 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when an invalid subfield is present', () => {
const phonesValue = {
primaryPhoneNumber: '+1234567890',
invalidField: 'invalid',
};
expect(() =>
validatePhonesFieldOrThrow(phonesValue, 'testField'),
).toThrow('Invalid subfield invalidField for phones field "testField"');
});
});
});
@@ -0,0 +1,51 @@
import { validateRatingAndSelectFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rating-and-select-field-or-throw.util';
describe('validateRatingAndSelectFieldOrThrow', () => {
const validOptions = ['option1', 'option2', 'option3'];
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateRatingAndSelectFieldOrThrow(
null,
'testField',
validOptions,
);
expect(result).toBeNull();
});
it('should return the string when value is a valid option', () => {
const result = validateRatingAndSelectFieldOrThrow(
'option1',
'testField',
validOptions,
);
expect(result).toBe('option1');
});
});
describe('invalid inputs', () => {
it('should throw when options is undefined', () => {
expect(() =>
validateRatingAndSelectFieldOrThrow('option1', 'testField', undefined),
).toThrow('Invalid options for field "testField"');
});
it('should throw when value is not in the options list', () => {
expect(() =>
validateRatingAndSelectFieldOrThrow(
'invalidOption',
'testField',
validOptions,
),
).toThrow('Invalid value \'invalidOption\' for field "testField"');
});
it('should throw when value is a number', () => {
expect(() =>
validateRatingAndSelectFieldOrThrow(123, 'testField', validOptions),
).toThrow('Invalid string value 123 for text field "testField"');
});
});
});
@@ -0,0 +1,60 @@
import { validateRawJsonFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util';
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
describe('validateRawJsonFieldOrThrow', () => {
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateRawJsonFieldOrThrow(null, 'testField');
expect(result).toBeNull();
});
it('should return empty object when value is an empty object', () => {
const result = validateRawJsonFieldOrThrow({}, 'testField');
expect(result).toEqual({});
});
it('should return the value when it is a valid JSON object', () => {
const jsonObject = { key: 'value', nested: { prop: 123 } };
const result = validateRawJsonFieldOrThrow(jsonObject, 'testField');
expect(result).toEqual(jsonObject);
});
it('should return the value when it is a valid JSON array', () => {
const jsonArray = [1, 2, 3, 'test', { key: 'value' }];
const result = validateRawJsonFieldOrThrow(jsonArray, 'testField');
expect(result).toEqual(jsonArray);
});
});
describe('invalid inputs', () => {
it('should throw when value is undefined', () => {
expect(() => validateRawJsonFieldOrThrow(undefined, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when value is a function', () => {
const functionValue = () => 'test';
expect(() =>
validateRawJsonFieldOrThrow(functionValue, 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when value is a number', () => {
expect(() => validateRawJsonFieldOrThrow(42, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when value is a string', () => {
expect(() =>
validateRawJsonFieldOrThrow('string value', 'testField'),
).toThrow(CommonQueryRunnerException);
});
});
});
@@ -0,0 +1,53 @@
import { validateRichTextV2FieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rich-text-v2-field-or-throw.util';
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
describe('validateRichTextV2FieldOrThrow', () => {
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateRichTextV2FieldOrThrow(null, 'testField');
expect(result).toBeNull();
});
it('should return null when value is an empty object', () => {
const result = validateRichTextV2FieldOrThrow({}, 'testField');
expect(result).toBeNull();
});
it('should return the value when it has valid subfields', () => {
const value = {
blocknote: 'some blocknote content',
markdown: '# Heading\nContent',
};
const result = validateRichTextV2FieldOrThrow(value, 'testField');
expect(result).toEqual(value);
});
});
describe('invalid inputs', () => {
it('should throw when value is undefined', () => {
expect(() =>
validateRichTextV2FieldOrThrow(undefined, 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when value is a string', () => {
expect(() =>
validateRichTextV2FieldOrThrow('not an object', 'testField'),
).toThrow(CommonQueryRunnerException);
});
it('should throw when value has invalid subfields', () => {
const value = { invalidField: 'value' };
expect(() => validateRichTextV2FieldOrThrow(value, 'testField')).toThrow(
CommonQueryRunnerException,
);
expect(() => validateRichTextV2FieldOrThrow(value, 'testField')).toThrow(
/Should have only blocknote, markdown subfields/,
);
});
});
});
@@ -0,0 +1,69 @@
import { validateTextFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util';
describe('validateTextFieldOrThrow', () => {
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateTextFieldOrThrow(null, 'testField');
expect(result).toBeNull();
});
it('should return empty string when value is an empty string', () => {
const result = validateTextFieldOrThrow('', 'testField');
expect(result).toEqual('');
});
it('should return the string when value is a regular string', () => {
const result = validateTextFieldOrThrow('hello world', 'testField');
expect(result).toBe('hello world');
});
});
describe('invalid inputs', () => {
it('should throw when value is undefined', () => {
expect(() => validateTextFieldOrThrow(undefined, 'testField')).toThrow(
'Invalid string value undefined for text field "testField"',
);
});
it('should throw when value is a number', () => {
expect(() => validateTextFieldOrThrow(123, 'testField')).toThrow(
'Invalid string value 123 for text field "testField"',
);
});
it('should throw when value is a float number', () => {
expect(() => validateTextFieldOrThrow(123.45, 'testField')).toThrow(
'Invalid string value 123.45 for text field "testField"',
);
});
it('should throw when value is a boolean (true)', () => {
expect(() => validateTextFieldOrThrow(true, 'testField')).toThrow(
'Invalid string value true for text field "testField"',
);
});
it('should throw when value is a boolean (false)', () => {
expect(() => validateTextFieldOrThrow(false, 'testField')).toThrow(
'Invalid string value false for text field "testField"',
);
});
it('should throw when value is an array', () => {
expect(() => validateTextFieldOrThrow([1, 2, 3], 'testField')).toThrow(
'Invalid string value [ 1, 2, 3 ] for text field "testField"',
);
});
it('should throw when value is an object', () => {
expect(() =>
validateTextFieldOrThrow({ key: 'value' }, 'testField'),
).toThrow(
'Invalid string value { key: \'value\' } for text field "testField"',
);
});
});
});
@@ -0,0 +1,39 @@
import { validateUUIDFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-uuid-field-or-throw.util';
import { CommonQueryRunnerException } from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
describe('validateUUIDFieldOrThrow', () => {
describe('valid inputs', () => {
it('should return null when value is null', () => {
const result = validateUUIDFieldOrThrow(null, 'testField');
expect(result).toBeNull();
});
it('should return the value when it is a valid UUID v4', () => {
const validUuid = '550e8400-e29b-41d4-a716-446655440000';
const result = validateUUIDFieldOrThrow(validUuid, 'testField');
expect(result).toBe(validUuid);
});
});
describe('invalid inputs', () => {
it('should throw when value is undefined', () => {
expect(() => validateUUIDFieldOrThrow(undefined, 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when value is an empty string', () => {
expect(() => validateUUIDFieldOrThrow('', 'testField')).toThrow(
CommonQueryRunnerException,
);
});
it('should throw when value is an invalid UUID format', () => {
expect(() =>
validateUUIDFieldOrThrow('invalid-uuid', 'testField'),
).toThrow(CommonQueryRunnerException);
});
});
});
@@ -0,0 +1,43 @@
import { isNull } from '@sniptt/guards';
import { FieldActorSource } from 'twenty-shared/types';
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';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateActorFieldOrThrow = (
value: unknown,
fieldName: string,
): { source: FieldActorSource; context: Record<string, unknown> } | null => {
const preValidatedValue = validateRawJsonFieldOrThrow(value, fieldName);
if (isNull(preValidatedValue)) return null;
for (const [subField, subFieldValue] of Object.entries(preValidatedValue)) {
switch (subField) {
case 'source':
validateRatingAndSelectFieldOrThrow(
subFieldValue,
`${fieldName}.${subField}`,
Object.keys(FieldActorSource),
);
break;
case 'context':
validateRawJsonFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
default:
throw new CommonQueryRunnerException(
`Invalid subfield ${subField} for actor field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
}
return value as {
source: FieldActorSource;
context: Record<string, unknown>;
};
};
@@ -0,0 +1,72 @@
import { isNull } from '@sniptt/guards';
import { validateNumericFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-numeric-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';
import { validateTextFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateAddressFieldOrThrow = (
value: unknown,
fieldName: string,
): {
addressStreet1?: string | null;
addressStreet2?: string | null;
addressCity?: string | null;
addressState?: string | null;
addressPostcode?: string | null;
addressCountry?: string | null;
addressLat?: number | null;
addressLng?: number | null;
} | null => {
const preValidatedValue = validateRawJsonFieldOrThrow(value, fieldName);
if (isNull(preValidatedValue)) return null;
for (const [subField, subFieldValue] of Object.entries(preValidatedValue)) {
switch (subField) {
case 'addressStreet1':
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
case 'addressStreet2':
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
case 'addressCity':
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
case 'addressState':
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
case 'addressPostcode':
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
case 'addressCountry':
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
case 'addressLat':
validateNumericFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
case 'addressLng':
validateNumericFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
default:
throw new CommonQueryRunnerException(
`Invalid subfield ${subField} for address field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
}
return value as {
addressStreet1?: string | null;
addressStreet2?: string | null;
addressCity?: string | null;
addressState?: string | null;
addressPostcode?: string | null;
addressCountry?: string | null;
addressLat?: number | null;
addressLng?: number | null;
};
};
@@ -0,0 +1,26 @@
import { inspect } from 'util';
import { isNull } from '@sniptt/guards';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateArrayFieldOrThrow = (
value: unknown,
fieldName: string,
): string | string[] | null => {
if (isNull(value)) return null;
if (typeof value === 'string') return value;
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
throw new CommonQueryRunnerException(
`Invalid value ${inspect(value)} for field "${fieldName} - Array values need to be string"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
return value;
};
@@ -0,0 +1,21 @@
import { inspect } from 'util';
import { isNull } from '@sniptt/guards';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateBooleanFieldOrThrow = (
value: unknown,
fieldName: string,
): boolean | null => {
if (typeof value !== 'boolean' && !isNull(value))
throw new CommonQueryRunnerException(
`Invalid boolean value ${inspect(value)} for field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
return value;
};
@@ -0,0 +1,43 @@
import { isNull } from '@sniptt/guards';
import { validateNumericFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-numeric-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';
import { validateTextFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateCurrencyFieldOrThrow = (
value: unknown,
fieldName: string,
): {
amountMicros?: number | string | null;
currencyCode?: string | null;
} | null => {
const preValidatedValue = validateRawJsonFieldOrThrow(value, fieldName);
if (isNull(preValidatedValue)) return null;
for (const [subField, subFieldValue] of Object.entries(preValidatedValue)) {
switch (subField) {
case 'amountMicros':
validateNumericFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
case 'currencyCode':
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
default:
throw new CommonQueryRunnerException(
`Invalid subfield ${subField} for currency field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
}
return value as {
amountMicros?: number | string | null;
currencyCode?: string | null;
};
};
@@ -0,0 +1,26 @@
import { inspect } from 'util';
import { isDate, isNull, isNumber, isString } from '@sniptt/guards';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateDateAndDateTimeFieldOrThrow = (
value: unknown,
fieldName: string,
) => {
if (isNull(value)) return null;
if (isString(value) || isNumber(value) || isDate(value)) {
const date = new Date(value);
if (!isNaN(date.getTime())) return value;
}
throw new CommonQueryRunnerException(
`Invalid value ${inspect(value)} for date or date-time field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
};
@@ -0,0 +1,42 @@
import { isNull } from '@sniptt/guards';
import { validateArrayFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-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';
import { validateTextFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateEmailsFieldOrThrow = (
value: unknown,
fieldName: string,
): {
primaryEmail?: string | null;
additionalEmails?: string[] | null;
} | null => {
const preValidatedValue = validateRawJsonFieldOrThrow(value, fieldName);
if (isNull(preValidatedValue)) return null;
for (const [subField, subFieldValue] of Object.entries(preValidatedValue)) {
switch (subField) {
case 'primaryEmail':
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
case 'additionalEmails':
validateArrayFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
default:
throw new CommonQueryRunnerException(
`Invalid subfield ${subField} for emails field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
}
return value as {
primaryEmail?: string | null;
additionalEmails?: string[] | null;
};
};
@@ -0,0 +1,41 @@
import { isNull } from '@sniptt/guards';
import { validateRawJsonFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util';
import { validateTextFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateFullNameFieldOrThrow = (
value: unknown,
fieldName: string,
): {
firstName?: string | null;
lastName?: string | null;
} | null => {
const preValidatedValue = validateRawJsonFieldOrThrow(value, fieldName);
if (isNull(preValidatedValue)) return null;
for (const [subField, subFieldValue] of Object.entries(preValidatedValue)) {
switch (subField) {
case 'firstName':
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
case 'lastName':
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
default:
throw new CommonQueryRunnerException(
`Invalid subfield ${subField} for full name field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
}
return value as {
firstName?: string | null;
lastName?: string | null;
};
};
@@ -0,0 +1,39 @@
import { isNull } from '@sniptt/guards';
import { validateRawJsonFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util';
import { validateTextFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { type LinksFieldGraphQLInput } from 'src/engine/core-modules/record-transformer/utils/transform-links-value.util';
export const validateLinksFieldOrThrow = (
value: unknown,
fieldName: string,
): LinksFieldGraphQLInput | null => {
const preValidatedValue = validateRawJsonFieldOrThrow(value, fieldName);
if (isNull(preValidatedValue)) return null;
for (const [subField, subFieldValue] of Object.entries(preValidatedValue)) {
switch (subField) {
case 'primaryLinkUrl':
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
case 'primaryLinkLabel':
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
case 'secondaryLinks':
validateRawJsonFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
default:
throw new CommonQueryRunnerException(
`Invalid subfield ${subField} for links field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
}
return value as LinksFieldGraphQLInput;
};
@@ -0,0 +1,41 @@
import { inspect } from 'util';
import { isNull } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { validateArrayFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-array-field-or-throw.util';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateMultiSelectFieldOrThrow = (
value: unknown,
fieldName: string,
options?: string[],
): string | string[] | null => {
const preValidatedValue = validateArrayFieldOrThrow(value, fieldName);
if (isNull(preValidatedValue)) return null;
if (!isDefined(options)) {
throw new CommonQueryRunnerException(
`Invalid options for field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
if (
(Array.isArray(preValidatedValue)
? preValidatedValue
: [preValidatedValue]
).some((item) => !options.includes(item))
) {
throw new CommonQueryRunnerException(
`Invalid value ${inspect(value)} for multi select field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
return value as string | string[];
};
@@ -0,0 +1,25 @@
import { inspect } from 'util';
import { isNull } from '@sniptt/guards';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateNumberFieldOrThrow = (
value: unknown,
fieldName: string,
): number | null => {
if (
(typeof value !== 'number' && !isNull(value)) ||
(typeof value === 'number' &&
(isNaN(value) || value === Infinity || value === -Infinity))
)
throw new CommonQueryRunnerException(
`Invalid number value ${inspect(value)} for field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
return value;
};
@@ -0,0 +1,18 @@
import { isNull } from '@sniptt/guards';
import { validateNumberFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-number-field-or-throw.util';
//Need to handle stringified numbers because of BigFloatScalarType custom gql type
export const validateNumericFieldOrThrow = (
value: unknown,
fieldName: string,
): number | string | null => {
if (value === '' || isNull(value)) return null;
const numberValue = Number(value);
validateNumberFieldOrThrow(numberValue, fieldName);
return value as number | string;
};
@@ -0,0 +1,43 @@
import { isNull } from '@sniptt/guards';
import { validateRawJsonFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-raw-json-field-or-throw.util';
import { validateTextFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { type PhonesFieldGraphQLInput } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util';
export const validatePhonesFieldOrThrow = (
value: unknown,
fieldName: string,
): PhonesFieldGraphQLInput => {
const preValidatedValue = validateRawJsonFieldOrThrow(value, fieldName);
if (isNull(preValidatedValue)) return null;
for (const [subField, subFieldValue] of Object.entries(preValidatedValue)) {
switch (subField) {
case 'primaryPhoneNumber':
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
case 'primaryPhoneCountryCode':
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
case 'primaryPhoneCallingCode':
validateTextFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
case 'additionalPhones':
validateRawJsonFieldOrThrow(subFieldValue, `${fieldName}.${subField}`);
break;
default:
throw new CommonQueryRunnerException(
`Invalid subfield ${subField} for phones field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
}
return value as PhonesFieldGraphQLInput;
};
@@ -0,0 +1,34 @@
import { inspect } from 'util';
import { isNull } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { validateTextFieldOrThrow } from 'src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-text-field-or-throw.util';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateRatingAndSelectFieldOrThrow = (
value: unknown,
fieldName: string,
options?: string[],
): string | null => {
const preValidatedValue = validateTextFieldOrThrow(value, fieldName);
if (!isDefined(options)) {
throw new CommonQueryRunnerException(
`Invalid options for field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
if (!isNull(preValidatedValue) && !options.includes(preValidatedValue)) {
throw new CommonQueryRunnerException(
`Invalid value ${inspect(value)} for field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
return preValidatedValue;
};
@@ -0,0 +1,24 @@
import { inspect } from 'util';
import { isNull, isObject } from '@sniptt/guards';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateRawJsonFieldOrThrow = (
value: unknown,
fieldName: string,
): object | null => {
if (isNull(value)) return null;
if (!isObject(value)) {
throw new CommonQueryRunnerException(
`Invalid object value ${inspect(value)} for field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
return value;
};
@@ -0,0 +1,53 @@
import { inspect } from 'util';
import { isNull, isObject } from '@sniptt/guards';
import {
compositeTypeDefinitions,
FieldMetadataType,
} from 'twenty-shared/types';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateRichTextV2FieldOrThrow = (
value: unknown,
fieldName: string,
): {
blocknote: string | null | undefined;
markdown: string | null | undefined;
} | null => {
if (isNull(value)) return null;
try {
const parsedValue = JSON.parse(JSON.stringify(value));
if (!isObject(parsedValue)) throw new Error('Should be an object');
if (Object.keys(parsedValue).length === 0) return null;
const subfields = Object.keys(parsedValue);
const richTextV2Subfields = compositeTypeDefinitions
.get(FieldMetadataType.RICH_TEXT_V2)
?.properties.filter(
(prop) => prop.hidden !== true && prop.hidden !== 'input',
)
.map((prop) => prop.name);
if (!subfields.every((subfield) => richTextV2Subfields?.includes(subfield)))
throw new Error(
`Should have only ${richTextV2Subfields?.join(', ')} subfields`,
);
return value as {
blocknote: string | null | undefined;
markdown: string | null | undefined;
};
} catch (error) {
throw new CommonQueryRunnerException(
`Invalid rich text v2 value ${inspect(value)} for field "${fieldName}" - ${error.message}`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
}
};
@@ -0,0 +1,21 @@
import { inspect } from 'util';
import { isNull } from '@sniptt/guards';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateTextFieldOrThrow = (
value: unknown,
fieldName: string,
): string | null => {
if (typeof value !== 'string' && !isNull(value))
throw new CommonQueryRunnerException(
`Invalid string value ${inspect(value)} for text field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
return value;
};
@@ -0,0 +1,22 @@
import { inspect } from 'util';
import { isNull } from '@sniptt/guards';
import { isValidUuid } from 'twenty-shared/utils';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateUUIDFieldOrThrow = (
value: unknown,
fieldName: string,
): string | null => {
if (!isValidUuid(value as string) && !isNull(value))
throw new CommonQueryRunnerException(
`Invalid UUID value ${inspect(value)} for field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA,
);
return value as string;
};
@@ -0,0 +1,107 @@
import { Injectable } from '@nestjs/common';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
@Injectable()
export class QueryRunnerArgsFactory {
constructor(
private readonly recordPositionService: RecordPositionService,
private readonly recordInputTransformerService: RecordInputTransformerService,
) {}
public overrideFilterByFieldMetadata<
T extends ObjectRecordFilter | undefined,
>(
filter: T,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
): T {
if (!isDefined(filter)) {
return filter;
}
const overrideFilter = (filterObject: ObjectRecordFilter) => {
return Object.entries(filterObject).reduce((acc, [key, value]) => {
if (key === 'and' || key === 'or') {
// @ts-expect-error legacy noImplicitAny
acc[key] = value.map((nestedFilter: ObjectRecordFilter) =>
overrideFilter(nestedFilter),
);
} else if (key === 'not') {
// @ts-expect-error legacy noImplicitAny
acc[key] = overrideFilter(value);
} else {
// @ts-expect-error legacy noImplicitAny
acc[key] = this.transformFilterValueByType(
key,
value,
objectMetadataItemWithFieldMaps,
);
}
return acc;
}, {});
};
return overrideFilter(filter) as T;
}
private transformFilterValueByType(
key: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
) {
const fieldMetadataId = objectMetadataItemWithFieldMaps.fieldIdByName[key];
const fieldMetadata =
objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
if (!fieldMetadata) {
return value;
}
// Special handling for filter values, which have a specific structure
switch (fieldMetadata.type) {
case FieldMetadataType.NUMBER: {
if (value?.is === 'NULL') {
return value;
} else {
return Object.fromEntries(
Object.entries(value).map(([filterKey, filterValue]) => [
filterKey,
Number(filterValue),
]),
);
}
}
default:
return value;
}
}
async overrideValueByFieldMetadata(
key: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any,
fieldMetadataMapByName: FieldMetadataMap,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
) {
const fieldMetadata = fieldMetadataMapByName[key];
if (!fieldMetadata) {
return value;
}
return this.recordInputTransformerService.process({
recordInput: { [key]: value },
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
});
}
}
@@ -6,7 +6,8 @@ import { Omit } from 'zod/v4/core/util.cjs';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
import { CommonSelectedFieldsHandler } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler';
import { DataArgProcessor } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor';
import { QueryRunnerArgsFactory } from 'src/engine/api/common/common-args-processors/query-runner-args.factory';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
@@ -25,8 +26,6 @@ import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace
import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory';
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
import { WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
@@ -62,7 +61,7 @@ export abstract class CommonBaseQueryRunnerService<
@Inject()
protected readonly queryRunnerArgsFactory: QueryRunnerArgsFactory;
@Inject()
protected readonly queryResultGettersFactory: QueryResultGettersFactory;
protected readonly dataArgProcessor: DataArgProcessor;
@Inject()
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager;
@Inject()
@@ -76,8 +75,6 @@ export abstract class CommonBaseQueryRunnerService<
@Inject()
protected readonly apiKeyRoleService: ApiKeyRoleService;
@Inject()
protected readonly selectedFieldsHandler: CommonSelectedFieldsHandler;
@Inject()
protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService;
@Inject()
protected readonly commonResultGettersService: CommonResultGettersService;
@@ -191,18 +188,19 @@ export abstract class CommonBaseQueryRunnerService<
);
const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext;
const computedArgs = await this.computeArgs(args, queryRunnerContext);
const hookedArgs =
(await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
operationName,
args as WorkspacePreQueryHookPayload<CommonQueryNames>,
computedArgs as WorkspacePreQueryHookPayload<CommonQueryNames>,
)) as CommonInput<Args>;
const computedArgs = await this.computeArgs(hookedArgs, queryRunnerContext);
return {
...computedArgs,
...hookedArgs,
selectedFieldsResult,
};
}
@@ -140,7 +140,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
return {
...args,
data: await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({
data: await this.dataArgProcessor.process({
partialRecordInputs: args.data,
authContext,
objectMetadataItemWithFieldMaps,
@@ -53,15 +53,15 @@ export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerServ
): Promise<CommonInput<CreateOneQueryArgs>> {
const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext;
const coercedData = await this.dataArgProcessor.process({
partialRecordInputs: [args.data],
authContext,
objectMetadataItemWithFieldMaps,
});
return {
...args,
data: (
await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({
partialRecordInputs: [args.data],
authContext,
objectMetadataItemWithFieldMaps,
})
)[0],
data: coercedData[0],
};
}
@@ -171,7 +171,7 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne
),
) ?? [],
),
data: await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({
data: await this.dataArgProcessor.process({
partialRecordInputs: args.data,
authContext,
objectMetadataItemWithFieldMaps,
@@ -29,10 +29,6 @@ import {
CommonQueryNames,
MergeManyQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util';
@@ -174,9 +170,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ
);
if (!priorityRecord) {
throw new GraphqlQueryRunnerException(
throw new CommonQueryRunnerException(
'Priority record not found',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
@@ -106,7 +106,7 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer
objectMetadataItemWithFieldMaps,
) || {},
data: (
await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({
await this.dataArgProcessor.process({
partialRecordInputs: [args.data],
authContext,
objectMetadataItemWithFieldMaps,
@@ -65,7 +65,7 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ
return {
...args,
data: (
await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({
await this.dataArgProcessor.process({
partialRecordInputs: [args.data],
authContext,
objectMetadataItemWithFieldMaps,
@@ -7,6 +7,7 @@ export enum CommonQueryRunnerExceptionCode {
INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT',
INVALID_AUTH_CONTEXT = 'INVALID_AUTH_CONTEXT',
ARGS_CONFLICT = 'ARGS_CONFLICT',
INVALID_ARGS_DATA = 'INVALID_ARGS_DATA',
INVALID_ARGS_FIRST = 'INVALID_ARGS_FIRST',
INVALID_ARGS_LAST = 'INVALID_ARGS_LAST',
UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT = 'UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT',
@@ -20,6 +20,7 @@ export const commonQueryRunnerToGraphqlApiExceptionHandler = (
case CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST:
case CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST:
case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
case CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA:
case CommonQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT:
case CommonQueryRunnerExceptionCode.INVALID_CURSOR:
case CommonQueryRunnerExceptionCode.UPSERT_MAX_RECORDS_EXCEEDED:
@@ -19,6 +19,7 @@ export const commonQueryRunnerToRestApiExceptionHandler = (
case CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST:
case CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST:
case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
case CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA:
case CommonQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT:
case CommonQueryRunnerExceptionCode.INVALID_CURSOR:
case CommonQueryRunnerExceptionCode.UPSERT_MAX_RECORDS_EXCEEDED:
@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CommonArgsHandlers } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-arg-handlers';
import { CommonArgsProcessors } from 'src/engine/api/common/common-args-processors/common-args-processors';
import { CommonQueryRunners } from 'src/engine/api/common/common-query-runners/common-query-runners';
import { CommonResultGettersService } from 'src/engine/api/common/common-result-getters/common-result-getters.service';
import { GroupByWithRecordsService } from 'src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service';
@@ -14,6 +14,8 @@ import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
@@ -39,13 +41,15 @@ import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-wo
ViewFilterGroupModule,
ThrottlerModule,
MetricsModule,
RecordPositionModule,
RecordTransformerModule,
GlobalWorkspaceDataSourceModule,
FeatureFlagModule,
],
providers: [
ProcessNestedRelationsHelper,
ProcessNestedRelationsV2Helper,
...CommonArgsHandlers,
...CommonArgsProcessors,
ProcessAggregateHelper,
...CommonQueryRunners,
CommonResultGettersService,
@@ -7,6 +7,5 @@ export interface CommonSelectedFields {
export type CommonSelectedFieldsResult = {
select: CommonSelectedFields;
relations: CommonSelectedFields;
//TODO = Refacto-common - to update when rest api will handle aggregates
aggregate: Record<string, AggregationField>;
};