зеркало из https://github.com/microsoft/paris.git
Add option to ignore fields' casing
This commit is contained in:
Родитель
158a60616b
Коммит
f7f353c78b
|
@ -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>;
|
||||
|
||||
}
|
|
@ -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 };
|
||||
|
|
Загрузка…
Ссылка в новой задаче