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:
-3
@@ -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];
|
||||
+4
@@ -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
|
||||
+271
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
+45
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
+27
@@ -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']);
|
||||
});
|
||||
});
|
||||
+54
@@ -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 });
|
||||
});
|
||||
});
|
||||
+33
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
+21
@@ -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);
|
||||
});
|
||||
});
|
||||
+21
@@ -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();
|
||||
});
|
||||
});
|
||||
+21
@@ -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');
|
||||
});
|
||||
});
|
||||
+24
@@ -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),
|
||||
};
|
||||
};
|
||||
+56
@@ -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),
|
||||
};
|
||||
};
|
||||
+14
@@ -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;
|
||||
};
|
||||
+26
@@ -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),
|
||||
};
|
||||
};
|
||||
+25
@@ -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),
|
||||
};
|
||||
};
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { isNull } from '@sniptt/guards';
|
||||
|
||||
export const transformNumericField = (
|
||||
value: number | string | null,
|
||||
): number | null => {
|
||||
return isNull(value) ? null : Number(value);
|
||||
};
|
||||
+12
@@ -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;
|
||||
};
|
||||
+8
@@ -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;
|
||||
};
|
||||
+84
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+159
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+55
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+70
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+73
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+97
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+87
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+70
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+88
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+73
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+87
@@ -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"',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+49
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+88
@@ -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"');
|
||||
});
|
||||
});
|
||||
});
|
||||
+51
@@ -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"');
|
||||
});
|
||||
});
|
||||
});
|
||||
+60
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+53
@@ -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/,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+69
@@ -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"',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+39
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+43
@@ -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>;
|
||||
};
|
||||
};
|
||||
+72
@@ -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;
|
||||
};
|
||||
};
|
||||
+26
@@ -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;
|
||||
};
|
||||
+21
@@ -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;
|
||||
};
|
||||
+43
@@ -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;
|
||||
};
|
||||
};
|
||||
+26
@@ -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,
|
||||
);
|
||||
};
|
||||
+42
@@ -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;
|
||||
};
|
||||
};
|
||||
+41
@@ -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;
|
||||
};
|
||||
};
|
||||
+39
@@ -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;
|
||||
};
|
||||
+41
@@ -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[];
|
||||
};
|
||||
+25
@@ -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;
|
||||
};
|
||||
+18
@@ -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;
|
||||
};
|
||||
+43
@@ -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;
|
||||
};
|
||||
+34
@@ -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;
|
||||
};
|
||||
+24
@@ -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;
|
||||
};
|
||||
+53
@@ -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,
|
||||
);
|
||||
}
|
||||
};
|
||||
+21
@@ -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;
|
||||
};
|
||||
+22
@@ -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;
|
||||
};
|
||||
+107
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
+8
-10
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+7
-7
@@ -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],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -171,7 +171,7 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne
|
||||
),
|
||||
) ?? [],
|
||||
),
|
||||
data: await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({
|
||||
data: await this.dataArgProcessor.process({
|
||||
partialRecordInputs: args.data,
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
|
||||
+2
-6
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
objectMetadataItemWithFieldMaps,
|
||||
) || {},
|
||||
data: (
|
||||
await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({
|
||||
await this.dataArgProcessor.process({
|
||||
partialRecordInputs: [args.data],
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
@@ -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',
|
||||
|
||||
+1
@@ -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:
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
-1
@@ -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>;
|
||||
};
|
||||
|
||||
-224
@@ -1,224 +0,0 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { ResolverArgsType } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
||||
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';
|
||||
|
||||
describe('QueryRunnerArgsFactory', () => {
|
||||
const recordPositionService = {
|
||||
overridePositionOnRecords: jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
({ partialRecordInputs }: { partialRecordInputs: any[] }) => {
|
||||
return Promise.resolve(
|
||||
partialRecordInputs.map((record: any) => ({
|
||||
...record,
|
||||
position:
|
||||
record.position === 'last' || !record.position
|
||||
? 2
|
||||
: record.position,
|
||||
})),
|
||||
);
|
||||
},
|
||||
),
|
||||
};
|
||||
const workspaceId = 'workspaceId';
|
||||
const options = {
|
||||
authContext: { workspace: { id: workspaceId } },
|
||||
objectMetadataItemWithFieldMaps: {
|
||||
isCustom: true,
|
||||
nameSingular: 'testNumber',
|
||||
fieldsById: {
|
||||
'position-id': {
|
||||
type: FieldMetadataType.POSITION,
|
||||
isCustom: true,
|
||||
name: 'position',
|
||||
},
|
||||
'testNumber-id': {
|
||||
type: FieldMetadataType.NUMBER,
|
||||
isCustom: true,
|
||||
name: 'testNumber',
|
||||
},
|
||||
'otherField-id': {
|
||||
type: FieldMetadataType.TEXT,
|
||||
isCustom: true,
|
||||
name: 'otherField',
|
||||
},
|
||||
} as unknown as FieldMetadataMap,
|
||||
fieldIdByName: {
|
||||
position: 'position-id',
|
||||
testNumber: 'testNumber-id',
|
||||
otherField: 'otherField-id',
|
||||
},
|
||||
fieldIdByJoinColumnName: {},
|
||||
},
|
||||
} as unknown as WorkspaceQueryRunnerOptions;
|
||||
|
||||
let factory: QueryRunnerArgsFactory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
QueryRunnerArgsFactory,
|
||||
RecordInputTransformerService,
|
||||
{
|
||||
provide: RecordPositionService,
|
||||
useValue: recordPositionService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
factory = module.get<QueryRunnerArgsFactory>(QueryRunnerArgsFactory);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(factory).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should simply return the args when data is an empty array', async () => {
|
||||
const args = {
|
||||
data: [],
|
||||
};
|
||||
const result = await factory.create(
|
||||
args,
|
||||
options,
|
||||
ResolverArgsType.CREATE_MANY,
|
||||
);
|
||||
|
||||
expect(result).toEqual(args);
|
||||
});
|
||||
|
||||
it('createMany type should override data position and number', async () => {
|
||||
const args = {
|
||||
id: 'uuid',
|
||||
data: [{ position: 'last', testNumber: 1 }],
|
||||
};
|
||||
|
||||
const result = await factory.create(
|
||||
args,
|
||||
options,
|
||||
ResolverArgsType.CREATE_MANY,
|
||||
);
|
||||
|
||||
const expectedArgs = {
|
||||
partialRecordInputs: [{ position: 'last', testNumber: 1 }],
|
||||
objectMetadata: {
|
||||
isCustom: true,
|
||||
nameSingular: 'testNumber',
|
||||
fieldIdByName: {
|
||||
position: 'position-id',
|
||||
testNumber: 'testNumber-id',
|
||||
otherField: 'otherField-id',
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
shouldBackfillPositionIfUndefined: true,
|
||||
};
|
||||
|
||||
expect(
|
||||
recordPositionService.overridePositionOnRecords,
|
||||
).toHaveBeenCalledWith(expectedArgs);
|
||||
expect(result).toEqual({
|
||||
id: 'uuid',
|
||||
data: [{ position: 2, testNumber: 1 }],
|
||||
});
|
||||
});
|
||||
|
||||
it('createMany type should override position if not present', async () => {
|
||||
const args = {
|
||||
id: 'uuid',
|
||||
data: [{ testNumber: 1 }],
|
||||
};
|
||||
|
||||
const result = await factory.create(
|
||||
args,
|
||||
options,
|
||||
ResolverArgsType.CREATE_MANY,
|
||||
);
|
||||
|
||||
const expectedArgs = {
|
||||
partialRecordInputs: [{ testNumber: 1 }],
|
||||
objectMetadata: {
|
||||
isCustom: true,
|
||||
nameSingular: 'testNumber',
|
||||
fieldIdByName: {
|
||||
position: 'position-id',
|
||||
testNumber: 'testNumber-id',
|
||||
otherField: 'otherField-id',
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
shouldBackfillPositionIfUndefined: true,
|
||||
};
|
||||
|
||||
expect(
|
||||
recordPositionService.overridePositionOnRecords,
|
||||
).toHaveBeenCalledWith(expectedArgs);
|
||||
expect(result).toEqual({
|
||||
id: 'uuid',
|
||||
data: [{ position: 2, testNumber: 1 }],
|
||||
});
|
||||
});
|
||||
|
||||
it('findMany type should override data position and number', async () => {
|
||||
const args = {
|
||||
id: 'uuid',
|
||||
filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } },
|
||||
};
|
||||
|
||||
const result = await factory.create(
|
||||
args,
|
||||
options,
|
||||
ResolverArgsType.FIND_MANY,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 'uuid',
|
||||
filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('findOne type should override number in filter', async () => {
|
||||
const args = {
|
||||
id: 'uuid',
|
||||
filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } },
|
||||
};
|
||||
|
||||
const result = await factory.create(
|
||||
args,
|
||||
options,
|
||||
ResolverArgsType.FIND_ONE,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 'uuid',
|
||||
filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('findDuplicates type should override number in data and id', async () => {
|
||||
const args = {
|
||||
ids: [123],
|
||||
data: [{ testNumber: 1, otherField: 'test' }],
|
||||
};
|
||||
|
||||
const result = await factory.create(
|
||||
args,
|
||||
options,
|
||||
ResolverArgsType.FIND_DUPLICATES,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
ids: [123],
|
||||
data: [{ testNumber: 1, position: 2, otherField: 'test' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
import { QueryRunnerArgsFactory } from './query-runner-args.factory';
|
||||
|
||||
import { QueryResultGettersFactory } from './query-result-getters/query-result-getters.factory';
|
||||
|
||||
export const workspaceQueryRunnerFactories = [
|
||||
QueryRunnerArgsFactory,
|
||||
QueryResultGettersFactory,
|
||||
];
|
||||
-237
@@ -1,237 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
|
||||
import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface';
|
||||
import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||
import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
|
||||
|
||||
import { isQueryResultFieldValueAConnection } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-connection.guard';
|
||||
import { isQueryResultFieldValueANestedRecordArray } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-nested-record-array.guard';
|
||||
import { isQueryResultFieldValueARecordArray } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-record-array.guard';
|
||||
import { isQueryResultFieldValueARecord } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/guards/is-query-result-field-value-a-record.guard';
|
||||
import { ActivityQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler';
|
||||
import { AttachmentQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler';
|
||||
import { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler';
|
||||
import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler';
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
|
||||
// TODO: find a way to prevent conflict between handlers executing logic on object relations
|
||||
// And this factory that is also executing logic on object relations
|
||||
// Right now the factory will override any change made on relations by the handlers
|
||||
@Injectable()
|
||||
export class QueryResultGettersFactory {
|
||||
private readonly logger = new Logger(QueryResultGettersFactory.name);
|
||||
private handlers: Map<string, QueryResultGetterHandlerInterface>;
|
||||
|
||||
constructor(private readonly fileService: FileService) {
|
||||
this.initializeHandlers();
|
||||
}
|
||||
|
||||
private initializeHandlers() {
|
||||
this.handlers = new Map<string, QueryResultGetterHandlerInterface>([
|
||||
['attachment', new AttachmentQueryResultGetterHandler(this.fileService)],
|
||||
['person', new PersonQueryResultGetterHandler(this.fileService)],
|
||||
[
|
||||
'workspaceMember',
|
||||
new WorkspaceMemberQueryResultGetterHandler(this.fileService),
|
||||
],
|
||||
['note', new ActivityQueryResultGetterHandler(this.fileService)],
|
||||
['task', new ActivityQueryResultGetterHandler(this.fileService)],
|
||||
]);
|
||||
}
|
||||
|
||||
private async processConnection(
|
||||
connection: IConnection<ObjectRecord>,
|
||||
objectMetadataItemId: string,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
workspaceId: string,
|
||||
): Promise<IConnection<ObjectRecord>> {
|
||||
return {
|
||||
...connection,
|
||||
edges: await Promise.all(
|
||||
connection.edges.map(async (edge: IEdge<ObjectRecord>) => ({
|
||||
...edge,
|
||||
node: await this.processRecord(
|
||||
edge.node,
|
||||
objectMetadataItemId,
|
||||
objectMetadataMaps,
|
||||
workspaceId,
|
||||
),
|
||||
})),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private async processNestedRecordArray(
|
||||
result: { records: ObjectRecord[] },
|
||||
objectMetadataItemId: string,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
workspaceId: string,
|
||||
) {
|
||||
return {
|
||||
...result,
|
||||
records: await Promise.all(
|
||||
result.records.map(
|
||||
async (record: ObjectRecord) =>
|
||||
await this.processRecord(
|
||||
record,
|
||||
objectMetadataItemId,
|
||||
objectMetadataMaps,
|
||||
workspaceId,
|
||||
),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private async processRecordArray(
|
||||
recordArray: ObjectRecord[],
|
||||
objectMetadataItemId: string,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
workspaceId: string,
|
||||
) {
|
||||
return await Promise.all(
|
||||
recordArray.map(
|
||||
async (record: ObjectRecord) =>
|
||||
await this.processRecord(
|
||||
record,
|
||||
objectMetadataItemId,
|
||||
objectMetadataMaps,
|
||||
workspaceId,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async processRecord(
|
||||
record: ObjectRecord,
|
||||
objectMetadataItemId: string,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
workspaceId: string,
|
||||
): Promise<ObjectRecord> {
|
||||
const objectMetadataMapItem = objectMetadataMaps.byId[objectMetadataItemId];
|
||||
|
||||
if (!isDefined(objectMetadataMapItem)) {
|
||||
throw new Error('Object metadata map item is not defined');
|
||||
}
|
||||
|
||||
const handler = this.getHandler(objectMetadataMapItem.nameSingular);
|
||||
|
||||
const relationFields = Object.keys(record)
|
||||
.map(
|
||||
(recordFieldName) =>
|
||||
objectMetadataMapItem.fieldsById[
|
||||
objectMetadataMapItem.fieldIdByName[recordFieldName]
|
||||
],
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((fieldMetadata) =>
|
||||
isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION),
|
||||
);
|
||||
|
||||
const relationFieldsProcessedMap = {} as Record<
|
||||
string,
|
||||
QueryResultFieldValue
|
||||
>;
|
||||
|
||||
for (const relationField of relationFields) {
|
||||
if (!isDefined(relationField.relationTargetObjectMetadataId)) {
|
||||
throw new Error('Relation target object metadata id is not defined');
|
||||
}
|
||||
|
||||
relationFieldsProcessedMap[relationField.name] =
|
||||
await this.processQueryResultField(
|
||||
record[relationField.name],
|
||||
relationField.relationTargetObjectMetadataId,
|
||||
objectMetadataMaps,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
const objectRecordProcessedWithoutRelationFields = await handler.handle(
|
||||
record,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const processedRecord = {
|
||||
...objectRecordProcessedWithoutRelationFields,
|
||||
...relationFieldsProcessedMap,
|
||||
};
|
||||
|
||||
return processedRecord;
|
||||
}
|
||||
|
||||
private async processQueryResultField(
|
||||
queryResultField: QueryResultFieldValue,
|
||||
objectMetadataItemId: string,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
workspaceId: string,
|
||||
) {
|
||||
if (isQueryResultFieldValueAConnection(queryResultField)) {
|
||||
return await this.processConnection(
|
||||
queryResultField,
|
||||
objectMetadataItemId,
|
||||
objectMetadataMaps,
|
||||
workspaceId,
|
||||
);
|
||||
} else if (isQueryResultFieldValueANestedRecordArray(queryResultField)) {
|
||||
return await this.processNestedRecordArray(
|
||||
queryResultField,
|
||||
objectMetadataItemId,
|
||||
objectMetadataMaps,
|
||||
workspaceId,
|
||||
);
|
||||
} else if (isQueryResultFieldValueARecordArray(queryResultField)) {
|
||||
return await this.processRecordArray(
|
||||
queryResultField,
|
||||
objectMetadataItemId,
|
||||
objectMetadataMaps,
|
||||
workspaceId,
|
||||
);
|
||||
} else if (isQueryResultFieldValueARecord(queryResultField)) {
|
||||
return await this.processRecord(
|
||||
queryResultField,
|
||||
objectMetadataItemId,
|
||||
objectMetadataMaps,
|
||||
workspaceId,
|
||||
);
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`Query result field is not a record, connection, nested record array or record array.
|
||||
This is an undetected case in query result getter that should be implemented !!`,
|
||||
);
|
||||
|
||||
return queryResultField;
|
||||
}
|
||||
}
|
||||
|
||||
async create(
|
||||
result: QueryResultFieldValue,
|
||||
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
|
||||
workspaceId: string,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): Promise<any> {
|
||||
return await this.processQueryResultField(
|
||||
result,
|
||||
objectMetadataItem.id,
|
||||
objectMetadataMaps,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
private getHandler(objectType: string): QueryResultGetterHandlerInterface {
|
||||
return (
|
||||
this.handlers.get(objectType) || {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
handle: (result: any) => result,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
-323
@@ -1,323 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataType, ObjectRecord } from 'twenty-shared/types';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import {
|
||||
type CreateManyResolverArgs,
|
||||
type CreateOneResolverArgs,
|
||||
type FindDuplicatesResolverArgs,
|
||||
type FindManyResolverArgs,
|
||||
type FindOneResolverArgs,
|
||||
GroupByResolverArgs,
|
||||
type MergeManyResolverArgs,
|
||||
type ResolverArgs,
|
||||
ResolverArgsType,
|
||||
type UpdateManyResolverArgs,
|
||||
type UpdateOneResolverArgs,
|
||||
type WorkspaceResolverBuilderMethodNames,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
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 { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
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,
|
||||
) {}
|
||||
|
||||
async create(
|
||||
args: ResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
resolverArgsType: WorkspaceResolverBuilderMethodNames,
|
||||
) {
|
||||
const fieldMetadataMapByNameByName =
|
||||
options.objectMetadataItemWithFieldMaps.fieldsById;
|
||||
|
||||
const { objectMetadataItemWithFieldMaps, authContext } = options;
|
||||
|
||||
switch (resolverArgsType) {
|
||||
case ResolverArgsType.CREATE_ONE:
|
||||
return {
|
||||
...args,
|
||||
data: (
|
||||
await this.overrideDataByFieldMetadata({
|
||||
partialRecordInputs: [(args as CreateOneResolverArgs).data],
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
})
|
||||
)[0],
|
||||
} satisfies CreateOneResolverArgs;
|
||||
case ResolverArgsType.CREATE_MANY:
|
||||
return {
|
||||
...args,
|
||||
data: await this.overrideDataByFieldMetadata({
|
||||
partialRecordInputs: (args as CreateManyResolverArgs).data,
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
}),
|
||||
} satisfies CreateManyResolverArgs;
|
||||
case ResolverArgsType.UPDATE_ONE:
|
||||
return {
|
||||
...args,
|
||||
id: (args as UpdateOneResolverArgs).id,
|
||||
data: (
|
||||
await this.overrideDataByFieldMetadata({
|
||||
partialRecordInputs: [(args as UpdateOneResolverArgs).data],
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
shouldBackfillPositionIfUndefined: false,
|
||||
})
|
||||
)[0],
|
||||
} satisfies UpdateOneResolverArgs;
|
||||
case ResolverArgsType.UPDATE_MANY:
|
||||
return {
|
||||
...args,
|
||||
filter: this.overrideFilterByFieldMetadata(
|
||||
(args as UpdateManyResolverArgs).filter,
|
||||
options.objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
data: (
|
||||
await this.overrideDataByFieldMetadata({
|
||||
partialRecordInputs: [(args as UpdateManyResolverArgs).data],
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
shouldBackfillPositionIfUndefined: false,
|
||||
})
|
||||
)[0],
|
||||
} satisfies UpdateManyResolverArgs;
|
||||
case ResolverArgsType.FIND_ONE:
|
||||
return {
|
||||
...args,
|
||||
filter: this.overrideFilterByFieldMetadata(
|
||||
(args as FindOneResolverArgs).filter,
|
||||
options.objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
};
|
||||
case ResolverArgsType.FIND_MANY:
|
||||
return {
|
||||
...args,
|
||||
filter: this.overrideFilterByFieldMetadata(
|
||||
(args as FindManyResolverArgs).filter,
|
||||
options.objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
};
|
||||
case ResolverArgsType.FIND_DUPLICATES:
|
||||
return {
|
||||
...args,
|
||||
ids: (await Promise.all(
|
||||
(args as FindDuplicatesResolverArgs).ids?.map((id) =>
|
||||
this.overrideValueByFieldMetadata(
|
||||
'id',
|
||||
id,
|
||||
fieldMetadataMapByNameByName,
|
||||
options.objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
) ?? [],
|
||||
)) as string[],
|
||||
data: await this.overrideDataByFieldMetadata({
|
||||
partialRecordInputs: (args as FindDuplicatesResolverArgs).data,
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
shouldBackfillPositionIfUndefined: false,
|
||||
}),
|
||||
} satisfies FindDuplicatesResolverArgs;
|
||||
case ResolverArgsType.MERGE_MANY:
|
||||
return {
|
||||
...args,
|
||||
ids: (await Promise.all(
|
||||
(args as MergeManyResolverArgs).ids?.map((id) =>
|
||||
this.overrideValueByFieldMetadata(
|
||||
'id',
|
||||
id,
|
||||
fieldMetadataMapByNameByName,
|
||||
options.objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
) ?? [],
|
||||
)) as string[],
|
||||
conflictPriorityIndex: (args as MergeManyResolverArgs)
|
||||
.conflictPriorityIndex,
|
||||
dryRun: (args as MergeManyResolverArgs).dryRun,
|
||||
} satisfies MergeManyResolverArgs;
|
||||
case ResolverArgsType.GROUP_BY:
|
||||
return {
|
||||
...args,
|
||||
filter: this.overrideFilterByFieldMetadata(
|
||||
(args as GroupByResolverArgs).filter,
|
||||
options.objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
};
|
||||
default:
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
async overrideDataByFieldMetadata({
|
||||
partialRecordInputs,
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
shouldBackfillPositionIfUndefined = true,
|
||||
}: {
|
||||
partialRecordInputs: Partial<ObjectRecord>[] | undefined;
|
||||
authContext: AuthContext;
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
shouldBackfillPositionIfUndefined?: boolean;
|
||||
}): Promise<Partial<ObjectRecord>[]> {
|
||||
if (!isDefined(partialRecordInputs)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allOverriddenRecords: Partial<ObjectRecord>[] = [];
|
||||
|
||||
const workspace = authContext.workspace;
|
||||
|
||||
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
|
||||
|
||||
const overriddenPositionRecords =
|
||||
await this.recordPositionService.overridePositionOnRecords({
|
||||
partialRecordInputs,
|
||||
workspaceId: workspace.id,
|
||||
objectMetadata: {
|
||||
isCustom: objectMetadataItemWithFieldMaps.isCustom,
|
||||
nameSingular: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
fieldIdByName: objectMetadataItemWithFieldMaps.fieldIdByName,
|
||||
},
|
||||
shouldBackfillPositionIfUndefined,
|
||||
});
|
||||
|
||||
for (const record of overriddenPositionRecords) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const createArgByArgKey: [string, any][] = await Promise.all(
|
||||
Object.entries(record).map(async ([key, value]) => {
|
||||
const fieldMetadataId =
|
||||
objectMetadataItemWithFieldMaps.fieldIdByName[key];
|
||||
const fieldMetadata =
|
||||
objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
|
||||
|
||||
if (!fieldMetadata) {
|
||||
return [key, value];
|
||||
}
|
||||
|
||||
switch (fieldMetadata.type) {
|
||||
case FieldMetadataType.NUMBER:
|
||||
case FieldMetadataType.RICH_TEXT:
|
||||
case FieldMetadataType.PHONES:
|
||||
case FieldMetadataType.RICH_TEXT_V2:
|
||||
case FieldMetadataType.LINKS:
|
||||
case FieldMetadataType.EMAILS: {
|
||||
const transformedRecord =
|
||||
await this.recordInputTransformerService.process({
|
||||
recordInput: { [key]: value },
|
||||
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
|
||||
});
|
||||
|
||||
return [key, transformedRecord[key]];
|
||||
}
|
||||
default:
|
||||
return [key, value];
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
allOverriddenRecords.push(Object.fromEntries(createArgByArgKey));
|
||||
}
|
||||
|
||||
return allOverriddenRecords;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
-1
@@ -22,7 +22,6 @@ import { twentyORMGraphqlApiExceptionHandler } from 'src/engine/twenty-orm/utils
|
||||
interface QueryFailedErrorWithCode extends QueryFailedError {
|
||||
code: string;
|
||||
}
|
||||
//TODO : Refacto-common - Should be handle first in common api layer
|
||||
|
||||
export const workspaceQueryRunnerGraphqlApiExceptionHandler = (
|
||||
error: QueryFailedErrorWithCode,
|
||||
|
||||
+1
-7
@@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { WorkspaceQueryBuilderModule } from 'src/engine/api/graphql/workspace-query-builder/workspace-query-builder.module';
|
||||
import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories';
|
||||
import { TelemetryListener } from 'src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener';
|
||||
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
|
||||
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||
@@ -29,11 +28,6 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
|
||||
RecordPositionModule,
|
||||
SubscriptionsModule,
|
||||
],
|
||||
providers: [
|
||||
...workspaceQueryRunnerFactories,
|
||||
EntityEventsToDbListener,
|
||||
TelemetryListener,
|
||||
],
|
||||
exports: [...workspaceQueryRunnerFactories],
|
||||
providers: [EntityEventsToDbListener, TelemetryListener],
|
||||
})
|
||||
export class WorkspaceQueryRunnerModule {}
|
||||
|
||||
@@ -20,7 +20,6 @@ import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.
|
||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
import {
|
||||
PermissionsException,
|
||||
@@ -55,8 +54,6 @@ export interface FormatResult {
|
||||
}
|
||||
|
||||
export abstract class RestApiBaseHandler {
|
||||
@Inject()
|
||||
protected readonly recordInputTransformerService: RecordInputTransformerService;
|
||||
@Inject()
|
||||
protected readonly twentyORMManager: TwentyORMManager;
|
||||
@Inject()
|
||||
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
//TODO : Refacto-common - remove this comment - This parser is a copy of the filter input factory without objectMetadata dependency. Validation will be done in common layer
|
||||
import { type FieldValue } from 'src/engine/api/rest/core/types/field-value.type';
|
||||
import { addDefaultConjunctionIfMissing } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util';
|
||||
import { checkFilterQuery } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-query.util';
|
||||
|
||||
+2
-4
@@ -1,8 +1,6 @@
|
||||
//TODO : Refacto-common - remove this comment - This parser is a copy of the OrderByInputFactory without objectMetadata dependency. Validation will be done in common layer
|
||||
|
||||
import { type ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { parseOrderByRestRequestCommon } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/utils/parse-order-by-rest-request-common.util';
|
||||
import { parseOrderBy } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/utils/parse-order-by-rest-request-common.util';
|
||||
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
|
||||
export const parseOrderByRestRequest = (
|
||||
@@ -10,5 +8,5 @@ export const parseOrderByRestRequest = (
|
||||
): ObjectRecordOrderBy => {
|
||||
const orderByQuery = request.query.order_by;
|
||||
|
||||
return parseOrderByRestRequestCommon(orderByQuery);
|
||||
return parseOrderBy(orderByQuery);
|
||||
};
|
||||
|
||||
+1
-3
@@ -1,5 +1,3 @@
|
||||
//TODO : Refacto-common - remove this comment - This parser is a copy of the OrderByInputFactory without objectMetadata dependency. Validation will be done in common layer
|
||||
|
||||
import { OrderByDirection } from 'twenty-shared/types';
|
||||
|
||||
import { type ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
@@ -14,7 +12,7 @@ import {
|
||||
|
||||
const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst;
|
||||
|
||||
export const parseOrderByRestRequestCommon = (
|
||||
export const parseOrderBy = (
|
||||
orderByQuery: string | string[] | ParsedQs | ParsedQs[] | undefined,
|
||||
): ObjectRecordOrderBy => {
|
||||
if (typeof orderByQuery !== 'string') {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { type ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { parseOrderByRestRequestCommon } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/utils/parse-order-by-rest-request-common.util';
|
||||
import { parseOrderBy } from 'src/engine/api/rest/input-request-parsers/order-by-parser-utils/utils/parse-order-by-rest-request-common.util';
|
||||
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
|
||||
|
||||
export const parseOrderByForRecordsWithGroupByRestRequest = (
|
||||
@@ -8,5 +8,5 @@ export const parseOrderByForRecordsWithGroupByRestRequest = (
|
||||
): ObjectRecordOrderBy | undefined => {
|
||||
const orderByForRecordsWithGroupByQuery = request.query.order_by_for_records;
|
||||
|
||||
return parseOrderByRestRequestCommon(orderByForRecordsWithGroupByQuery);
|
||||
return parseOrderBy(orderByForRecordsWithGroupByQuery);
|
||||
};
|
||||
|
||||
+4
-8
@@ -32,9 +32,9 @@ describe('computeSchemaComponents', () => {
|
||||
"lastName": "Osinski",
|
||||
},
|
||||
"fieldLinks": {
|
||||
"additionalLinks": [],
|
||||
"primaryLinkLabel": "",
|
||||
"primaryLinkUrl": "https://narrow-help.net/",
|
||||
"secondaryLinks": [],
|
||||
},
|
||||
"fieldMultiSelect": [
|
||||
"OPTION_1",
|
||||
@@ -46,9 +46,7 @@ describe('computeSchemaComponents', () => {
|
||||
"primaryPhoneCountryCode": "FR",
|
||||
"primaryPhoneNumber": "06 10 20 30 40",
|
||||
},
|
||||
"fieldSelect": [
|
||||
"OPTION_1",
|
||||
],
|
||||
"fieldSelect": "OPTION_1",
|
||||
},
|
||||
"properties": {
|
||||
"fieldActor": {
|
||||
@@ -535,9 +533,9 @@ describe('computeSchemaComponents', () => {
|
||||
"lastName": "Jones",
|
||||
},
|
||||
"fieldLinks": {
|
||||
"additionalLinks": [],
|
||||
"primaryLinkLabel": "",
|
||||
"primaryLinkUrl": "https://unlawful-blowgun.biz",
|
||||
"secondaryLinks": [],
|
||||
},
|
||||
"fieldMultiSelect": [
|
||||
"OPTION_1",
|
||||
@@ -549,9 +547,7 @@ describe('computeSchemaComponents', () => {
|
||||
"primaryPhoneCountryCode": "FR",
|
||||
"primaryPhoneNumber": "06 10 20 30 40",
|
||||
},
|
||||
"fieldSelect": [
|
||||
"OPTION_1",
|
||||
],
|
||||
"fieldSelect": "OPTION_1",
|
||||
},
|
||||
"properties": {
|
||||
"fieldActor": {
|
||||
|
||||
+3
-3
@@ -58,7 +58,7 @@ export const generateRandomFieldValue = ({
|
||||
return {
|
||||
primaryLinkLabel: '',
|
||||
primaryLinkUrl: faker.internet.url(),
|
||||
additionalLinks: [],
|
||||
secondaryLinks: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,10 +82,10 @@ export const generateRandomFieldValue = ({
|
||||
|
||||
case FieldMetadataType.SELECT: {
|
||||
if (!isDefined(field.options) || !isDefined(field.options[0].value)) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
|
||||
return [field.options[0].value];
|
||||
return field.options[0].value;
|
||||
}
|
||||
|
||||
case FieldMetadataType.MULTI_SELECT: {
|
||||
|
||||
+8
-80
@@ -2,15 +2,16 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
FieldMetadataType,
|
||||
ObjectRecord,
|
||||
compositeTypeDefinitions,
|
||||
type RichTextV2Metadata,
|
||||
richTextV2ValueSchema,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
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 { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { transformRichTextV2Value } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
@Injectable()
|
||||
export class RecordInputTransformerService {
|
||||
@@ -18,15 +19,9 @@ export class RecordInputTransformerService {
|
||||
recordInput,
|
||||
objectMetadataMapItem,
|
||||
}: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
recordInput: Record<string, any>;
|
||||
recordInput: Partial<ObjectRecord>;
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
}): Promise<Record<string, any>> {
|
||||
if (!recordInput) {
|
||||
return recordInput;
|
||||
}
|
||||
|
||||
}): Promise<Partial<ObjectRecord>> {
|
||||
let transformedEntries = {};
|
||||
|
||||
for (const [key, value] of Object.entries(recordInput)) {
|
||||
@@ -72,11 +67,11 @@ export class RecordInputTransformerService {
|
||||
'Rich text is not supported, please use RICH_TEXT_V2 instead',
|
||||
);
|
||||
case FieldMetadataType.RICH_TEXT_V2:
|
||||
return this.transformRichTextV2Value(value);
|
||||
return await transformRichTextV2Value(value);
|
||||
case FieldMetadataType.LINKS:
|
||||
return transformLinksValue(value);
|
||||
case FieldMetadataType.EMAILS:
|
||||
return this.transformEmailsValue(value);
|
||||
return transformEmailsValue(value);
|
||||
case FieldMetadataType.PHONES:
|
||||
return transformPhonesValue({ input: value });
|
||||
default:
|
||||
@@ -84,73 +79,6 @@ export class RecordInputTransformerService {
|
||||
}
|
||||
}
|
||||
|
||||
private async transformRichTextV2Value(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
richTextValue: any,
|
||||
): Promise<RichTextV2Metadata> {
|
||||
const parsedValue = richTextV2ValueSchema.parse(richTextValue);
|
||||
|
||||
const { ServerBlockNoteEditor } = await import('@blocknote/server-util');
|
||||
|
||||
const serverBlockNoteEditor = ServerBlockNoteEditor.create();
|
||||
|
||||
// Patch: Handle cases where blocknote to markdown conversion fails for certain block types (custom/code blocks)
|
||||
// Todo : This may be resolved once the server-utils library is updated with proper conversion support - #947
|
||||
let convertedMarkdown: string | null = null;
|
||||
|
||||
try {
|
||||
convertedMarkdown = isDefined(parsedValue.blocknote)
|
||||
? await serverBlockNoteEditor.blocksToMarkdownLossy(
|
||||
JSON.parse(parsedValue.blocknote),
|
||||
)
|
||||
: null;
|
||||
} catch {
|
||||
convertedMarkdown = parsedValue.blocknote || null;
|
||||
}
|
||||
|
||||
const convertedBlocknote = parsedValue.markdown
|
||||
? JSON.stringify(
|
||||
await serverBlockNoteEditor.tryParseMarkdownToBlocks(
|
||||
parsedValue.markdown,
|
||||
),
|
||||
)
|
||||
: null;
|
||||
|
||||
return {
|
||||
markdown: parsedValue.markdown || convertedMarkdown,
|
||||
blocknote: parsedValue.blocknote || convertedBlocknote,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private transformEmailsValue(value: any): any {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
let additionalEmails = value?.additionalEmails;
|
||||
const primaryEmail = value?.primaryEmail
|
||||
? value.primaryEmail.toLowerCase()
|
||||
: '';
|
||||
|
||||
if (additionalEmails) {
|
||||
try {
|
||||
const emailArray = JSON.parse(additionalEmails) as string[];
|
||||
|
||||
additionalEmails = JSON.stringify(
|
||||
emailArray.map((email) => email.toLowerCase()),
|
||||
);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
primaryEmail,
|
||||
additionalEmails,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private stringifySubFields(fieldMetadataType: FieldMetadataType, value: any) {
|
||||
const compositeType = compositeTypeDefinitions.get(fieldMetadataType);
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const transformEmailsValue = (value: any): any => {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
let additionalEmails = value?.additionalEmails;
|
||||
const primaryEmail = value?.primaryEmail
|
||||
? value.primaryEmail.toLowerCase()
|
||||
: '';
|
||||
|
||||
if (additionalEmails) {
|
||||
try {
|
||||
const emailArray = (
|
||||
isNonEmptyString(additionalEmails)
|
||||
? JSON.parse(additionalEmails)
|
||||
: additionalEmails
|
||||
) as string[];
|
||||
|
||||
additionalEmails = JSON.stringify(
|
||||
emailArray.map((email) => email.toLowerCase()),
|
||||
);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
primaryEmail,
|
||||
additionalEmails,
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -32,7 +32,7 @@ export const transformLinksValue = (
|
||||
|
||||
const secondaryLinksArray = isNonEmptyString(secondaryLinksRaw)
|
||||
? parseJson<LinkMetadataNullable[]>(secondaryLinksRaw)
|
||||
: null;
|
||||
: secondaryLinksRaw;
|
||||
|
||||
const { primaryLinkLabel, primaryLinkUrl, secondaryLinks } = removeEmptyLinks(
|
||||
{
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import {
|
||||
type RichTextV2Metadata,
|
||||
richTextV2ValueSchema,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const transformRichTextV2Value = async (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
richTextValue: any,
|
||||
): Promise<RichTextV2Metadata> => {
|
||||
const parsedValue = isNonEmptyString(richTextValue)
|
||||
? richTextV2ValueSchema.parse(richTextValue)
|
||||
: richTextValue;
|
||||
|
||||
const { ServerBlockNoteEditor } = await import('@blocknote/server-util');
|
||||
|
||||
const serverBlockNoteEditor = ServerBlockNoteEditor.create();
|
||||
|
||||
// Patch: Handle cases where blocknote to markdown conversion fails for certain block types (custom/code blocks)
|
||||
// Todo : This may be resolved once the server-utils library is updated with proper conversion support - #947
|
||||
let convertedMarkdown: string | null = null;
|
||||
|
||||
try {
|
||||
convertedMarkdown = isDefined(parsedValue.blocknote)
|
||||
? await serverBlockNoteEditor.blocksToMarkdownLossy(
|
||||
JSON.parse(parsedValue.blocknote),
|
||||
)
|
||||
: null;
|
||||
} catch {
|
||||
convertedMarkdown = parsedValue.blocknote || null;
|
||||
}
|
||||
|
||||
const convertedBlocknote = parsedValue.markdown
|
||||
? JSON.stringify(
|
||||
await serverBlockNoteEditor.tryParseMarkdownToBlocks(
|
||||
parsedValue.markdown,
|
||||
),
|
||||
)
|
||||
: null;
|
||||
|
||||
return {
|
||||
markdown: parsedValue.markdown || convertedMarkdown,
|
||||
blocknote: parsedValue.blocknote || convertedBlocknote,
|
||||
};
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Create input validation - ADDRESS Gql create input - failure ADDRESS - should fail with : {"addressField":"not-an-address"} 1`] = `"Expected type "AddressCreateInput" to be an object."`;
|
||||
|
||||
exports[`Create input validation - ADDRESS Rest create input - failure ADDRESS - should fail with : {"addressField":"not-an-address"} 1`] = `"["Invalid object value 'not-an-address' for field \\"addressField\\""]"`;
|
||||
+2
-2
@@ -4,6 +4,6 @@ exports[`Create input validation - ARRAY Gql create input - failure ARRAY - shou
|
||||
|
||||
exports[`Create input validation - ARRAY Gql create input - failure ARRAY - should fail with : {"arrayField":true} 1`] = `"String cannot represent a non string value: true"`;
|
||||
|
||||
exports[`Create input validation - ARRAY Rest create input - failure ARRAY - should fail with : {"arrayField":1} 1`] = `"["malformed array literal: \\"1\\""]"`;
|
||||
exports[`Create input validation - ARRAY Rest create input - failure ARRAY - should fail with : {"arrayField":1} 1`] = `"["Invalid value 1 for field \\"arrayField - Array values need to be string\\""]"`;
|
||||
|
||||
exports[`Create input validation - ARRAY Rest create input - failure ARRAY - should fail with : {"arrayField":true} 1`] = `"["malformed array literal: \\"true\\""]"`;
|
||||
exports[`Create input validation - ARRAY Rest create input - failure ARRAY - should fail with : {"arrayField":true} 1`] = `"["Invalid value true for field \\"arrayField - Array values need to be string\\""]"`;
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Create input validation - CURRENCY Gql create input - failure CURRENCY - should fail with : {"currencyField":"not-a-currency"} 1`] = `"Expected type "CurrencyCreateInput" to be an object."`;
|
||||
|
||||
exports[`Create input validation - CURRENCY Rest create input - failure CURRENCY - should fail with : {"currencyField":"not-a-currency"} 1`] = `"["Invalid object value 'not-a-currency' for field \\"currencyField\\""]"`;
|
||||
+8
-8
@@ -1,21 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":"malformed-date"} 1`] = `"invalid input syntax for type date: "malformed-date""`;
|
||||
exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":"malformed-date"} 1`] = `"Invalid value 'malformed-date' for date or date-time field "dateField""`;
|
||||
|
||||
exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":[]} 1`] = `"invalid input syntax for type date: "{}""`;
|
||||
exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":[]} 1`] = `"Invalid value [] for date or date-time field "dateField""`;
|
||||
|
||||
exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":{}} 1`] = `"invalid input syntax for type date: "{}""`;
|
||||
exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":{}} 1`] = `"Invalid value {} for date or date-time field "dateField""`;
|
||||
|
||||
exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":1} 1`] = `"invalid input syntax for type date: "1""`;
|
||||
|
||||
exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":true} 1`] = `"invalid input syntax for type date: "true""`;
|
||||
exports[`Create input validation - DATE Gql create input - failure DATE - should fail with : {"dateField":true} 1`] = `"Invalid value true for date or date-time field "dateField""`;
|
||||
|
||||
exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":"malformed-date"} 1`] = `"["invalid input syntax for type date: \\"malformed-date\\""]"`;
|
||||
exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":"malformed-date"} 1`] = `"["Invalid value 'malformed-date' for date or date-time field \\"dateField\\""]"`;
|
||||
|
||||
exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":[]} 1`] = `"["invalid input syntax for type date: \\"{}\\""]"`;
|
||||
exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":[]} 1`] = `"["Invalid value [] for date or date-time field \\"dateField\\""]"`;
|
||||
|
||||
exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":{}} 1`] = `"["invalid input syntax for type date: \\"{}\\""]"`;
|
||||
exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":{}} 1`] = `"["Invalid value {} for date or date-time field \\"dateField\\""]"`;
|
||||
|
||||
exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":1} 1`] = `"["invalid input syntax for type date: \\"1\\""]"`;
|
||||
|
||||
exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":true} 1`] = `"["invalid input syntax for type date: \\"true\\""]"`;
|
||||
exports[`Create input validation - DATE Rest create input - failure DATE - should fail with : {"dateField":true} 1`] = `"["Invalid value true for date or date-time field \\"dateField\\""]"`;
|
||||
|
||||
+8
-8
@@ -1,21 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":"malformed-date"} 1`] = `"invalid input syntax for type timestamp with time zone: "malformed-date""`;
|
||||
exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":"malformed-date"} 1`] = `"Invalid value 'malformed-date' for date or date-time field "dateTimeField""`;
|
||||
|
||||
exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":[]} 1`] = `"invalid input syntax for type timestamp with time zone: "{}""`;
|
||||
exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":[]} 1`] = `"Invalid value [] for date or date-time field "dateTimeField""`;
|
||||
|
||||
exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":{}} 1`] = `"invalid input syntax for type timestamp with time zone: "{}""`;
|
||||
exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":{}} 1`] = `"Invalid value {} for date or date-time field "dateTimeField""`;
|
||||
|
||||
exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":1} 1`] = `"invalid input syntax for type timestamp with time zone: "1""`;
|
||||
|
||||
exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":true} 1`] = `"invalid input syntax for type timestamp with time zone: "true""`;
|
||||
exports[`Create input validation - DATE_TIME Gql create input - failure DATE_TIME - should fail with : {"dateTimeField":true} 1`] = `"Invalid value true for date or date-time field "dateTimeField""`;
|
||||
|
||||
exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":"malformed-date"} 1`] = `"["invalid input syntax for type timestamp with time zone: \\"malformed-date\\""]"`;
|
||||
exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":"malformed-date"} 1`] = `"["Invalid value 'malformed-date' for date or date-time field \\"dateTimeField\\""]"`;
|
||||
|
||||
exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":[]} 1`] = `"["invalid input syntax for type timestamp with time zone: \\"{}\\""]"`;
|
||||
exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":[]} 1`] = `"["Invalid value [] for date or date-time field \\"dateTimeField\\""]"`;
|
||||
|
||||
exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":{}} 1`] = `"["invalid input syntax for type timestamp with time zone: \\"{}\\""]"`;
|
||||
exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":{}} 1`] = `"["Invalid value {} for date or date-time field \\"dateTimeField\\""]"`;
|
||||
|
||||
exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":1} 1`] = `"["invalid input syntax for type timestamp with time zone: \\"1\\""]"`;
|
||||
|
||||
exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":true} 1`] = `"["invalid input syntax for type timestamp with time zone: \\"true\\""]"`;
|
||||
exports[`Create input validation - DATE_TIME Rest create input - failure DATE_TIME - should fail with : {"dateTimeField":true} 1`] = `"["Invalid value true for date or date-time field \\"dateTimeField\\""]"`;
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Create input validation - EMAILS Gql create input - failure EMAILS - should fail with : {"emailsField":"not-an-email"} 1`] = `"Expected type "EmailsCreateInput" to be an object."`;
|
||||
|
||||
exports[`Create input validation - EMAILS Rest create input - failure EMAILS - should fail with : {"emailsField":"not-an-email"} 1`] = `"["Invalid object value 'not-an-email' for field \\"emailsField\\""]"`;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Create input validation - FULL_NAME Gql create input - failure FULL_NAME - should fail with : {"fullNameField":"not-a-full-name"} 1`] = `"Expected type "FullNameCreateInput" to be an object."`;
|
||||
|
||||
exports[`Create input validation - FULL_NAME Rest create input - failure FULL_NAME - should fail with : {"fullNameField":"not-a-full-name"} 1`] = `"["Invalid object value 'not-a-full-name' for field \\"fullNameField\\""]"`;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Create input validation - LINKS Gql create input - failure LINKS - should fail with : {"linksField":"not-a-link"} 1`] = `"Expected type "LinksCreateInput" to be an object."`;
|
||||
|
||||
exports[`Create input validation - LINKS Rest create input - failure LINKS - should fail with : {"linksField":"not-a-link"} 1`] = `"["Invalid object value 'not-a-link' for field \\"linksField\\""]"`;
|
||||
+7
-3
@@ -2,12 +2,16 @@
|
||||
|
||||
exports[`Create input validation - MULTI_SELECT Gql create input - failure MULTI_SELECT - should fail with : {"multiSelectField":"not-a-select-option"} 1`] = `"Value "not-a-select-option" does not exist in "ApiInputValidationTestObjectMultiSelectFieldEnum" enum."`;
|
||||
|
||||
exports[`Create input validation - MULTI_SELECT Gql create input - failure MULTI_SELECT - should fail with : {"multiSelectField":{}} 1`] = `"Enum "ApiInputValidationTestObjectMultiSelectFieldEnum" cannot represent non-string value: {}."`;
|
||||
|
||||
exports[`Create input validation - MULTI_SELECT Gql create input - failure MULTI_SELECT - should fail with : {"multiSelectField":1} 1`] = `"Enum "ApiInputValidationTestObjectMultiSelectFieldEnum" cannot represent non-string value: 1."`;
|
||||
|
||||
exports[`Create input validation - MULTI_SELECT Gql create input - failure MULTI_SELECT - should fail with : {"multiSelectField":true} 1`] = `"Enum "ApiInputValidationTestObjectMultiSelectFieldEnum" cannot represent non-string value: true."`;
|
||||
|
||||
exports[`Create input validation - MULTI_SELECT Rest create input - failure MULTI_SELECT - should fail with : {"multiSelectField":"not-a-select-option"} 1`] = `"["malformed array literal: \\"not-a-select-option\\""]"`;
|
||||
exports[`Create input validation - MULTI_SELECT Rest create input - failure MULTI_SELECT - should fail with : {"multiSelectField":"not-a-select-option"} 1`] = `"["Invalid value 'not-a-select-option' for multi select field \\"multiSelectField\\""]"`;
|
||||
|
||||
exports[`Create input validation - MULTI_SELECT Rest create input - failure MULTI_SELECT - should fail with : {"multiSelectField":1} 1`] = `"["malformed array literal: \\"1\\""]"`;
|
||||
exports[`Create input validation - MULTI_SELECT Rest create input - failure MULTI_SELECT - should fail with : {"multiSelectField":{}} 1`] = `"["Invalid value {} for field \\"multiSelectField - Array values need to be string\\""]"`;
|
||||
|
||||
exports[`Create input validation - MULTI_SELECT Rest create input - failure MULTI_SELECT - should fail with : {"multiSelectField":true} 1`] = `"["malformed array literal: \\"true\\""]"`;
|
||||
exports[`Create input validation - MULTI_SELECT Rest create input - failure MULTI_SELECT - should fail with : {"multiSelectField":1} 1`] = `"["Invalid value 1 for field \\"multiSelectField - Array values need to be string\\""]"`;
|
||||
|
||||
exports[`Create input validation - MULTI_SELECT Rest create input - failure MULTI_SELECT - should fail with : {"multiSelectField":true} 1`] = `"["Invalid value true for field \\"multiSelectField - Array values need to be string\\""]"`;
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Create input validation - PHONES Gql create input - failure PHONES - should fail with : {"phonesField":"not-a-phone"} 1`] = `"Expected type "PhonesCreateInput" to be an object."`;
|
||||
|
||||
exports[`Create input validation - PHONES Rest create input - failure PHONES - should fail with : {"phonesField":"not-a-phone"} 1`] = `"["Invalid object value 'not-a-phone' for field \\"phonesField\\""]"`;
|
||||
+5
-5
@@ -10,12 +10,12 @@ exports[`Create input validation - RATING Gql create input - failure RATING - sh
|
||||
|
||||
exports[`Create input validation - RATING Gql create input - failure RATING - should fail with : {"ratingField":true} 1`] = `"Enum "ApiInputValidationTestObjectRatingFieldEnum" cannot represent non-string value: true."`;
|
||||
|
||||
exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":"not-a-rating"} 1`] = `"["invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5.\\"_apiInputValidationTestObject_ratingField_enum\\": \\"not-a-rating\\""]"`;
|
||||
exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":"not-a-rating"} 1`] = `"["Invalid value 'not-a-rating' for field \\"ratingField\\""]"`;
|
||||
|
||||
exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":[]} 1`] = `"["invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5.\\"_apiInputValidationTestObject_ratingField_enum\\": \\"\\""]"`;
|
||||
exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":[]} 1`] = `"["Invalid string value [] for text field \\"ratingField\\""]"`;
|
||||
|
||||
exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":{}} 1`] = `"["invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5.\\"_apiInputValidationTestObject_ratingField_enum\\": \\"[object Object]\\""]"`;
|
||||
exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":{}} 1`] = `"["Invalid string value {} for text field \\"ratingField\\""]"`;
|
||||
|
||||
exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":1} 1`] = `"["invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5.\\"_apiInputValidationTestObject_ratingField_enum\\": \\"1\\""]"`;
|
||||
exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":1} 1`] = `"["Invalid string value 1 for text field \\"ratingField\\""]"`;
|
||||
|
||||
exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":true} 1`] = `"["invalid input value for enum workspace_1wgvd1injqtife6y4rvfbu3h5.\\"_apiInputValidationTestObject_ratingField_enum\\": \\"true\\""]"`;
|
||||
exports[`Create input validation - RATING Rest create input - failure RATING - should fail with : {"ratingField":true} 1`] = `"["Invalid string value true for text field \\"ratingField\\""]"`;
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Create input validation - RAW_JSON Gql create input - failure RAW_JSON - should fail with : {"rawJsonField":"not-a-json"} 1`] = `"Unexpected token 'o', "not-a-json" is not valid JSON"`;
|
||||
exports[`Create input validation - RAW_JSON Gql create input - failure RAW_JSON - should fail with : {"rawJsonField":"not-a-json"} 1`] = `"Invalid object value 'not-a-json' for field "rawJsonField""`;
|
||||
|
||||
exports[`Create input validation - RAW_JSON Rest create input - failure RAW_JSON - should fail with : {"rawJsonField":"not-a-json"} 1`] = `"["Unexpected token 'o', \\"not-a-json\\" is not valid JSON"]"`;
|
||||
exports[`Create input validation - RAW_JSON Rest create input - failure RAW_JSON - should fail with : {"rawJsonField":"not-a-json"} 1`] = `"["Invalid object value 'not-a-json' for field \\"rawJsonField\\""]"`;
|
||||
|
||||
+18
-6
@@ -1,17 +1,29 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":"non-uuid"} 1`] = `"invalid input syntax for type uuid: "non-uuid""`;
|
||||
exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":"non-uuid"} 1`] = `"Invalid UUID value 'non-uuid' for field "manyToOneRelationFieldId""`;
|
||||
|
||||
exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":1} 1`] = `"invalid input syntax for type uuid: "1""`;
|
||||
exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":[]} 1`] = `"ID cannot represent value: []"`;
|
||||
|
||||
exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":{}} 1`] = `"ID cannot represent value: {}"`;
|
||||
|
||||
exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":1} 1`] = `"Invalid UUID value '1' for field "manyToOneRelationFieldId""`;
|
||||
|
||||
exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":true} 1`] = `"ID cannot represent value: true"`;
|
||||
|
||||
exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"oneToManyRelationFieldId":"not-existing-field"} 1`] = `"Field "oneToManyRelationFieldId" is not defined by type "ApiInputValidationTestObjectCreateInput". Did you mean "manyToOneRelationFieldId" or "manyToOneRelationField"?"`;
|
||||
|
||||
exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":"non-uuid"} 1`] = `"["invalid input syntax for type uuid: \\"non-uuid\\""]"`;
|
||||
exports[`Create input validation - RELATION Gql create input - failure RELATION - should fail with : {"oneToOneRelationField":"not-existing-field"} 1`] = `"Field "oneToOneRelationField" is not defined by type "ApiInputValidationTestObjectCreateInput". Did you mean "manyToOneRelationField" or "manyToOneRelationFieldId"?"`;
|
||||
|
||||
exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":1} 1`] = `"["invalid input syntax for type uuid: \\"1\\""]"`;
|
||||
exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":"non-uuid"} 1`] = `"["Invalid UUID value 'non-uuid' for field \\"manyToOneRelationFieldId\\""]"`;
|
||||
|
||||
exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":true} 1`] = `"["invalid input syntax for type uuid: \\"true\\""]"`;
|
||||
exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":[]} 1`] = `"["Invalid UUID value [] for field \\"manyToOneRelationFieldId\\""]"`;
|
||||
|
||||
exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"oneToManyRelationFieldId":"not-existing-field"} 1`] = `"["Field metadata for field \\"oneToManyRelationFieldId\\" is missing in object metadata apiInputValidationTestObject"]"`;
|
||||
exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":{}} 1`] = `"["Invalid UUID value {} for field \\"manyToOneRelationFieldId\\""]"`;
|
||||
|
||||
exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":1} 1`] = `"["Invalid UUID value 1 for field \\"manyToOneRelationFieldId\\""]"`;
|
||||
|
||||
exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"manyToOneRelationFieldId":true} 1`] = `"["Invalid UUID value true for field \\"manyToOneRelationFieldId\\""]"`;
|
||||
|
||||
exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"oneToManyRelationFieldId":"not-existing-field"} 1`] = `"["Object apiInputValidationTestObject doesn't have any \\"oneToManyRelationFieldId\\" field."]"`;
|
||||
|
||||
exports[`Create input validation - RELATION Rest create input - failure RELATION - should fail with : {"oneToOneRelationField":"not-existing-field"} 1`] = `"["Object apiInputValidationTestObject doesn't have any \\"oneToOneRelationField\\" field."]"`;
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Create input validation - RICH_TEXT Gql create input - failure RICH_TEXT - should fail with : {"richTextField":"test"} 1`] = `"Rich text is not supported, please use RICH_TEXT_V2 instead"`;
|
||||
exports[`Create input validation - RICH_TEXT Gql create input - failure RICH_TEXT - should fail with : {"richTextField":"test"} 1`] = `"richTextField RICH_TEXT-typed field does not support write operations"`;
|
||||
|
||||
exports[`Create input validation - RICH_TEXT Rest create input - failure RICH_TEXT - should fail with : {"richTextField":"test"} 1`] = `"["Rich text is not supported, please use RICH_TEXT_V2 instead"]"`;
|
||||
exports[`Create input validation - RICH_TEXT Rest create input - failure RICH_TEXT - should fail with : {"richTextField":"test"} 1`] = `"["richTextField RICH_TEXT-typed field does not support write operations"]"`;
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Create input validation - RICH_TEXT_V2 Gql create input - failure RICH_TEXT_V2 - should fail with : {"richTextV2Field":"not-a-rich-text"} 1`] = `"Expected type "RichTextV2CreateInput" to be an object."`;
|
||||
|
||||
exports[`Create input validation - RICH_TEXT_V2 Rest create input - failure RICH_TEXT_V2 - should fail with : {"richTextV2Field":"not-a-rich-text"} 1`] = `"["Invalid rich text v2 value 'not-a-rich-text' for field \\"richTextV2Field\\" - Should be an object"]"`;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user