mirror of
https://github.com/expo/entity.git
synced 2026-11-13 20:28:06 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f102ef29d |
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 };
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user