Add option to ignore fields' casing

This commit is contained in:
Nitsan Amit 2022-07-15 11:42:14 +03:00
Родитель 158a60616b
Коммит f7f353c78b
6 изменённых файлов: 225 добавлений и 13 удалений

Просмотреть файл

@ -9,6 +9,15 @@ export interface DataOptions{
*/
allowCache?:boolean,
/**
* If true, the entity fields parsing will be case insensitive (and so for all sub-entities).
* e.g, a value for a field named "ProviderName" will be parsed into fields: "providerName", "providername", "ProviderName", "PROVIDERNAME",
* and any other class field that satisfies field.toLowerCase() === "ProviderName".toLowerCase().
* Notice that casing of fields on the raw "fieldData" object passed to the entity's "parse" function, will remain as is.
* @default false
*/
ignoreFieldsCasing?:boolean,
/**
* The {DataAvailability} mode for fetching data.
*/

Просмотреть файл

@ -41,8 +41,10 @@ export class Modeler {
modelData: Partial<TEntity> = {},
subModels: Array<Observable<ModelPropertyValue<TEntity>>> = [];
if (entity instanceof ModelEntity)
modelData.id = rawData[entityIdProperty];
if (options.ignoreFieldsCasing) {
rawData = Modeler.lowercaseKeysOfRawData(rawData);
entityIdProperty = typeof(entityIdProperty) === "string" ? entityIdProperty.toLowerCase() : entityIdProperty;
}
let getModelDataError:Error = new Error(`Failed to create ${entity.singularName}.`);
@ -72,7 +74,7 @@ export class Modeler {
return;
}
let entityFieldRawData: any = Modeler.getFieldRawData<TRawData>(entityField, rawData);
let entityFieldRawData: any = Modeler.getFieldRawData<TRawData>(entityField, rawData, options.ignoreFieldsCasing);
if (entityField.parse) {
try {
@ -104,6 +106,9 @@ export class Modeler {
}
});
if (entity instanceof ModelEntity)
modelData.id = rawData[entityIdProperty];
let model$:Observable<TEntity>;
if (subModels.length) {
@ -182,25 +187,44 @@ export class Modeler {
});
}
/**
* If @rawData is an object, this method will return the same object, with all its keys lowercased.
* The function is not recursive, since it's anyway called on each sub-model.
*/
static lowercaseKeysOfRawData<TRawData extends any = any>(rawData:TRawData):any {
if (rawData instanceof Object && !(rawData instanceof Array)) {
const newRawData: any = {};
for (const key in rawData) {
if (rawData.hasOwnProperty(key)) {
newRawData[key.toLowerCase()] = rawData[key];
}
}
return newRawData;
}
else {
return rawData;
}
}
/**
* Given an EntityField configuration and the raw data provided to the entity's modeler, returns the raw data to use for that field.
*/
static getFieldRawData<TRawData extends any = any>(entityField: Field, rawData:TRawData):any{
static getFieldRawData<TRawData extends any = any>(entityField: Field, rawData:TRawData, ignoreCase: boolean):any{
let fieldRawData: any;
if (entityField.data) {
if (entityField.data instanceof Array) {
for (let i = 0, path:string; i < entityField.data.length && fieldRawData === undefined; i++) {
path = entityField.data[i];
path = ignoreCase ? entityField.data[i].toLowerCase() : entityField.data[i];
const value = path === FIELD_DATA_SELF ? rawData : get(rawData, path);
if (value !== undefined && value !== null)
fieldRawData = value;
}
}
else
fieldRawData = entityField.data === FIELD_DATA_SELF ? rawData : get(rawData, entityField.data);
fieldRawData = entityField.data === FIELD_DATA_SELF ? rawData : get(rawData, ignoreCase ? entityField.data.toLowerCase() : entityField.data);
}
else
fieldRawData = rawData[entityField.id];
fieldRawData = rawData[ignoreCase ? entityField.id.toLowerCase() : entityField.id];
return fieldRawData;
}

Просмотреть файл

@ -0,0 +1,19 @@
import {Entity} from "../../lib/config/decorators/entity.decorator";
import {EntityModelBase} from "../../lib/config/entity-model.base";
import {EntityField} from "../../lib/config/decorators/entity-field.decorator";
export const commentStatusValues = [
{id: 0, name: 'Approved'},
{id: 1, name: 'Rejected'},
{id: 2, name: 'PENDING'},
];
@Entity({
singularName: "Comment status",
pluralName: "Comment statuses",
values: commentStatusValues
})
export class CommentStatus extends EntityModelBase<number> {
@EntityField() name: string;
}

Просмотреть файл

@ -0,0 +1,31 @@
import {Entity} from "../../lib/config/decorators/entity.decorator";
import {EntityModelBase} from "../../lib/config/entity-model.base";
import {EntityField} from "../../lib/config/decorators/entity-field.decorator";
import {User} from "./user.entity";
import {FIELD_DATA_SELF} from "../../lib/config/entity-field.config";
import {CommentStatus} from "./comment-status.entity";
@Entity({
singularName: 'Comment',
pluralName: 'Comments',
endpoint: 'comments',
idProperty: 'commentID',
})
export class Comment extends EntityModelBase {
@EntityField()
comment: string;
@EntityField({data: FIELD_DATA_SELF})
commentObject: Object;
@EntityField()
CreatedBy: User;
@EntityField()
status: CommentStatus;
@EntityField({arrayOf: User})
Liked: Array<User>;
}

33
test/mock/user.entity.ts Normal file
Просмотреть файл

@ -0,0 +1,33 @@
import {Entity} from "../../lib/config/decorators/entity.decorator";
import {EntityField} from "../../lib/config/decorators/entity-field.decorator";
import {EntityModelBase} from "../../lib/main";
export enum UserType {
ADMIN = "ADMIN",
USER = "USER",
}
@Entity({
singularName: 'User',
pluralName: 'Users',
endpoint: 'users',
readonly: true,
})
export class User extends EntityModelBase {
@EntityField({data: ["username"]})
name: string;
@EntityField()
Age: number;
@EntityField({required: false, arrayOf: String})
nickNames?: Array<string>;
@EntityField({data: "address", parse: (fieldData) => fieldData.city})
city: string;
@EntityField({type: UserType, defaultValue: UserType.USER})
USER_TYPE?: UserType;
}

Просмотреть файл

@ -3,6 +3,7 @@ import {Animal, Person, Thing} from '../mock/thing.entity';
import {DataStoreService} from '../../lib/data_access/data-store.service';
import {Paris} from '../../lib/paris';
import {Todo} from "../mock/todo.entity";
import {Comment} from "../mock/commet.entity";
import {DataEntityType} from "../../lib/api/entity/data-entity.base";
import {Tag} from "../mock/tag.value-object";
import {Modeler} from "../../lib/modeling/modeler";
@ -19,6 +20,8 @@ import {ModelBase} from "../../lib/config/model.base";
import {ValueObject} from "../../lib/config/decorators/value-object.decorator";
import {ModelConfig} from "../../lib/config/model-config";
import {Dog} from "../mock/dog.entity";
import {User, UserType} from "../mock/user.entity";
import {commentStatusValues} from "../mock/comment-status.entity";
describe('Modeler', () => {
let paris: Paris;
@ -135,35 +138,128 @@ describe('Modeler', () => {
});
it('uses the id of the field if no data was specified', () => {
expect(Modeler.getFieldRawData(baseField, rawData)).toEqual(rawData.name);
expect(Modeler.getFieldRawData(baseField, rawData, false)).toEqual(rawData.name);
});
it('uses the "data" of the field as the path of the data to use', () => {
const fieldConfig = Object.assign({ data: 'alias' }, baseField);
expect(Modeler.getFieldRawData(fieldConfig, rawData)).toEqual(rawData[fieldConfig.data]);
expect(Modeler.getFieldRawData(fieldConfig, rawData, false)).toEqual(rawData[fieldConfig.data]);
});
it('uses the "data" of the field as the path of the data to use, even inside objects', () => {
const fieldConfig = Object.assign({ data: 'fullName.firstName' }, baseField);
expect(Modeler.getFieldRawData(fieldConfig, rawData)).toEqual(rawData.fullName.firstName);
expect(Modeler.getFieldRawData(fieldConfig, rawData, false)).toEqual(rawData.fullName.firstName);
});
it('uses the "data" of the field to prioritize raw data properties', () => {
const fieldConfig = Object.assign({ data: ['exactName', 'alias', "name"] }, baseField);
expect(Modeler.getFieldRawData(fieldConfig, rawData)).toEqual(rawData.alias);
expect(Modeler.getFieldRawData(fieldConfig, rawData, false)).toEqual(rawData.alias);
});
it('uses the "data" of the field to prioritize raw data properties, event for paths', () => {
const fieldConfig = Object.assign({ data: ['exactName', 'fullName.firstName', 'alias', "name"] }, baseField);
expect(Modeler.getFieldRawData(fieldConfig, rawData)).toEqual(rawData.fullName.firstName);
expect(Modeler.getFieldRawData(fieldConfig, rawData, false)).toEqual(rawData.fullName.firstName);
});
it('uses the special "FIELD_DATA_SELF" data config to use the whole rawData', () => {
const fieldConfig = Object.assign({ data: FIELD_DATA_SELF }, baseField);
expect(Modeler.getFieldRawData(fieldConfig, rawData)).toEqual(rawData);
expect(Modeler.getFieldRawData(fieldConfig, rawData, false)).toEqual(rawData);
})
});
describe('ignorePropertiesCase', () => {
const commentRawData = {
CommentId: 'comment1',
COMMENT: 'Hello world!',
StatuS: 2,
createdBy: {
UserName: 'nitsanamit',
age: 26,
NickNames: ['nits', 'namit'],
address: {
city: 'Herzliya',
},
user_type: UserType.ADMIN
},
liked: [
{
username: 'billgates',
AGE: 56,
Address: {
city: 'California',
}
},
{
Name: 'Robert',
UserName: 'Spongbob',
age: 5,
NickNames: ['Squarepants'],
address: {
City: 'Bikini Bottom',
},
User_Type: UserType.USER
},
]
};
const commentEntityConfig = (<DataEntityType<Comment>>Comment).entityConfig;
let commentItem$:Observable<Comment>;
beforeEach(() => {
commentItem$ = paris.modeler.modelEntity(commentRawData, commentEntityConfig, {ignoreFieldsCasing: true});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('ignores casing of simple entity fields', done => {
commentItem$.subscribe((comment: Comment) => {
expect(comment.id).toEqual(commentRawData.CommentId);
expect(comment.comment).toEqual(commentRawData.COMMENT);
expect(comment.status).toEqual(commentStatusValues[commentRawData.StatuS]);
done();
});
});
it('ignores casing of recursive entity models', done => {
commentItem$.subscribe((comment: Comment) => {
expect(comment.CreatedBy.name).toEqual(commentRawData.createdBy.UserName);
expect(comment.CreatedBy.Age).toEqual(commentRawData.createdBy.age);
expect(comment.CreatedBy.nickNames).toEqual(commentRawData.createdBy.NickNames);
expect(comment.CreatedBy.city).toEqual(commentRawData.createdBy.address.city);
expect(comment.CreatedBy.USER_TYPE).toEqual(commentRawData.createdBy.user_type);
done();
});
});
it('ignores casing of array properties', done => {
commentItem$.subscribe((comment: Comment) => {
expect(comment.Liked[0].name).toEqual(commentRawData.liked[0].username);
expect(comment.Liked[0].Age).toEqual(commentRawData.liked[0].AGE);
expect(comment.Liked[0].nickNames).toStrictEqual([]);
expect(comment.Liked[0].city).toEqual(commentRawData.liked[0].Address.city);
expect(comment.Liked[0].USER_TYPE).toEqual((<DataEntityType<User>>User).entityConfig.fields.get('USER_TYPE').defaultValue);
expect(comment.Liked[1].name).toEqual(commentRawData.liked[1].UserName);
expect(comment.Liked[1].Age).toEqual(commentRawData.liked[1].age);
expect(comment.Liked[1].nickNames).toEqual(commentRawData.liked[1].NickNames);
expect(comment.Liked[1].USER_TYPE).toEqual(commentRawData.liked[1].User_Type);
done();
});
});
it('keeps the fieldsData original casing for entity fields with a "parse" function', done => {
commentItem$.subscribe((comment: Comment) => {
expect(comment.Liked[1].city).toBeNull();
done();
});
});
});
describe('defaultValue', () => {
it("doesn't set default value to a field that has value", done => {
const todoRawData = { id: 1, text: 'First', isDone: true };