Compare commits

...
Author SHA1 Message Date
Will Schurman 7f102ef29d feat: add EntityField transformation 2022-07-01 13:00:43 -04:00
15 changed files with 283 additions and 86 deletions

No files matched your search

@@ -1,4 +1,4 @@
import { DateField } from '@expo/entity';
import { DateField, LowercaseStringField } from '@expo/entity';
export const redisTransformerMap = new Map([
[
@@ -22,4 +22,10 @@ export const redisTransformerMap = new Map([
read: (val: any) => (val ? new Date(val) : val),
},
],
[
LowercaseStringField.name,
{
write: (val: any) => (typeof val === 'string' ? val.toLowerCase() : val),
},
],
]);
@@ -7,6 +7,7 @@ import {
TableQuerySelectionModifiers,
TableFieldSingleValueEqualityCondition,
TableFieldMultiValueEqualityCondition,
LowercaseStringField,
} from '@expo/entity';
import { Knex } from 'knex';
@@ -36,6 +37,12 @@ export default class PostgresEntityDatabaseAdapter<TFields> extends EntityDataba
write: (val: any[] | any) => (Array.isArray(val) ? JSON.stringify(val) : val),
},
],
[
LowercaseStringField.name,
{
write: (val: any) => (typeof val === 'string' ? val.toLowerCase() : val),
},
],
]);
}
@@ -1,8 +0,0 @@
import { EntityFieldDefinition } from '@expo/entity';
import { Address4, Address6 } from 'ip-address';
export class IPAddressField extends EntityFieldDefinition<string> {
protected validateInputValueInternal(value: string): boolean {
return Address4.isValid(value) || Address6.isValid(value);
}
}
@@ -0,0 +1,12 @@
import { EntityFieldDefinition } from '@expo/entity';
import { Address4, Address6 } from 'ip-address';
export class IPAddressField extends EntityFieldDefinition<string> {
protected validateAndTransformInputValueInternal(
value: string
): { isValid: false } | { isValid: true; transformedValue: string } {
return Address4.isValid(value) || Address6.isValid(value)
? { isValid: true, transformedValue: value }
: { isValid: false };
}
}
@@ -1,6 +1,6 @@
import { describeFieldTestCase } from '@expo/entity';
import { IPAddressField } from '../EntityFields';
import { IPAddressField } from '../IPAddressField';
describeFieldTestCase(
new IPAddressField({ columnName: 'wat' }),
@@ -12,5 +12,6 @@ describeFieldTestCase(
'1:2:3:4:5:6:7:8/64',
'fedc:ba98:7654:3210:fedc:ba98:7654:3210',
],
['', 'abs', '0.0.0.x/0', '123.21.23', '1:2:3:4:5:6', '198.10/8']
['', 'abs', '0.0.0.x/0', '123.21.23', '1:2:3:4:5:6', '198.10/8'],
[{ in: '192.168.1.1', out: '192.168.1.1' }]
);
@@ -4,4 +4,4 @@
* @module @expo/entity-ip-address-field
*/
export * from './EntityFields';
export * from './IPAddressField';
+12 -4
View File
@@ -121,12 +121,20 @@ export abstract class EntityFieldDefinition<T> {
* - EntityLoader.loadByFieldValue - to ensure the value being loaded by is a valid value
* - EntityMutator.setField - to ensure the value being set is a valid value
*/
public validateInputValue(value: T | null | undefined): boolean {
public validateAndTransformInputValue(
value: T | null | undefined
): { isValid: false } | { isValid: true; transformedValue: T | null | undefined } {
if (value === null || value === undefined) {
return true;
return { isValid: true, transformedValue: value };
}
return this.validateInputValueInternal(value);
return this.validateAndTransformInputValueInternal(value);
}
protected abstract validateAndTransformInputValueInternal(
value: T
): { isValid: false } | { isValid: true; transformedValue: T };
public transformInputValue(value: T): T {
return value;
}
protected abstract validateInputValueInternal(value: T): boolean;
}
+78 -22
View File
@@ -6,8 +6,28 @@ import { EntityFieldDefinition } from './EntityFieldDefinition';
* {@link EntityFieldDefinition} for a column with a JS string type.
*/
export class StringField extends EntityFieldDefinition<string> {
protected validateInputValueInternal(value: string): boolean {
return typeof value === 'string';
protected validateAndTransformInputValueInternal(
value: string
): { isValid: false } | { isValid: true; transformedValue: string } {
return typeof value === 'string'
? { isValid: true, transformedValue: value }
: { isValid: false };
}
}
/**
* {@link EntityFieldDefinition} for a column with a JS string type in which
* stored values are lower-cased.
*/
export class LowercaseStringField extends StringField {
protected override validateAndTransformInputValueInternal(
value: string
): { isValid: false } | { isValid: true; transformedValue: string } {
const superValue = super.validateAndTransformInputValueInternal(value);
if (superValue.isValid) {
return { isValid: true, transformedValue: superValue.transformedValue.toLowerCase() };
}
return { isValid: false };
}
}
@@ -16,8 +36,14 @@ export class StringField extends EntityFieldDefinition<string> {
* Enforces that the string is a valid UUID.
*/
export class UUIDField extends StringField {
protected override validateInputValueInternal(value: string): boolean {
return validateUUID(value);
public override validateAndTransformInputValueInternal(
value: string
): { isValid: false } | { isValid: true; transformedValue: string } {
const superValue = super.validateAndTransformInputValueInternal(value);
if (superValue.isValid && validateUUID(superValue.transformedValue)) {
return { isValid: true, transformedValue: superValue.transformedValue };
}
return { isValid: false };
}
}
@@ -25,8 +51,10 @@ export class UUIDField extends StringField {
* {@link EntityFieldDefinition} for a column with a JS Date type.
*/
export class DateField extends EntityFieldDefinition<Date> {
protected validateInputValueInternal(value: Date): boolean {
return value instanceof Date;
protected validateAndTransformInputValueInternal(
value: Date
): { isValid: false } | { isValid: true; transformedValue: Date } {
return value instanceof Date ? { isValid: true, transformedValue: value } : { isValid: false };
}
}
@@ -34,8 +62,12 @@ export class DateField extends EntityFieldDefinition<Date> {
* {@link EntityFieldDefinition} for a column with a JS boolean type.
*/
export class BooleanField extends EntityFieldDefinition<boolean> {
protected validateInputValueInternal(value: boolean): boolean {
return typeof value === 'boolean';
protected validateAndTransformInputValueInternal(
value: boolean
): { isValid: false } | { isValid: true; transformedValue: boolean } {
return typeof value === 'boolean'
? { isValid: true, transformedValue: value }
: { isValid: false };
}
}
@@ -44,8 +76,12 @@ export class BooleanField extends EntityFieldDefinition<boolean> {
* Enforces that the number is an integer.
*/
export class IntField extends EntityFieldDefinition<number> {
protected validateInputValueInternal(value: number): boolean {
return typeof value === 'number' && Number.isInteger(value);
protected validateAndTransformInputValueInternal(
value: number
): { isValid: false } | { isValid: true; transformedValue: number } {
return typeof value === 'number' && Number.isInteger(value)
? { isValid: true, transformedValue: value }
: { isValid: false };
}
}
@@ -54,8 +90,12 @@ export class IntField extends EntityFieldDefinition<number> {
* Enforces that the number is a float (which includes integers in JS).
*/
export class FloatField extends EntityFieldDefinition<number> {
protected validateInputValueInternal(value: number): boolean {
return typeof value === 'number';
protected validateAndTransformInputValueInternal(
value: number
): { isValid: false } | { isValid: true; transformedValue: number } {
return typeof value === 'number'
? { isValid: true, transformedValue: value }
: { isValid: false };
}
}
@@ -64,8 +104,12 @@ export class FloatField extends EntityFieldDefinition<number> {
* Enforces that every member of the string array is a string.
*/
export class StringArrayField extends EntityFieldDefinition<string[]> {
protected validateInputValueInternal(value: string[]): boolean {
return Array.isArray(value) && value.every((subValue) => typeof subValue === 'string');
protected validateAndTransformInputValueInternal(
value: string[]
): { isValid: false } | { isValid: true; transformedValue: string[] } {
return Array.isArray(value) && value.every((subValue) => typeof subValue === 'string')
? { isValid: true, transformedValue: value }
: { isValid: false };
}
}
@@ -73,8 +117,12 @@ export class StringArrayField extends EntityFieldDefinition<string[]> {
* {@link EntityFieldDefinition} for a column with a JS JSON object type.
*/
export class JSONObjectField extends EntityFieldDefinition<object> {
protected validateInputValueInternal(value: object): boolean {
return typeof value === 'object' && !Array.isArray(value);
protected validateAndTransformInputValueInternal(
value: object
): { isValid: false } | { isValid: true; transformedValue: object } {
return typeof value === 'object' && !Array.isArray(value)
? { isValid: true, transformedValue: value }
: { isValid: false };
}
}
@@ -82,8 +130,12 @@ export class JSONObjectField extends EntityFieldDefinition<object> {
* {@link EntityFieldDefinition} for a enum column with a JS string or number type.
*/
export class EnumField extends EntityFieldDefinition<string | number> {
protected validateInputValueInternal(value: string | number): boolean {
return typeof value === 'number' || typeof value === 'string';
protected validateAndTransformInputValueInternal(
value: string | number
): { isValid: false } | { isValid: true; transformedValue: string | number } {
return typeof value === 'number' || typeof value === 'string'
? { isValid: true, transformedValue: value }
: { isValid: false };
}
}
@@ -91,8 +143,10 @@ export class EnumField extends EntityFieldDefinition<string | number> {
* {@link EntityFieldDefinition} for a column with a JS JSON array type.
*/
export class JSONArrayField extends EntityFieldDefinition<any[]> {
protected validateInputValueInternal(value: any[]): boolean {
return Array.isArray(value);
protected validateAndTransformInputValueInternal(
value: any[]
): { isValid: false } | { isValid: true; transformedValue: any[] } {
return Array.isArray(value) ? { isValid: true, transformedValue: value } : { isValid: false };
}
}
@@ -101,7 +155,9 @@ export class JSONArrayField extends EntityFieldDefinition<any[]> {
* Does not do any validation.
*/
export class MaybeJSONArrayField extends EntityFieldDefinition<any | any[]> {
protected validateInputValueInternal(_value: any): boolean {
return true;
protected validateAndTransformInputValueInternal(
value: any
): { isValid: false } | { isValid: true; transformedValue: any } {
return { isValid: true, transformedValue: value };
}
}
+35 -17
View File
@@ -82,12 +82,15 @@ export default class EntityLoader<
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<ReadonlyMap<NonNullable<TFields[N]>, readonly Result<TEntity>[]>> {
this.validateFieldValues(fieldName, fieldValues);
const validatedAndTransformedFieldValues = this.validateAndTransformFieldValues(
fieldName,
fieldValues
);
const fieldValuesToFieldObjects = await this.dataManager.loadManyByFieldEqualingAsync(
this.queryContext,
fieldName,
fieldValues
validatedAndTransformedFieldValues
);
return await this.constructAndAuthorizeEntitiesAsync(fieldValuesToFieldObjects);
@@ -199,16 +202,29 @@ export default class EntityLoader<
fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
): Promise<readonly Result<TEntity>[]> {
for (const fieldEqualityOperand of fieldEqualityOperands) {
const fieldValues = isSingleValueFieldEqualityCondition(fieldEqualityOperand)
? [fieldEqualityOperand.fieldValue]
: fieldEqualityOperand.fieldValues;
this.validateFieldValues(fieldEqualityOperand.fieldName, fieldValues);
}
const validatedAndTransformedFieldEqualityOperands: FieldEqualityCondition<TFields, N>[] =
fieldEqualityOperands.map((fieldEqualityOperand) => {
if (isSingleValueFieldEqualityCondition(fieldEqualityOperand)) {
return {
...fieldEqualityOperand,
fieldValue: this.validateAndTransformFieldValues(fieldEqualityOperand.fieldName, [
fieldEqualityOperand.fieldValue,
])[0]!,
};
} else {
return {
...fieldEqualityOperand,
fieldValues: this.validateAndTransformFieldValues(
fieldEqualityOperand.fieldName,
fieldEqualityOperand.fieldValues
),
};
}
});
const fieldObjects = await this.dataManager.loadManyByFieldEqualityConjunctionAsync(
this.queryContext,
fieldEqualityOperands,
validatedAndTransformedFieldEqualityOperands,
querySelectionModifiers
);
const uncheckedEntityResults = this.tryConstructEntities(fieldObjects);
@@ -347,17 +363,19 @@ export default class EntityLoader<
});
}
private validateFieldValues<N extends keyof Pick<TFields, TSelectedFields>>(
fieldName: N,
fieldValues: readonly TFields[N][]
): void {
private validateAndTransformFieldValues<
N extends keyof Pick<TFields, TSelectedFields>,
T extends TFields[N]
>(fieldName: N, fieldValues: readonly T[]): readonly T[] {
const fieldDefinition = this.entityConfiguration.schema.get(fieldName);
invariant(fieldDefinition, `must have field definition for field = ${String(fieldName)}`);
for (const fieldValue of fieldValues) {
const isInputValid = fieldDefinition.validateInputValue(fieldValue);
if (!isInputValid) {
return fieldValues.map((fieldValue) => {
const result = fieldDefinition.validateAndTransformInputValue(fieldValue);
if (result.isValid) {
return result.transformedValue;
} else {
throw new EntityInvalidFieldValueError(this.entityClass, fieldName, fieldValue);
}
}
});
}
}
+25 -9
View File
@@ -81,16 +81,22 @@ abstract class BaseMutator<
protected readonly metricsAdapter: IEntityMetricsAdapter
) {}
protected validateFields(fields: Partial<TFields>): void {
protected validateAndTransformFields(fields: Partial<TFields>): Partial<TFields> {
const result: Partial<TFields> = {};
for (const fieldName in fields) {
const fieldValue = fields[fieldName];
const fieldDefinition = this.entityConfiguration.schema.get(fieldName);
invariant(fieldDefinition, `must have field definition for field = ${fieldName}`);
const isInputValid = fieldDefinition.validateInputValue(fieldValue);
if (!isInputValid) {
const validationResult = fieldDefinition.validateAndTransformInputValue(fieldValue);
if (validationResult.isValid) {
result[fieldName] = validationResult.transformedValue;
} else {
throw new EntityInvalidFieldValueError(this.entityClass, fieldName, fieldValue);
}
}
return result;
}
protected async executeMutationValidatorsAsync(
@@ -210,11 +216,13 @@ export class CreateMutator<
private async createInternalAsync(
queryContext: EntityTransactionalQueryContext
): Promise<Result<TEntity>> {
this.validateFields(this.fieldsForEntity);
const transformedAndValidatedFieldsForEntity = this.validateAndTransformFields(
this.fieldsForEntity
);
const temporaryEntityForPrivacyCheck = new this.entityClass(this.viewerContext, {
[this.entityConfiguration.idField]: '00000000-0000-0000-0000-000000000000', // zero UUID
...this.fieldsForEntity,
...transformedAndValidatedFieldsForEntity,
} as unknown as TFields);
const authorizeCreateResult = await asyncResult(
@@ -249,7 +257,10 @@ export class CreateMutator<
{ type: EntityMutationType.CREATE }
);
const insertResult = await this.databaseAdapter.insertAsync(queryContext, this.fieldsForEntity);
const insertResult = await this.databaseAdapter.insertAsync(
queryContext,
transformedAndValidatedFieldsForEntity
);
const entityLoader = this.entityLoaderFactory.forLoad(
this.viewerContext,
@@ -409,9 +420,14 @@ export class UpdateMutator<
private async updateInternalAsync(
queryContext: EntityTransactionalQueryContext
): Promise<Result<TEntity>> {
this.validateFields(this.updatedFields);
const transformedAndValidatedFieldsForEntity = this.validateAndTransformFields(
this.fieldsForEntity
) as TFields;
const entityAboutToBeUpdated = new this.entityClass(this.viewerContext, this.fieldsForEntity);
const entityAboutToBeUpdated = new this.entityClass(
this.viewerContext,
transformedAndValidatedFieldsForEntity
);
const authorizeUpdateResult = await asyncResult(
this.privacyPolicy.authorizeUpdateAsync(
this.viewerContext,
@@ -464,7 +480,7 @@ export class UpdateMutator<
)
);
queryContext.appendPostCommitInvalidationCallback(
entityLoader.invalidateFieldsAsync.bind(entityLoader, this.fieldsForEntity)
entityLoader.invalidateFieldsAsync.bind(entityLoader, transformedAndValidatedFieldsForEntity)
);
const unauthorizedEntityAfterUpdate = new this.entityClass(this.viewerContext, updateResult);
@@ -288,7 +288,7 @@ describe(EnforcingEntityLoader, () => {
'invalidateFieldsAsync',
'invalidateEntityAsync',
'tryConstructEntities',
'validateFieldValues',
'validateAndTransformFieldValues',
'constructAndAuthorizeEntitiesAsync',
];
expect(loaderProperties).toEqual(expect.arrayContaining(knownLoaderOnlyDifferences));
@@ -13,12 +13,15 @@ import {
EnumField,
JSONArrayField,
MaybeJSONArrayField,
LowercaseStringField,
} from '../EntityFields';
import describeFieldTestCase from '../utils/testing/describeFieldTestCase';
class TestFieldDefinition extends EntityFieldDefinition<string> {
protected validateInputValueInternal(value: string): boolean {
return value === 'helloworld';
protected validateAndTransformInputValueInternal(
value: string
): { isValid: false } | { isValid: true; transformedValue: string } {
return value === 'helloworld' ? { isValid: true, transformedValue: value } : { isValid: false };
}
}
@@ -35,49 +38,97 @@ describe(EntityFieldDefinition, () => {
test('validator returns true when value is null', () => {
const fieldDefinition = new TestFieldDefinition({ columnName: 'wat', cache: true });
expect(fieldDefinition.validateInputValue(null)).toBe(true);
expect(fieldDefinition.validateAndTransformInputValue(null).isValid).toBe(true);
});
test('validator returns true when value is undefined', () => {
const fieldDefinition = new TestFieldDefinition({ columnName: 'wat', cache: true });
expect(fieldDefinition.validateInputValue(undefined)).toBe(true);
expect(fieldDefinition.validateAndTransformInputValue(undefined).isValid).toBe(true);
});
test('validator returns false when value is invalid', () => {
const fieldDefinition = new TestFieldDefinition({ columnName: 'wat', cache: true });
expect(fieldDefinition.validateInputValue('nothelloworld')).toBe(false);
expect(fieldDefinition.validateAndTransformInputValue('nothelloworld').isValid).toBe(false);
});
test('validator returns true when value is valid', () => {
const fieldDefinition = new TestFieldDefinition({ columnName: 'wat', cache: true });
expect(fieldDefinition.validateInputValue('helloworld')).toBe(true);
expect(fieldDefinition.validateAndTransformInputValue('helloworld').isValid).toBe(true);
});
});
describeFieldTestCase(new StringField({ columnName: 'wat' }), ['hello', ''], [1, true, {}, [[]]]);
describeFieldTestCase(
new StringField({ columnName: 'wat' }),
['hello', ''],
[1, true, {}, [[]]],
[{ in: 'hello', out: 'hello' }]
);
describeFieldTestCase(
new LowercaseStringField({ columnName: 'wat' }),
['HELLO', 'hello'],
[1, true, {}, [[]]],
[
{ in: 'low', out: 'low' },
{ in: 'UP', out: 'up' },
]
);
describeFieldTestCase(
new UUIDField({ columnName: 'wat' }),
[uuidv1(), uuidv3('wat', uuidv3.DNS), uuidv4(), uuidv5('wat', uuidv5.DNS)],
[uuidv4().replace('-', ''), '', 'hello']
[uuidv4().replace('-', ''), '', 'hello'],
[uuidv4()].map((u) => ({ in: u, out: u }))
);
describeFieldTestCase(
new DateField({ columnName: 'wat' }),
[new Date()],
[Date.now()],
[new Date()].map((u) => ({ in: u, out: u }))
);
describeFieldTestCase(
new BooleanField({ columnName: 'wat' }),
[true, false],
[0, 1, ''],
[{ in: true, out: true }]
);
describeFieldTestCase(
new IntField({ columnName: 'wat' }),
[1],
['1', 0.5, true],
[{ in: 1, out: 1 }]
);
describeFieldTestCase(
new FloatField({ columnName: 'wat' }),
[1, 0.5, -0.5],
['1'],
[{ in: 3.5, out: 3.5 }]
);
describeFieldTestCase(new DateField({ columnName: 'wat' }), [new Date()], [Date.now()]);
describeFieldTestCase(new BooleanField({ columnName: 'wat' }), [true, false], [0, 1, '']);
describeFieldTestCase(new IntField({ columnName: 'wat' }), [1], ['1', 0.5, true]);
describeFieldTestCase(new FloatField({ columnName: 'wat' }), [1, 0.5, -0.5], ['1']);
describeFieldTestCase(
new StringArrayField({ columnName: 'wat' }),
[[['what']] as any, [[]] as any], // jest test cases need extra wrapping array
['hello']
['hello'],
[{ in: ['hello'], out: ['hello'] }]
);
describeFieldTestCase(
new JSONObjectField({ columnName: 'wat' }),
[{}],
[true, 'hello'],
[{ in: { hello: 1 }, out: { hello: 1 } }]
);
describeFieldTestCase(
new EnumField({ columnName: 'wat' }),
['hello', 1],
[true],
[{ in: 1, out: 1 }]
);
describeFieldTestCase(new JSONObjectField({ columnName: 'wat' }), [{}], [true, 'hello']);
describeFieldTestCase(new EnumField({ columnName: 'wat' }), ['hello', 1], [true]);
describeFieldTestCase(
new JSONArrayField({ columnName: 'wat' }),
[[[1, 2]] as any, [['hello']] as any], // jest test cases need extra wrapping array
[1, 'hello']
[1, 'hello'],
[{ in: [1, 2], out: [1, 2] }]
);
describeFieldTestCase(
new MaybeJSONArrayField({ columnName: 'wat' }),
[1, 'hello', [['hello']]], // jest test cases need extra wrapping array
[]
[],
[{ in: 1, out: 1 }]
);
@@ -41,6 +41,7 @@ describe(EntityLoader, () => {
testIndexedField: 'h1',
intField: 5,
stringField: 'huh',
lowercasedField: 'lw1',
dateField: dateToInsert,
nullableField: null,
},
@@ -49,6 +50,7 @@ describe(EntityLoader, () => {
testIndexedField: 'h2',
intField: 3,
stringField: 'huh',
lowercasedField: 'lw2',
dateField: dateToInsert,
nullableField: null,
},
@@ -87,6 +89,11 @@ describe(EntityLoader, () => {
);
expect(entities.map((m) => m.getID())).toEqual([id1, id2]);
const entitiesLoadedTransformedField = await enforceResultsAsync(
entityLoader.loadManyByFieldEqualingAsync('lowercasedField', 'LW1')
);
expect(entitiesLoadedTransformedField.map((m) => m.getID())).toEqual([id1]);
const entityResultNumber3 = await entityLoader.loadByFieldEqualingAsync('intField', 3);
expect(entityResultNumber3).not.toBeNull();
expect(entityResultNumber3!.enforceValue().getID()).toEqual(id2);
@@ -134,6 +141,7 @@ describe(EntityLoader, () => {
{
customIdField: id1,
stringField: 'huh',
lowercasedField: 'lw1',
intField: 4,
testIndexedField: '4',
dateField: new Date(),
@@ -142,6 +150,7 @@ describe(EntityLoader, () => {
{
customIdField: id2,
stringField: 'huh',
lowercasedField: 'lw2',
intField: 4,
testIndexedField: '5',
dateField: new Date(),
@@ -150,6 +159,7 @@ describe(EntityLoader, () => {
{
customIdField: id3,
stringField: 'huh2',
lowercasedField: 'lw3',
intField: 4,
testIndexedField: '6',
dateField: new Date(),
@@ -231,6 +241,7 @@ describe(EntityLoader, () => {
{
customIdField: id1,
stringField: 'huh',
lowercasedField: 'lw1',
testIndexedField: '1',
intField: 3,
dateField: new Date(),
@@ -3,7 +3,7 @@ import { result, Result } from '@expo/results';
import Entity from '../Entity';
import { EntityCompanionDefinition } from '../EntityCompanionProvider';
import EntityConfiguration from '../EntityConfiguration';
import { UUIDField, StringField, DateField, IntField } from '../EntityFields';
import { UUIDField, StringField, DateField, IntField, LowercaseStringField } from '../EntityFields';
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
import ViewerContext from '../ViewerContext';
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
@@ -12,6 +12,7 @@ export type TestFields = {
customIdField: string;
testIndexedField: string;
stringField: string;
lowercasedField: string;
intField: number;
dateField: Date;
nullableField: string | null;
@@ -31,6 +32,9 @@ export const testEntityConfiguration = new EntityConfiguration<TestFields>({
stringField: new StringField({
columnName: 'string_field',
}),
lowercasedField: new LowercaseStringField({
columnName: 'lowercase_field',
}),
intField: new IntField({
columnName: 'number_field',
}),
@@ -86,6 +90,7 @@ export default class TestEntity extends Entity<TestFields, string, ViewerContext
customIdField: testValue,
testIndexedField: 'hello',
stringField: 'hello',
lowercasedField: 'hello',
intField: 1,
dateField: new Date(),
nullableField: null,
@@ -3,18 +3,32 @@ import { EntityFieldDefinition } from '../../EntityFieldDefinition';
export default function describeFieldTestCase<T>(
fieldDefinition: EntityFieldDefinition<T>,
validValues: T[],
invalidValues: any[]
invalidValues: any[],
transformValues: {
in: T;
out: T;
}[]
): void {
describe(fieldDefinition.constructor.name, () => {
if (validValues.length > 0) {
test.each(validValues)(`${fieldDefinition.constructor.name}.valid %p`, (value) => {
expect(fieldDefinition.validateInputValue(value)).toBe(true);
expect(fieldDefinition.validateAndTransformInputValue(value).isValid).toBe(true);
});
}
if (invalidValues.length > 0) {
test.each(invalidValues)(`${fieldDefinition.constructor.name}.invalid %p`, (value) => {
expect(fieldDefinition.validateInputValue(value)).toBe(false);
expect(fieldDefinition.validateAndTransformInputValue(value).isValid).toBe(false);
});
}
if (transformValues.length > 0) {
test.each(transformValues)(`${fieldDefinition.constructor.name}.transformed %p`, (value) => {
const result = fieldDefinition.validateAndTransformInputValue(value.in);
expect(result.isValid).toBe(true);
if (result.isValid) {
expect(result.transformedValue).toEqual(value.out);
}
});
}
});