diff --git a/src/dataMapper.js b/src/dataMapper.js index ba6828a..40ffd28 100644 --- a/src/dataMapper.js +++ b/src/dataMapper.js @@ -1,5 +1,5 @@ const Convention = require('./convention') -const { entity } = require('@herbsjs/herbs') +const { entity, checker } = require('@herbsjs/herbs') const dependency = { convention: Convention } class DataMapper { @@ -34,14 +34,27 @@ class DataMapper { const fields = Object.keys(schema) .map((field) => { - if (typeof schema[field] === 'function') return { type: Function } + if (typeof schema[field] === 'function') return null + const isArray = Array.isArray(schema[field].type) const type = fieldType(schema[field].type) const isEntity = entity.isEntity(type) const nameDb = convention.toCollectionFieldName(field) + const isID = entityIDs.includes(field) - return { name: field, type, isEntity, nameDb, isArray, isID } + + const object = { name: field, type, isEntity, nameDb, isArray, isID } + + if (isEntity) { + const entitySchema = isArray + ? schema[field].type[0].prototype.meta.schema + : schema[field].type.prototype.meta.schema + object.children = this.buildAllFields(entitySchema, [], convention) + } + + return object }) + .filter(Boolean) const allFields = fields.filter((f) => f.type !== Function) @@ -58,16 +71,56 @@ class DataMapper { collectionFields() { return this.allFields - .filter((i) => !i.isEntity) .map((i) => i.nameDb) } + isNotNullOrUndefined(field, instance) { + if (instance[field.name] === null || instance[field.name] === undefined) return false + return true + } + + transformField(field, instance) { + if (field.isEntity) { + return { [field.nameDb]: this.parseEntity(field, instance[field.name]) } + } + + return { [field.nameDb]: instance[field.name] } + } + + parseEntity(field, value) { + if (field.isArray && checker.isArray(value)) { + const parsedArray = value.map(item => this.parseEntity(field, item)) + return parsedArray.reduce((acc, curr, index) => { + acc[index] = curr + return acc + }, {}) + } + + const parsedEntity = Object.keys(value).reduce((acc, key) => { + if (value[key] === null || value[key] === undefined) return acc + + const childField = field.children.find((i) => i.name === key) + + if (childField?.isEntity) { + acc[childField.nameDb] = this.parseEntity(childField, value[key]) + + return acc + } + + acc[childField.nameDb] = value[key] + + return acc + }, {}) + + return parsedEntity + } + collectionFieldsWithValue(instance) { let collectionFields = this.allFields - .filter((i) => !i.isEntity) - .map(i => ({ [i.nameDb]: instance[i.name] })) - .reduce((x, y) => ({ ...x, ...y })) + .filter((field) => this.isNotNullOrUndefined(field, instance)) + .map((field) => this.transformField(field, instance)) + .reduce((acc, current) => ({ ...acc, ...current }), {}) if (instance.id === undefined) { delete instance.id @@ -83,9 +136,9 @@ class DataMapper { buildProxy() { - function getDataParser(type, isArray) { + function getDataParser(type, isArray, isArrayOfEntities, field) { function arrayDataParser(value, parser) { - if (value === null) return null + if (checker.isEmpty(value)) return null return value.map((i) => parser(i)) } @@ -94,11 +147,34 @@ class DataMapper { return parser(value) } - if (isArray) { + if (isArray && !isArrayOfEntities) { const parser = getDataParser(type, false) return (value) => arrayDataParser(value, parser) } + if (isArrayOfEntities) { + return (value) => { + if (checker.isEmpty(value)) return null + return value?.map((item) => { + const object = Object.keys(item).reduce((obj, key) => { + const childField = field?.children.find((i) => i.nameDb === key) + + if (childField.isEntity) { + obj[childField.name] = processEntity(childField, item) + + return obj + } + + const parser = getDataParser(field.type, false) + obj[childField.name] = parser(item[childField.nameDb]) + + return obj + }, {}) + return object + }) + } + } + if ((type === Date) || (!convention.isScalarType(type))) return (x) => x @@ -108,6 +184,44 @@ class DataMapper { const convention = this.convention const proxy = {} + function processEntity(field, payload) { + const entityValue = payload[field.nameDb] + + if (checker.isEmpty(entityValue)) return undefined + + const object = field.type.schema.fields.reduce((obj, entityField) => { + const fieldNameDb = convention.toCollectionFieldName(entityField.name) + + const typeIsArray = checker.isArray(entityField.type) + + const isEntity = typeIsArray + ? entity.isEntity(entityField.type[0]) + : entity.isEntity(entityField.type) + + if (isEntity) { + const childField = field?.children.find((i) => i.name === entityField.name) + + if (childField.isArray) { + const arrayOfEntityParser = getDataParser(childField.type, childField.isArray, childField.isEntity, childField) + obj[entityField.name] = arrayOfEntityParser(payload[field.nameDb][fieldNameDb]) + + return obj + } + + obj[entityField.name] = processEntity(childField, payload[field.nameDb]) + + return obj + } + + const fieldParser = getDataParser(entityField.type, Array.isArray(entityField.type)) + + obj[entityField.name] = fieldParser(payload[field.nameDb][fieldNameDb]) + return obj + }, {}) + + return object + } + Object.defineProperty(proxy, '_payload', { enumerable: false, wricollection: true, @@ -126,7 +240,15 @@ class DataMapper { Object.defineProperty(proxy, field.name, { enumerable: true, get: function () { - if (field.isEntity) return undefined + if (field.isEntity && !field.isArray) { + return processEntity(field, this._payload) + } + + if (field.isEntity && field.isArray) { + const arrayOfEntityParser = getDataParser(field.type, field.isArray, field.isEntity, field) + return arrayOfEntityParser(this._payload[nameDb]) + } + return parser(this._payload[nameDb]) } }) diff --git a/src/herbs2mongo.js b/src/herbs2mongo.js index 2696932..4f84aea 100644 --- a/src/herbs2mongo.js +++ b/src/herbs2mongo.js @@ -1,3 +1,4 @@ const Repository = require('./repository') +const DataMapper = require('./dataMapper') -module.exports = { Repository } \ No newline at end of file +module.exports = { Repository, DataMapper } \ No newline at end of file diff --git a/test/dataMapper.test.js b/test/dataMapper.test.js index fb66e6a..fa0224f 100644 --- a/test/dataMapper.test.js +++ b/test/dataMapper.test.js @@ -38,7 +38,6 @@ describe('Data Mapper', () => { assert.deepStrictEqual(toEntity.idField, 1) assert.deepStrictEqual(toEntity.field1, true) assert.deepStrictEqual(toEntity.fieldName, false) - }) it('should convert an entity field to the collection string convetion', () => { @@ -98,6 +97,213 @@ describe('Data Mapper', () => { }) }) + describe('Simple Nested Entity', () => { + const GreatGreatGrandChildEntity = entity('Great Great-Grand Child entity', { + simpleString: field(String), + simpleBoolean: field(Boolean) + }) + + const GreatGrandChildEntity = entity('Great-Grand Child entity', { + simpleString: field(String), + simpleNumber: field(Number), + simpleStringArray: field([String]), + greatGreatGrandChild: field([GreatGreatGrandChildEntity]) + }) + + const CousinEntity = entity('Cousin entity', { + greatGrandChildEntity: field(GreatGrandChildEntity) + }) + + const GrandChildEntity = entity('Grand Child entity', { + greatGrandChild: field(GreatGrandChildEntity), + cousin: field(CousinEntity) + }) + + const ChildEntity = entity('Child entity', { + grandChild: field(GrandChildEntity), + simpleString: field(String) + }) + + + const givenAnNestedEntity = () => { + + return entity('A nested entity', { + idField: field(Number), + field1: field(Boolean), + childEntity: field(ChildEntity), + arrayChildEntity: field([ChildEntity]) + }) + } + + it('should convert data from collection to nested entity', () => { + //given + const Entity = givenAnNestedEntity() + const entityIDs = ['idField'] + const dataMapper = new DataMapper(Entity, entityIDs) + const childEntity = new ChildEntity() + childEntity.grandChild = new GrandChildEntity() + childEntity.simpleString = 'String' + childEntity.grandChild.greatGrandChild = new GreatGrandChildEntity() + childEntity.grandChild.greatGrandChild.simpleString = 'String' + childEntity.grandChild.greatGrandChild.simpleStringArray = ['String'] + childEntity.grandChild.greatGrandChild.simpleNumber = 1 + const greatGreatGrandChild = new GreatGreatGrandChildEntity() + greatGreatGrandChild.simpleString = 'greatGreatGrandChildEntity' + childEntity.grandChild.greatGrandChild.greatGreatGrandChild = [greatGreatGrandChild] + + //when + const toEntity = dataMapper.toEntity({ + id_field: 1, + field1: true, + child_entity: { + grand_child: { + great_grand_child: { + simple_string: 'String', + simple_string_array: [ + 'String' + ], + simple_number: 1, + great_great_grand_child: [ + { + simple_string: 'greatGreatGrandChildEntity' + } + ], + cousin: null + }, + }, + simple_string: 'String' + }, + array_child_entity: [ + { + grand_child: { + great_grand_child: { + simple_string: 'String', + simple_string_array: [ + 'String' + ], + simple_number: 1, + great_great_grand_child: [ + { + simple_string: 'greatGreatGrandChildEntity' + } + ] + } + }, + simple_string: 'String' + } + ] + }) + + //then + assert.deepStrictEqual(toEntity.idField, 1) + assert.deepStrictEqual(toEntity.field1, true) + assert.deepStrictEqual(toEntity.childEntity, childEntity) + assert.deepStrictEqual(toEntity.arrayChildEntity, [childEntity]) + }) + + it('should retrieve collection fields an nested entity', () => { + //given + const Entity = givenAnNestedEntity() + const entityInstance = new Entity() + entityInstance.idField = 1 + entityInstance.field1 = true + entityInstance.childEntity = { + field1: 'String' + } + const entityIDs = ['idField'] + const dataMapper = new DataMapper(Entity, entityIDs) + + //when + const toEntity = dataMapper.collectionFields() + + //then + assert.deepStrictEqual(toEntity, ['id_field', 'field1', 'child_entity', 'array_child_entity']) + }) + + it('should retrieve collection fields with values of an nested entity', () => { + //given + const Entity = givenAnNestedEntity() + const entityInstance = new Entity() + entityInstance.idField = 1 + entityInstance.field1 = true + entityInstance.childEntity = { + grandChild: { + greatGrandChild: { + simpleString: 'String', + greatGreatGrandChild: [ + { + simpleString: 'String' + } + ], + cousin: null + } + }, + simpleString: 'String' + } + entityInstance.arrayChildEntity = [ + { + grandChild: { + greatGrandChild: { + simpleString: 'String' + } + }, + simpleString: 'String' + } + ] + const entityIDs = ['idField'] + const dataMapper = new DataMapper(Entity, entityIDs) + + //when + const toEntity = dataMapper.collectionFieldsWithValue(entityInstance) + + //then + assert.deepStrictEqual(toEntity, { + id_field: 1, + field1: true, + child_entity: { + grand_child: { + great_grand_child: { + simple_string: 'String', + great_great_grand_child: { + '0': { + simple_string: 'String' + } + } + } + }, + simple_string: 'String' + }, + array_child_entity: { + 0: { + grand_child: { + great_grand_child: { + simple_string: 'String' + } + }, + simple_string: 'String' + } + } + }) + }) + + it('should retrieve collection fields with values of an nested entity with child entity as empty object', () => { + //given + const Entity = givenAnNestedEntity() + const entityInstance = new Entity() + entityInstance.idField = 1 + entityInstance.field1 = true + entityInstance.childEntity = {} + const entityIDs = ['idField'] + const dataMapper = new DataMapper(Entity, entityIDs) + + //when + const toEntity = dataMapper.collectionFieldsWithValue(entityInstance) + + //then + assert.deepStrictEqual(toEntity, { id_field: 1, field1: true, child_entity: {} }) + }) + }) + describe('Complex Entity - Multiple Types', () => { const givenAnComplexEntity = () => { diff --git a/test/queries/find.js b/test/queries/find.js index 759d80d..70ecd7c 100644 --- a/test/queries/find.js +++ b/test/queries/find.js @@ -134,7 +134,7 @@ describe('Query Find', () => { const ret = await itemRepo.find({ filter: { stringTest: ["aString"] } }) //then - assert.deepStrictEqual(ret[0].toJSON(), { id: '60edc25fc39277307ca9a7ff', stringTest: "aString", numberTest: 100, booleanTest: true , entityTest: undefined, entitiesTest: undefined }) + assert.deepStrictEqual(ret[0].toJSON(), { id: '60edc25fc39277307ca9a7ff', stringTest: "aString", numberTest: 100, booleanTest: true , entityTest: undefined, entitiesTest: null }) assert.deepStrictEqual(ret[0].isValid(),true ) }) diff --git a/test/queries/findByID.js b/test/queries/findByID.js index 558495d..8cb43df 100644 --- a/test/queries/findByID.js +++ b/test/queries/findByID.js @@ -64,7 +64,7 @@ describe('Query Find by ID', () => { const ret = await itemRepo.findByID(anEntity.id) //then - assert.deepStrictEqual(ret[0].toJSON(), { id: '60edc25fc39277307ca9a7ff', stringTest: "aString", numberTest: 100, booleanTest: true , entityTest: undefined, entitiesTest: undefined }) + assert.deepStrictEqual(ret[0].toJSON(), { id: '60edc25fc39277307ca9a7ff', stringTest: "aString", numberTest: 100, booleanTest: true , entityTest: undefined, entitiesTest: null }) assert.deepStrictEqual(ret[0].isValid(),true ) }) diff --git a/testdb/findByID.js b/testdb/findByID.js index 76309cc..f7c9972 100644 --- a/testdb/findByID.js +++ b/testdb/findByID.js @@ -8,25 +8,48 @@ let client = {} describe('Query Find by ID', () => { - const collection = 'test_repository' - const database = 'herbs2mongo_testdb' + const collection = 'test_repository' + const database = 'herbs2mongo_testdb' before(async () => { - client = await connection() - - await client.dropDatabase() - - await client.createCollection(collection) - - await client.collection(collection).insertOne( { _id: new ObjectId("60edc25fc39277307ca9a7ff"), number_test: 100, boolean_test: true, string_test: 'aString' }) - await client.collection(collection).insertOne( { _id: new ObjectId("70edc25fc39277307ca9a700"), number_test: 200, boolean_test: false }) - await client.collection(collection).insertOne( { _id: new ObjectId("80edd25fc39272307ca9a712"), number_test: 300, boolean_test: false }) + client = await connection() + + await client.dropDatabase() + + await client.createCollection(collection) + + await client.collection(collection).insertOne({ _id: new ObjectId("60edc25fc39277307ca9a7ff"), number_test: 100, boolean_test: true, string_test: 'aString' }) + await client.collection(collection).insertOne({ _id: new ObjectId("70edc25fc39277307ca9a700"), number_test: 200, boolean_test: false }) + await client.collection(collection).insertOne({ _id: new ObjectId("80edd25fc39272307ca9a712"), number_test: 300, boolean_test: false }) + await client.collection(collection).insertOne({ + _id: new ObjectId("64acbc1ba6a28fbd4501c25c"), + number_test: 400, + boolean_test: true, + string_test: "aString", + child_entity: { + number_test: 100, + boolean_test: true, + string_test: 'aString', + grand_child_test: { + number_test: 100, + boolean_test: true, + string_test: 'aString', + array_entities_test: [ + { + number_test: 100, + boolean_test: true, + string_test: 'aString', + } + ] + } + } + }) }) after(async () => { - await client.dropDatabase() + await client.dropDatabase() }) @@ -38,16 +61,39 @@ describe('Query Find by ID', () => { } } + const GreatGrandChildEntity = entity('Great-Grand child entity', { + numberTest: field(Number), + stringTest: field(String), + booleanTest: field(Boolean), + }) + + const GrandChildEntity = entity('Grand child entity', { + numberTest: field(Number), + stringTest: field(String), + booleanTest: field(Boolean), + arrayTest: field([String]), + arrayEntitiesTest: field([GreatGrandChildEntity]) + }) + + const ChildEntity = entity('Child entity', { + numberTest: field(Number), + stringTest: field(String), + booleanTest: field(Boolean), + arrayTest: field([String]), + grandChildTest: field(GrandChildEntity) + }) + const givenAnEntity = () => { return entity('A entity', { id: field(String), numberTest: field(Number), stringTest: field(String), - booleanTest: field(Boolean) + booleanTest: field(Boolean), + childEntity: field(ChildEntity) }) } - it('should return entities', async () => { + it('should return entity', async () => { //given const anEntity = givenAnEntity() const ItemRepository = givenAnRepositoryClass({ @@ -65,8 +111,50 @@ describe('Query Find by ID', () => { const ret = await itemRepo.findByID(anEntity.id) //then - assert.deepStrictEqual(ret[0].toJSON(), { id: '60edc25fc39277307ca9a7ff', stringTest: "aString", numberTest: 100, booleanTest: true }) - assert.deepStrictEqual(ret[0].isValid(),true ) + assert.deepStrictEqual(ret[0].toJSON(), { id: '60edc25fc39277307ca9a7ff', stringTest: "aString", numberTest: 100, booleanTest: true, childEntity: undefined }) + assert.deepStrictEqual(ret[0].isValid(), true) + }) + + it('should return nested entitiy', async () => { + //given + const anEntity = givenAnEntity() + const ItemRepository = givenAnRepositoryClass({ + entity: anEntity, + collection, + database, + ids: ['id'], + mongodb: await connection + }) + const injection = {} + const itemRepo = new ItemRepository(injection) + + anEntity.id = '64acbc1ba6a28fbd4501c25c' + //when + const ret = await itemRepo.findByID(anEntity.id) + + //then + assert.deepStrictEqual(ret[0].toJSON(), { + id: '64acbc1ba6a28fbd4501c25c', + numberTest: 400, + booleanTest: true, + stringTest: "aString", + childEntity: { + numberTest: 100, booleanTest: true, stringTest: 'aString', arrayTest: null, grandChildTest: { + numberTest: 100, + booleanTest: true, + stringTest: 'aString', + arrayTest: null, + arrayEntitiesTest: [ + { + numberTest: 100, + booleanTest: true, + stringTest: 'aString' + } + ] + } + } + }) + assert.deepStrictEqual(ret[0].isValid(), true) }) it('should return multiple entities', async () => { @@ -81,17 +169,17 @@ describe('Query Find by ID', () => { }) const injection = {} const itemRepo = new ItemRepository(injection) - + const ids = [ '60edc25fc39277307ca9a7ff', '80edd25fc39272307ca9a712', - ] + ] //when const ret = await itemRepo.findByID(ids) //then - assert.deepStrictEqual(ret[0].toJSON(), { id: '60edc25fc39277307ca9a7ff', stringTest: "aString", numberTest: 100, booleanTest: true }) - assert.deepStrictEqual(ret[0].isValid(),true ) + assert.deepStrictEqual(ret[0].toJSON(), { id: '60edc25fc39277307ca9a7ff', stringTest: "aString", numberTest: 100, booleanTest: true, childEntity: undefined }) + assert.deepStrictEqual(ret[0].isValid(), true) }) })