diff --git a/packages/classes/mapped-types/tsconfig.spec.json b/packages/classes/mapped-types/tsconfig.spec.json index 1798b378a..de89ecb76 100644 --- a/packages/classes/mapped-types/tsconfig.spec.json +++ b/packages/classes/mapped-types/tsconfig.spec.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "../../../dist/out-tsc", "module": "commonjs", - "types": ["jest", "node"] + "types": ["jest", "node", "reflect-metadata"] }, "include": [ "**/*.spec.ts", diff --git a/packages/classes/src/lib/storages/class-instance.storage.ts b/packages/classes/src/lib/storages/class-instance.storage.ts index 02aa11138..729413a7e 100644 --- a/packages/classes/src/lib/storages/class-instance.storage.ts +++ b/packages/classes/src/lib/storages/class-instance.storage.ts @@ -1,5 +1,18 @@ import type { Constructible } from '../types'; +/* + # Implementation strategy + Create a tree of `Map`s, such that indexing the tree recursively (with items + of a key array, sequentially), traverses the tree, so that when the key array + is exhausted, the tree node we arrive at contains the value for that key + array under the guaranteed-unique `Symbol` key `dataSymbol`. +*/ + +type DataMap = Map; +type PathMap = Map; +type ArrayKeyedMap = PathMap | DataMap; +const DATA_SYMBOL = Symbol('map-data'); + /** * Internal ClassInstanceStorage * @@ -9,24 +22,24 @@ import type { Constructible } from '../types'; * @private */ export class ClassInstanceStorage { - private depthStorage = new WeakMap>(); + private depthStorage = new WeakMap(); private recursiveCountStorage = new WeakMap< Constructible, - Map + ArrayKeyedMap >(); getDepthAndCount( parent: Constructible, - member: string + member: string[] ): [depth?: number, count?: number] { return [this.getDepth(parent, member), this.getCount(parent, member)]; } - getDepth(parent: Constructible, member: string): number | undefined { + getDepth(parent: Constructible, member: string[]): number | undefined { return ClassInstanceStorage.getInternal(this.depthStorage, parent, member); } - getCount(parent: Constructible, member: string): number | undefined { + getCount(parent: Constructible, member: string[]): number | undefined { return ClassInstanceStorage.getInternal( this.recursiveCountStorage, parent, @@ -34,11 +47,11 @@ export class ClassInstanceStorage { ); } - setDepth(parent: Constructible, member: string, depth: number): void { + setDepth(parent: Constructible, member: string[], depth: number): void { ClassInstanceStorage.setInternal(this.depthStorage, parent, member, depth); } - setCount(parent: Constructible, member: string, count: number): void { + setCount(parent: Constructible, member: string[], count: number): void { ClassInstanceStorage.setInternal( this.recursiveCountStorage, parent, @@ -47,7 +60,7 @@ export class ClassInstanceStorage { ); } - resetCount(parent: Constructible, member: string): void { + resetCount(parent: Constructible, member: string[]): void { this.setCount(parent, member, 0); } @@ -61,42 +74,80 @@ export class ClassInstanceStorage { dispose(): void { this.recursiveCountStorage = new WeakMap< Constructible, - Map + ArrayKeyedMap >(); - this.depthStorage = new WeakMap>(); + this.depthStorage = new WeakMap(); } private static getInternal( - storage: WeakMap>, + storage: WeakMap, parent: Constructible, - member: string + member: string[] ): number | undefined { const parentVal = storage.get(parent); - return parentVal ? parentVal.get(member) : undefined; + return parentVal ? arrayMapGet(parentVal, member) : undefined; } private static setInternal( - storage: WeakMap>, + storage: WeakMap, parent: Constructible, - member: string, + member: string[], value: number ): void { if (!storage.has(parent)) { - storage.set(parent, new Map().set(member, value)); + storage.set(parent, arrayMapSet(new Map(), member, value)) return; } if (!this.hasInternal(storage, parent, member)) { - storage.get(parent)!.set(member, value); + arrayMapSet(storage.get(parent), member, value) } } private static hasInternal( - storage: WeakMap>, + storage: WeakMap, parent: Constructible, - member: string + member: string[] ): boolean { const parentVal = storage.get(parent); - return parentVal ? parentVal.has(member) : false; + return parentVal ? arrayMapHas(parentVal, member) : false; + } +} + +function arrayMapSet(root: ArrayKeyedMap, path: string[], value: number): ArrayKeyedMap { + let map = root; + for (const item of path) { + let nextMap = (map as PathMap).get(item) as PathMap; + if (!nextMap) { + // Create next map if none exists + nextMap = new Map(); + (map as PathMap).set(item, nextMap); + } + map = nextMap; + } + // Reached end of path. Set the data symbol to the given value + (map as DataMap).set(DATA_SYMBOL, value); + return root; +} + +function arrayMapHas(root: ArrayKeyedMap, path: string[]): boolean { + let map = root; + for (const item of path) { + const nextMap = (map as PathMap).get(item); + if (nextMap) { + map = nextMap; + } else { + return false; + } + } + return (map as DataMap).has(DATA_SYMBOL); +} + +function arrayMapGet(root: ArrayKeyedMap, path: string[]): number | undefined { + let map = root; + for (const item of path) { + map = (map as PathMap).get(item); + if (!map) return undefined; } + return (map as DataMap).get(DATA_SYMBOL); } diff --git a/packages/classes/src/lib/storages/class-metadata.storage.ts b/packages/classes/src/lib/storages/class-metadata.storage.ts index 7712a9379..bae4f6958 100644 --- a/packages/classes/src/lib/storages/class-metadata.storage.ts +++ b/packages/classes/src/lib/storages/class-metadata.storage.ts @@ -1,5 +1,6 @@ import type { Metadata, MetadataStorage } from '@automapper/types'; import type { Constructible } from '../types'; +import { isSamePath } from '@automapper/core'; /** * Internal ClassMetadataStorage @@ -17,19 +18,18 @@ export class ClassMetadataStorage implements MetadataStorage { getMetadata(model: Constructible): Array> { const metadataList = this.storage.get(model) ?? []; - let i = metadataList.length; // empty metadata - if (!i) { + if (!metadataList.length) { // try to get the metadata on the prototype of the class return model.name ? this.getMetadata(Object.getPrototypeOf(model)) : []; } const resultMetadataList: Array> = []; - while (i--) { + for (let i = 0; i < metadataList.length; i++) { const metadata = metadataList[i]; // skip existing - if (resultMetadataList.some(([metaKey]) => metaKey === metadata[0])) { + if (resultMetadataList.some(([metaKey]) => isSamePath(metaKey, metadata[0]))) { continue; } resultMetadataList.push(metadataList[i]); @@ -40,9 +40,9 @@ export class ClassMetadataStorage implements MetadataStorage { getMetadataForKey( model: Constructible, - key: string + key: string[] ): Metadata | undefined { - return this.getMetadata(model).find(([metaKey]) => metaKey === key); + return this.getMetadata(model).find(([metaKey]) => isSamePath(metaKey, key)); } addMetadata(model: Constructible, metadata: Metadata): void { @@ -56,7 +56,7 @@ export class ClassMetadataStorage implements MetadataStorage { const merged = [...protoExists, ...exists]; // if already exists, break - if (merged.some(([existKey]) => existKey === metadata[0])) { + if (merged.some(([existKey]) => isSamePath(existKey, metadata[0]))) { return; } diff --git a/packages/classes/src/lib/utils/explore-metadata.util.ts b/packages/classes/src/lib/utils/explore-metadata.util.ts index 6992b2e4b..0d89c73ba 100644 --- a/packages/classes/src/lib/utils/explore-metadata.util.ts +++ b/packages/classes/src/lib/utils/explore-metadata.util.ts @@ -25,9 +25,9 @@ export function exploreMetadata( propertyKey, { typeFn, depth, isGetterOnly }, ] of metadataList) { - metadataStorage.addMetadata(model, [propertyKey, typeFn, isGetterOnly]); + metadataStorage.addMetadata(model, [[propertyKey], typeFn, isGetterOnly]); if (depth != null) { - instanceStorage.setDepth(model, propertyKey, depth); + instanceStorage.setDepth(model, [propertyKey], depth); } } } diff --git a/packages/classes/src/lib/utils/instantiate.util.ts b/packages/classes/src/lib/utils/instantiate.util.ts index c50db36fd..4751b1c34 100644 --- a/packages/classes/src/lib/utils/instantiate.util.ts +++ b/packages/classes/src/lib/utils/instantiate.util.ts @@ -1,8 +1,10 @@ import { + get, + setMutate, isDateConstructor, isDefined, isEmpty, - isPrimitiveConstructor, + isPrimitiveConstructor } from '@automapper/core'; import type { Dictionary } from '@automapper/types'; import type { ClassInstanceStorage, ClassMetadataStorage } from '../storages'; @@ -37,10 +39,9 @@ export function instantiate>( // initialize a nestedConstructible with empty [] const nestedConstructible: unknown[] = []; - let i = metadata.length; // reversed loop - while (i--) { + for (let i = 0; i < metadata.length; i++) { // destructure const [key, meta, isGetterOnly] = metadata[i]; @@ -50,7 +51,7 @@ export function instantiate>( } // get the value at the current key - const valueAtKey = (instance as Record)[key]; + const valueAtKey = get(instance as Record, key); // call the meta fn to get the metaResult of the current key const metaResult = meta(); @@ -58,17 +59,19 @@ export function instantiate>( // if is String, Number, Boolean, Array, assign valueAtKey or undefined // null meta means this has any type or an arbitrary object, treat as primitives if (isPrimitiveConstructor(metaResult) || metaResult === null) { - (instance as Record)[key] = isDefined(valueAtKey, true) + const value = isDefined(valueAtKey, true) ? valueAtKey : undefined; + setMutate(instance as Record, key, value); continue; } // if is Date, assign a new Date value if valueAtKey is defined, otherwise, undefined if (isDateConstructor(metaResult)) { - (instance as Record)[key] = isDefined(valueAtKey) + const value = isDefined(valueAtKey) ? new Date(valueAtKey as number) : undefined; + setMutate(instance as Record, key, value); continue; } @@ -79,7 +82,7 @@ export function instantiate>( // if the value at key is an array if (Array.isArray(valueAtKey)) { // loop through each value and recursively call instantiate with each value - (instance as Record)[key] = valueAtKey.map((val) => { + const value = valueAtKey.map((val) => { const [instantiateResultItem] = instantiate( instanceStorage, metadataStorage, @@ -87,7 +90,8 @@ export function instantiate>( val ); return instantiateResultItem; - }); + }) + setMutate(instance as Record, key, value); continue; } @@ -100,14 +104,14 @@ export function instantiate>( metaResult as Constructible, valueAtKey as Dictionary ); - (instance as Record)[key] = definedInstantiateResult; + setMutate(instance as Record, key, definedInstantiateResult); continue; } // if value is null/undefined but defaultValue is not // should assign straightaway if (isDefined(defaultValue)) { - (instance as Record)[key] = valueAtKey; + setMutate(instance as Record, key, valueAtKey); continue; } @@ -117,9 +121,7 @@ export function instantiate>( // if no depth, just instantiate with new keyword without recursive if (depth === 0) { - (instance as Record)[ - key - ] = new (metaResult as Constructible)(); + setMutate(instance as Record, key, new (metaResult as Constructible)()); continue; } @@ -127,9 +129,7 @@ export function instantiate>( // reset the count then assign with new keyword if (depth === count) { instanceStorage.resetCount(model, key); - (instance as Record)[ - key - ] = new (metaResult as Constructible)(); + setMutate(instance as Record, key, new (metaResult as Constructible)()); continue; } @@ -140,7 +140,7 @@ export function instantiate>( metadataStorage, metaResult as Constructible ); - (instance as Record)[key] = instantiateResult; + setMutate(instance as Record, key, instantiateResult); } // after all, resetAllCount on the current model diff --git a/packages/classes/src/lib/utils/is-destination-path-on-source.util.ts b/packages/classes/src/lib/utils/is-destination-path-on-source.util.ts index 32baca008..1509814ae 100644 --- a/packages/classes/src/lib/utils/is-destination-path-on-source.util.ts +++ b/packages/classes/src/lib/utils/is-destination-path-on-source.util.ts @@ -1,11 +1,11 @@ export function isDestinationPathOnSource( sourceProto: Record ) { - return (sourceObj: any, sourcePath: string) => { - return !( - !sourceObj.hasOwnProperty(sourcePath) && - !sourceProto.hasOwnProperty(sourcePath) && - !Object.getPrototypeOf(sourceObj).hasOwnProperty(sourcePath) + return (sourceObj: any, sourcePath: string[]) => { + return sourcePath.length === 1 && !( + !sourceObj.hasOwnProperty(sourcePath[0]) && + !sourceProto.hasOwnProperty(sourcePath[0]) && + !Object.getPrototypeOf(sourceObj).hasOwnProperty(sourcePath[0]) ); }; } diff --git a/packages/classes/src/lib/utils/is-multipart-source-paths-in-source.util.ts b/packages/classes/src/lib/utils/is-multipart-source-paths-in-source.util.ts index 7ca5c6b7c..2f5db9465 100644 --- a/packages/classes/src/lib/utils/is-multipart-source-paths-in-source.util.ts +++ b/packages/classes/src/lib/utils/is-multipart-source-paths-in-source.util.ts @@ -1,14 +1,14 @@ import { isClass } from './is-class.util'; export function isMultipartSourcePathsInSource( - dottedSourcePaths: string[], + sourcePaths: string[], sourceInstance: Record ) { return !( - dottedSourcePaths.length > 1 && - (!sourceInstance.hasOwnProperty(dottedSourcePaths[0]) || - (sourceInstance[dottedSourcePaths[0]] && + sourcePaths.length > 1 && + (!sourceInstance.hasOwnProperty(sourcePaths[0]) || + (sourceInstance[sourcePaths[0]] && // eslint-disable-next-line @typescript-eslint/ban-types - isClass((sourceInstance[dottedSourcePaths[0]] as unknown) as Function))) + isClass((sourceInstance[sourcePaths[0]]) as Function))) ); } diff --git a/packages/classes/src/lib/utils/specs/instantiate.util.spec.ts b/packages/classes/src/lib/utils/specs/instantiate.util.spec.ts index c8db6d4ef..d5aacd3e4 100644 --- a/packages/classes/src/lib/utils/specs/instantiate.util.spec.ts +++ b/packages/classes/src/lib/utils/specs/instantiate.util.spec.ts @@ -60,7 +60,7 @@ describe('instantiate', () => { const result = parameterizedInstantiate(); expect(result).toEqual([ { ...fooInstance, foo: undefined }, - [['bar', Bar]], + [[['bar'], Bar]], ]); }); @@ -81,7 +81,7 @@ describe('instantiate', () => { foo: undefined, bar: { ...fooInstance.bar, bar: undefined }, }, - [['bar', Bar]], + [[['bar'], Bar]], ]); }); }); @@ -112,14 +112,14 @@ describe('instantiate', () => { mockWithoutDepth(); const result = parameterizedInstantiate(); - expect(result).toEqual([defaultFoo, [['bar', Bar]]]); + expect(result).toEqual([defaultFoo, [[['bar'], Bar]]]); }); it('should return proper instance with depth', () => { mockWithDepth(); const result = parameterizedInstantiate(); - expect(result).toEqual([defaultFoo, [['bar', Bar]]]); + expect(result).toEqual([defaultFoo, [[['bar'], Bar]]]); }); }); @@ -128,7 +128,7 @@ describe('instantiate', () => { function mockEmpty() { when(mockedInstanceStorage.getDepthAndCount) - .calledWith(fooMatcher as any, 'bar') + .calledWith(fooMatcher as any, ['bar']) .mockReturnValueOnce([0, 0]); when(mockedMetadataStorage.getMetadata) .calledWith(fooMatcher as any) @@ -137,37 +137,37 @@ describe('instantiate', () => { function mockWithoutDepth() { when(mockedInstanceStorage.getDepthAndCount) - .calledWith(fooMatcher as any, 'bar') + .calledWith(fooMatcher as any, ['bar']) .mockReturnValueOnce([0, 0]); when(mockedMetadataStorage.getMetadata) .calledWith(fooMatcher as any) .mockReturnValueOnce([ - ['foo', () => String], - ['bar', () => Bar], + [['foo'], () => String], + [['bar'], () => Bar], ]); when(mockedMetadataStorage.getMetadata) .calledWith(barMatcher as any) .mockReturnValueOnce([ - ['bar', () => String], - ['date', () => Date], + [['bar'], () => String], + [['date'], () => Date], ]); } function mockWithDepth() { when(mockedInstanceStorage.getDepthAndCount) - .calledWith(fooMatcher as any, 'bar') + .calledWith(fooMatcher as any, ['bar']) .mockReturnValueOnce([1, 0]); when(mockedMetadataStorage.getMetadata) .calledWith(fooMatcher as any) .mockReturnValueOnce([ - ['foo', () => String], - ['bar', () => Bar], + [['foo'], () => String], + [['bar'], () => Bar], ]); when(mockedMetadataStorage.getMetadata) .calledWith(barMatcher as any) .mockReturnValueOnce([ - ['bar', () => String], - ['date', () => Date], + [['bar'], () => String], + [['date'], () => Date], ]); } }); diff --git a/packages/core/src/lib/create-mapper/create-map-for-member.util.ts b/packages/core/src/lib/create-mapper/create-map-for-member.util.ts index ace896166..c5ecce4f6 100644 --- a/packages/core/src/lib/create-mapper/create-map-for-member.util.ts +++ b/packages/core/src/lib/create-mapper/create-map-for-member.util.ts @@ -15,6 +15,7 @@ import { TransformationType, } from '@automapper/types'; import { getMemberPath } from './get-member-path.util'; +import { isSamePath } from '../utils'; /** * @@ -51,7 +52,7 @@ export function createMapForMember< } // initialize sourcePath - let sourcePath = ''; + let sourcePath: string[] = []; // if the transformation is MapWith, we have information on the source value selector if ( @@ -69,7 +70,7 @@ export function createMapForMember< // check existProp on mapping const existProp = mapping[MappingClassId.properties].find( - ([propName]) => propName === memberPath + ([propName]) => isSamePath(propName, memberPath) ); // if exists, overrides diff --git a/packages/core/src/lib/create-mapper/create-mapper.ts b/packages/core/src/lib/create-mapper/create-mapper.ts index 26d7b5d82..d7413cc5d 100644 --- a/packages/core/src/lib/create-mapper/create-mapper.ts +++ b/packages/core/src/lib/create-mapper/create-mapper.ts @@ -3,8 +3,8 @@ import type { CreateMapperOptions, MapArrayOptions, MapOptions, - Mapper, - MappingProfile, + Mapper, Mapping, + MappingProfile } from '@automapper/types'; import { mapArray, mapMutate, mapReturn } from '../map'; import { createMapFluentFunction } from './create-map-fluent-function.util'; @@ -65,7 +65,7 @@ export function createMapper({ : []; // get mapping between Source and Destination - const mapping = this.getMapping(source, destination); + const mapping: Mapping = this.getMapping(source, destination); // check mutate or return diff --git a/packages/core/src/lib/create-mapper/get-member-path.util.ts b/packages/core/src/lib/create-mapper/get-member-path.util.ts index 58410f853..255a5feaa 100644 --- a/packages/core/src/lib/create-mapper/get-member-path.util.ts +++ b/packages/core/src/lib/create-mapper/get-member-path.util.ts @@ -33,9 +33,9 @@ export function getMembers(fnSelector: Selector string * getMemberPath(s => s) === '' * ``` */ -export function getMemberPath(fn: Selector): string { +export function getMemberPath(fn: Selector): string[] { const members = getMembers(fn); - return members ? members.join('.') : ''; + return members ? members : []; } /** diff --git a/packages/core/src/lib/create-mapper/specs/get-member-path.spec.ts b/packages/core/src/lib/create-mapper/specs/get-member-path.spec.ts index 1c409717b..7f3f01d04 100644 --- a/packages/core/src/lib/create-mapper/specs/get-member-path.spec.ts +++ b/packages/core/src/lib/create-mapper/specs/get-member-path.spec.ts @@ -109,63 +109,78 @@ describe('getMemberPath', () => { const namedOne = (s: Foo) => s.foo; path = getMemberPath(namedOne); - expect(path).toEqual('foo'); + expect(path).toEqual(['foo']); const namedOne2 = (s: Foo) => s['foo']; path = getMemberPath(namedOne2); - expect(path).toEqual('foo'); + expect(path).toEqual(['foo']); path = getMemberPath((s: Foo) => s['snake_case']); - expect(path).toEqual('snake_case'); + expect(path).toEqual(['snake_case']); path = getMemberPath((s: Foo) => s.snake_case); - expect(path).toEqual('snake_case'); + expect(path).toEqual(['snake_case']); path = getMemberPath((s: Foo) => s['odd-property']); - expect(path).toEqual('odd-property'); + expect(path).toEqual(['odd-property']); path = getMemberPath((s: Foo) => s.snake_case); - expect(path).toEqual('snake_case'); + expect(path).toEqual(['snake_case']); path = getMemberPath((s: Foo) => s['even[odd].prop']); - expect(path).toEqual('even[odd].prop'); + expect(path).toEqual(['even[odd].prop']); path = getMemberPath((s: Foo) => s.á); - expect(path).toEqual('á'); + expect(path).toEqual(['á']); path = getMemberPath((s: Foo) => s['with_sṕéçiâl_chàrs']); - expect(path).toEqual('with_sṕéçiâl_chàrs'); + expect(path).toEqual(['with_sṕéçiâl_chàrs']); path = getMemberPath((s: Foo) => s[' foo ']); - expect(path).toEqual(' foo '); + expect(path).toEqual([' foo ']); path = getMemberPath((s: Foo) => s['odd' + '-' + 'property']); - expect(path).toEqual('odd-property'); + expect(path).toEqual(['odd-property']); path = getMemberPath((s: Foo) => s[`${'odd-property'}`]); - expect(path).toEqual('odd-property'); + expect(path).toEqual(['odd-property']); + + path = getMemberPath((s: Foo) => s['.startDot']); + expect(path).toEqual(['.startDot']); + + path = getMemberPath((s: Foo) => s['mid.Dot']); + expect(path).toEqual(['mid.Dot']); + + path = getMemberPath((s: Foo) => s['endDot.']); + expect(path).toEqual(['endDot.']); }); it('should return properly for nested path for ES6 arrow syntax', () => { let path: ReturnType; path = getMemberPath((s: Foo) => s.bar.baz); - expect(path).toEqual('bar.baz'); + expect(path).toEqual(['bar','baz']); path = getMemberPath((s: Foo) => s['bar']['baz']); - expect(path).toEqual('bar.baz'); + expect(path).toEqual(['bar','baz']); path = getMemberPath((s: Foo) => s.bar.baz['']); - expect(path).toEqual('bar.baz.'); + expect(path).toEqual(['bar','baz', '']); path = getMemberPath((s: Foo) => s[' foo '][' bar '].baz); - expect(path).toEqual(' foo . bar .baz'); + expect(path).toEqual([' foo ',' bar ','baz']); path = getMemberPath((s: Foo) => s['odd' + '-' + 'property']['odd' + '+' + 'property']); - expect(path).toEqual('odd-property.odd+property'); + expect(path).toEqual(['odd-property','odd+property']); path = getMemberPath((s: Foo) => s[`${'odd-property'}`][`${'odd+property'}`].baz); - expect(path).toEqual('odd-property.odd+property.baz'); + expect(path).toEqual(['odd-property','odd+property','baz']); + + path = getMemberPath((s: Foo) => s.bar.baz['']['']['']); + expect(path).toEqual(['bar', 'baz', '', '', '']); + + path = getMemberPath((s: Foo) => s['mid.Dot']['.startDot']); + expect(path).toEqual(['mid.Dot', '.startDot']); }); it('should return properly for ES5 function syntax', () => { @@ -174,27 +189,27 @@ describe('getMemberPath', () => { path = getMemberPath(function (s: Foo) { return s.foo; }); - expect(path).toEqual('foo'); + expect(path).toEqual(['foo']); path = getMemberPath(function namedOne(s: Foo) { return s.foo; }); - expect(path).toEqual('foo'); + expect(path).toEqual(['foo']); path = getMemberPath(function (s: Foo) { return s['foo']; }); - expect(path).toEqual('foo'); + expect(path).toEqual(['foo']); path = getMemberPath(function (s: Foo) { return s[' odd' + '-' + 'property ']; }); - expect(path).toEqual(' odd-property '); + expect(path).toEqual([' odd-property ']); path = getMemberPath(function (s: Foo) { return s[`${'odd-property'}`] }); - expect(path).toEqual('odd-property'); + expect(path).toEqual(['odd-property']); }); it('should return properly for nested path for ES5 function syntax', () => { @@ -203,45 +218,63 @@ describe('getMemberPath', () => { path = getMemberPath(function (s: Foo) { return s.bar.baz; }); - expect(path).toEqual('bar.baz'); + expect(path).toEqual(['bar','baz']); path = getMemberPath(function (s: Foo) { return s['bar']['baz']; }); - expect(path).toEqual('bar.baz'); + expect(path).toEqual(['bar','baz']); }); it('should return properly for properties with return keyword', () => { let path: ReturnType; path = getMemberPath((s: Foo) => s.returnFoo); - expect(path).toEqual('returnFoo'); + expect(path).toEqual(['returnFoo']); path = getMemberPath((s: Foo) => s['returnFoo']); - expect(path).toEqual('returnFoo'); + expect(path).toEqual(['returnFoo']); path = getMemberPath((s: Foo) => { return s.returnFoo; }); - expect(path).toEqual('returnFoo'); + expect(path).toEqual(['returnFoo']); path = getMemberPath((s: Foo) => { return s['returnFoo']; }); - expect(path).toEqual('returnFoo'); + expect(path).toEqual(['returnFoo']); path = getMemberPath((s: Foo) => { return s.bar['baz']; }); - expect(path).toEqual('bar.baz'); + expect(path).toEqual(['bar','baz']); path = getMemberPath(function (s: Foo) { return s.returnFoo; }); - expect(path).toEqual('returnFoo'); + expect(path).toEqual(['returnFoo']); path = getMemberPath(function (s: Foo) { return s['returnFoo']; }); - expect(path).toEqual('returnFoo'); + expect(path).toEqual(['returnFoo']); + + path = getMemberPath((s: Foo) => s['.startDot']); + expect(path).toEqual(['.startDot']); + + path = getMemberPath((s: Foo) => { + return s['mid.Dot']; + }); + expect(path).toEqual(['mid.Dot']); + + path = getMemberPath(function (s: Foo) { + return s['mid.Dot']['.startDot']; + }); + expect(path).toEqual(['mid.Dot', '.startDot']); + + path = getMemberPath(function(s: Foo) { + return s['endDot.']; + }); + expect(path).toEqual(['endDot.']); }); }); diff --git a/packages/core/src/lib/initialize-utils/create-initial-mapping.util.ts b/packages/core/src/lib/initialize-utils/create-initial-mapping.util.ts index 4587dae3a..af7ccf09a 100644 --- a/packages/core/src/lib/initialize-utils/create-initial-mapping.util.ts +++ b/packages/core/src/lib/initialize-utils/create-initial-mapping.util.ts @@ -23,9 +23,9 @@ function defaultIsMultipartSourcePathsInSource( function defaultIsDestinationPathOnSource( sourceObj: Record, - sourcePath: string + sourcePath: string[] ): boolean { - return sourceObj.hasOwnProperty(sourcePath); + return sourcePath.length === 1 && sourceObj.hasOwnProperty(sourcePath[0]); } interface CreateInitialMappingOptions { @@ -35,9 +35,9 @@ interface CreateInitialMappingOptions { ) => boolean; isDestinationPathOnSource?: ( sourceObj: unknown, - sourcePath: string + sourcePath: string[] ) => boolean; - isMetadataNullAtKey?: (key: string) => boolean; + isMetadataNullAtKey?: (key: string[]) => boolean; prePropertiesLoop?: (mapping: Mapping) => void; } @@ -72,8 +72,7 @@ export function createInitialMapping( const destinationPaths = getPathRecursive(destinationObj) || []; const namingConventions = mapping[MappingClassId.namingConventions]; - let i = destinationPaths.length; - while (i--) { + for (let i = 0; i < destinationPaths.length; i++) { const destinationPath = destinationPaths[i]; const destinationNestedMetadataAtPath = getNestedMetaKeyAtDestinationPath( destinationNestedMetadataMap, @@ -87,8 +86,7 @@ export function createInitialMapping( namingConventions ); - const dottedSourcePaths = sourcePath.split('.'); - if (!isMultipartSourcePathsInSource(dottedSourcePaths, sourceObj)) { + if (!isMultipartSourcePathsInSource(sourcePath, sourceObj)) { continue; } @@ -112,7 +110,7 @@ export function createInitialMapping( [ [destinationPath], [ - mapInitialize(...sourcePaths!), + mapInitialize(sourcePaths!), isMetadataNullAtKey(destinationPath), ], ], diff --git a/packages/core/src/lib/initialize-utils/extend-mappings.util.ts b/packages/core/src/lib/initialize-utils/extend-mappings.util.ts index d6591396b..534aa7dda 100644 --- a/packages/core/src/lib/initialize-utils/extend-mappings.util.ts +++ b/packages/core/src/lib/initialize-utils/extend-mappings.util.ts @@ -1,13 +1,14 @@ import type { Mapping } from '@automapper/types'; import { MappingClassId, MappingPropertiesClassId } from '@automapper/types'; +import { isSamePath } from '../utils'; -export function extendMappings(bases: any[], mapping: Mapping) { +export function extendMappings(bases: Mapping[], mapping: Mapping) { for (const mappingToExtend of bases) { const propsToExtend = mappingToExtend[MappingClassId.properties]; for (let i = 0, len = propsToExtend.length; i < len; i++) { const [propToExtendKey, propToExtendMappingProp] = propsToExtend[i]; const existProp = mapping[MappingClassId.properties].find( - ([pKey]) => pKey === propToExtendKey + ([pKey]) => isSamePath(pKey, propToExtendKey) ); if (existProp) { existProp[MappingPropertiesClassId.path] = propToExtendKey; diff --git a/packages/core/src/lib/initialize-utils/get-flattening-source-paths.util.ts b/packages/core/src/lib/initialize-utils/get-flattening-source-paths.util.ts index b4deb2443..831317b64 100644 --- a/packages/core/src/lib/initialize-utils/get-flattening-source-paths.util.ts +++ b/packages/core/src/lib/initialize-utils/get-flattening-source-paths.util.ts @@ -2,14 +2,14 @@ import type { NamingConvention } from '@automapper/types'; export function getFlatteningSourcePaths( src: Record, - srcPath: string, + srcPath: string[], namingConventions: [NamingConvention, NamingConvention] -) { +): string[] | undefined { const [sourceNamingConvention] = namingConventions; - const splitSourcePaths = srcPath - .split(sourceNamingConvention.splittingExpression) - .filter(Boolean) - .filter((p) => p !== '.'); + const splitSourcePaths: string[] = [].concat(...srcPath.map(s => + s.split(sourceNamingConvention.splittingExpression) + .filter(Boolean)) + ); const [first, ...paths] = splitSourcePaths.slice( 0, @@ -23,7 +23,7 @@ export function getFlatteningSourcePaths( for (let i = 0, len = paths.length; i < len; i++) { trueFirstPartOfSource = sourceNamingConvention.transformPropertyName([ trueFirstPartOfSource, - paths[i], + paths[i] ]); if (src.hasOwnProperty(trueFirstPartOfSource)) { stopIndex = i + 1; @@ -37,13 +37,10 @@ export function getFlatteningSourcePaths( return; } - return [ - [trueFirstPartOfSource] - .concat( - sourceNamingConvention.transformPropertyName( - splitSourcePaths.slice(stopIndex + 1, splitSourcePaths.length + 1) - ) - ) - .join('.'), - ]; + return [].concat( + trueFirstPartOfSource, + sourceNamingConvention.transformPropertyName( + splitSourcePaths.slice(stopIndex + 1, splitSourcePaths.length + 1) + ) + ); } diff --git a/packages/core/src/lib/initialize-utils/get-nested-metakey-at-destination-path.util.ts b/packages/core/src/lib/initialize-utils/get-nested-metakey-at-destination-path.util.ts index 554d1fa5b..c379e3e11 100644 --- a/packages/core/src/lib/initialize-utils/get-nested-metakey-at-destination-path.util.ts +++ b/packages/core/src/lib/initialize-utils/get-nested-metakey-at-destination-path.util.ts @@ -1,10 +1,10 @@ import type { NamingConvention } from '@automapper/types'; -import { isDefined, isEmpty } from '../utils'; +import { isDefined, isEmpty, isSamePath } from '../utils'; export function getNestedMetaKeyAtDestinationPath( destinationNestedMeta: any[], sourceNestedMeta: any[], - destinationPath: string, + destinationPath: string[], namingConventions: [NamingConvention, NamingConvention] | undefined ) { let destinationNestedMetaKeyAtPath: [unknown, unknown]; @@ -12,7 +12,7 @@ export function getNestedMetaKeyAtDestinationPath( if (!isEmpty(sourceNestedMeta) && !isEmpty(destinationNestedMeta)) { let sourceNestedMetaAtPath; const destinationNestedMetaAtPath = destinationNestedMeta.find( - ([dnmPath]) => dnmPath === destinationPath + ([dnmPath]: [string[]]) => isSamePath(dnmPath, destinationPath) )?.[1]; if (isDefined(namingConventions)) { @@ -21,16 +21,15 @@ export function getNestedMetaKeyAtDestinationPath( destinationNamingConvention, ] = namingConventions!; sourceNestedMetaAtPath = sourceNestedMeta.find( - ([snmPath]) => - destinationNamingConvention.transformPropertyName( - snmPath - .split(sourceNamingConvention.splittingExpression) - .filter(Boolean) - ) === destinationPath + ([snmPath]: [string[]]) => + isSamePath(snmPath.map(s => + destinationNamingConvention.transformPropertyName( + s.split(sourceNamingConvention.splittingExpression).filter(Boolean)) + ), destinationPath) )?.[1]; } else { sourceNestedMetaAtPath = sourceNestedMeta.find( - ([snmPath]) => snmPath === destinationPath + ([snmPath]: [string[]]) => isSamePath(snmPath, destinationPath) )?.[1]; } diff --git a/packages/core/src/lib/initialize-utils/get-path-recursive.util.ts b/packages/core/src/lib/initialize-utils/get-path-recursive.util.ts index e49542e93..84ffb970f 100644 --- a/packages/core/src/lib/initialize-utils/get-path-recursive.util.ts +++ b/packages/core/src/lib/initialize-utils/get-path-recursive.util.ts @@ -1,3 +1,5 @@ +import { uniquePaths } from '../utils'; + /** * Loop through an object and recursively get paths of each property * @@ -7,19 +9,20 @@ */ export function getPathRecursive( node: Record, - prefix = '', - prev: string[] = [] -): string[] | undefined { + prefix: string[] = [], + prev: string[][] = [] +): string[][] | undefined { if (node == null) { return; } const result = prev; + let hadChildPaths = false; const keys = Object.getOwnPropertyNames(node); for (let i = 0, len = keys.length; i < len; i++) { const key = keys[i]; - const path = prefix + key; + const path: string[] = [].concat(...prefix, key); result.push(path); const child = node[key]; @@ -27,16 +30,17 @@ export function getPathRecursive( const queue = Array.isArray(child) ? child : [child]; for (const childNode of queue) { - const childPaths = getPathRecursive(childNode, path + '.'); + const childPaths = getPathRecursive(childNode, path); if (childPaths) { - for (const childPath of childPaths) { - if (result.includes(childPath)) continue; - result.push(childPath); - } + hadChildPaths = true; + result.push(...childPaths); } } } } + if (hadChildPaths) { + return uniquePaths(result); + } return result; } diff --git a/packages/core/src/lib/initialize-utils/get-source-property-path.util.ts b/packages/core/src/lib/initialize-utils/get-source-property-path.util.ts index a4a28c7ec..da904e765 100644 --- a/packages/core/src/lib/initialize-utils/get-source-property-path.util.ts +++ b/packages/core/src/lib/initialize-utils/get-source-property-path.util.ts @@ -2,9 +2,9 @@ import type { NamingConvention } from '@automapper/types'; import { isDefined } from '../utils'; export function getSourcePropertyPath( - path: string, + path: string[], namingConventions?: Readonly<[NamingConvention, NamingConvention]> -): string { +): string[] { if (!isDefined(namingConventions)) { return path; } @@ -14,17 +14,9 @@ export function getSourcePropertyPath( destinationNamingConvention, ] = namingConventions!; - const splitPath = path.split('.'); - if (splitPath.length > 1) { - return splitPath - .map((key) => getSourcePropertyPath(key, namingConventions)) - .join('.'); - } - - const keyParts = path - .split(destinationNamingConvention.splittingExpression) - .filter(Boolean); + const keyParts = path.map(s => s.split(destinationNamingConvention.splittingExpression) + .filter(Boolean)).filter(p => p.length > 0); return !keyParts.length ? path - : sourceNamingConvention.transformPropertyName(keyParts); + : keyParts.map(p => sourceNamingConvention.transformPropertyName(p)); } diff --git a/packages/core/src/lib/initialize-utils/map-initialize.ts b/packages/core/src/lib/initialize-utils/map-initialize.ts index 752333edc..00a09f45c 100644 --- a/packages/core/src/lib/initialize-utils/map-initialize.ts +++ b/packages/core/src/lib/initialize-utils/map-initialize.ts @@ -1,20 +1,18 @@ import type { Dictionary, MapInitializeReturn, - SelectorReturn, + SelectorReturn } from '@automapper/types'; import { TransformationType } from '@automapper/types'; import { get } from '../utils'; -export function mapInitialize< - TSource extends Dictionary = any, +export function mapInitialize = any, TDestination extends Dictionary = any, - TSelectorReturn = SelectorReturn ->( - ...sourcePaths: string[] + TSelectorReturn = SelectorReturn>( + sourcePath: string[] ): MapInitializeReturn { return [ TransformationType.MapInitialize, - (source) => get(source, ...sourcePaths) as TSelectorReturn, + (source) => get(source, sourcePath) as TSelectorReturn ]; } diff --git a/packages/core/src/lib/initialize-utils/specs/get-path-recursive.spec.ts b/packages/core/src/lib/initialize-utils/specs/get-path-recursive.spec.ts index 4d28d0e37..e05954617 100644 --- a/packages/core/src/lib/initialize-utils/specs/get-path-recursive.spec.ts +++ b/packages/core/src/lib/initialize-utils/specs/get-path-recursive.spec.ts @@ -19,16 +19,23 @@ describe('getPathRecursive', () => { }, }, ], + ['mid.Dot']: { + ['.startDot']: undefined, + ['endDot.']: undefined, + }, }; const resultPaths = [ - 'foo', - 'bar', - 'bar.baz', - 'baz', - 'barBaz', - 'barBaz.fooBar', - 'barBaz.fooBar.barFoo', + ['foo'], + ['bar'], + ['bar','baz'], + ['baz'], + ['barBaz'], + ['barBaz','fooBar'], + ['barBaz','fooBar','barFoo'], + ['mid.Dot'], + ['mid.Dot','.startDot'], + ['mid.Dot','endDot.'], ]; it('should work', () => { diff --git a/packages/core/src/lib/initialize-utils/specs/get-source-property-path.spec.ts b/packages/core/src/lib/initialize-utils/specs/get-source-property-path.spec.ts index 4bb0647cd..4adb1c3dc 100644 --- a/packages/core/src/lib/initialize-utils/specs/get-source-property-path.spec.ts +++ b/packages/core/src/lib/initialize-utils/specs/get-source-property-path.spec.ts @@ -11,21 +11,21 @@ describe('getSourcePropertyPath', () => { ] as const; it('should return path as-is if namingConventions are not provided', () => { - const sourcePath = getSourcePropertyPath('foo.bar'); - expect(sourcePath).toEqual('foo.bar'); + const sourcePath = getSourcePropertyPath(['foo', 'bar']); + expect(sourcePath).toEqual(['foo','bar']); }); it('should return path with namingConventions', () => { let sourcePath = getSourcePropertyPath( - 'Foo.Bar', + ['Foo','Bar'], camelPascalNamingConventions ); - expect(sourcePath).toEqual('foo.bar'); + expect(sourcePath).toEqual(['foo','bar']); sourcePath = getSourcePropertyPath( - 'FooBarBaz', + ['FooBarBaz'], camelPascalNamingConventions ); - expect(sourcePath).toEqual('fooBarBaz'); + expect(sourcePath).toEqual(['fooBarBaz']); }); }); diff --git a/packages/core/src/lib/initialize-utils/specs/map-initialize.spec.ts b/packages/core/src/lib/initialize-utils/specs/map-initialize.spec.ts index eca7edaab..53c5fb3b3 100644 --- a/packages/core/src/lib/initialize-utils/specs/map-initialize.spec.ts +++ b/packages/core/src/lib/initialize-utils/specs/map-initialize.spec.ts @@ -9,7 +9,7 @@ describe('MapInitializeFunction', () => { }; it('should return correctly', () => { - const mapInitFn = mapInitialize(''); + const mapInitFn = mapInitialize(['']); expect(mapInitFn).toBeTruthy(); expect(mapInitFn[MapFnClassId.type]).toEqual( TransformationType.MapInitialize @@ -18,13 +18,13 @@ describe('MapInitializeFunction', () => { }); it('should map correctly', () => { - const mapInitFn = mapInitialize('foo.bar'); + const mapInitFn = mapInitialize(['foo', 'bar']); const result = mapInitFn[MapFnClassId.fn](source); expect(result).toEqual(source.foo.bar); }); it('should map to undefined for default val', () => { - const mapInitFn = mapInitialize('foo.baz'); + const mapInitFn = mapInitialize(['foo', 'baz']); const result = mapInitFn[MapFnClassId.fn](source); expect(result).toEqual(undefined); }); diff --git a/packages/core/src/lib/map/map.ts b/packages/core/src/lib/map/map.ts index 7cc79d2fc..177efce5c 100644 --- a/packages/core/src/lib/map/map.ts +++ b/packages/core/src/lib/map/map.ts @@ -17,8 +17,7 @@ import type { MemberMapReturn, } from '@automapper/types'; import { MapFnClassId, TransformationType } from '@automapper/types'; -import { isEmpty } from '../utils'; -import { set, setMutate } from './set.util'; +import { isEmpty, set, setMutate } from '../utils'; /** * Instruction on how to map a particular member on the destination @@ -34,7 +33,7 @@ function mapMember = any>( transformationMapFn: MemberMapReturn, sourceObj: TSource, destination: unknown, - destinationMemberPath: string, + destinationMemberPath: string[], extraArguments: Record | undefined, mapper: Mapper ) { @@ -92,11 +91,11 @@ function assertUnmappedProperties< TDestination extends Dictionary = any >( destination: TDestination, - configuredKeys: string[], + configuredKeys: string[][], errorHandler: ErrorHandler ) { const unmappedKeys = Object.keys(destination).filter( - (k) => !configuredKeys.includes(k) + (k) => !configuredKeys.some(ck => ck[0] === k) ); if (unmappedKeys.length) { errorHandler.handle(` @@ -128,7 +127,7 @@ export function mapReturn< isMapArray = false ): TDestination { const setMemberReturn = ( - destinationMemberPath: string, + destinationMemberPath: string[], destination?: TDestination ) => (value: unknown) => { destination = set(destination!, destinationMemberPath, value); @@ -164,7 +163,7 @@ export function mapMutate< errorHandler: ErrorHandler, destinationObj: TDestination ): void { - const setMemberMutate = (destinationMember: string) => (value: unknown) => { + const setMemberMutate = (destinationMember: string[]) => (value: unknown) => { setMutate(destinationObj, destinationMember, value); }; map(sourceObj, mapping, options, mapper, errorHandler, setMemberMutate); @@ -190,7 +189,7 @@ function map< mapper: Mapper, errorHandler: ErrorHandler, setMemberFn: ( - destinationMemberPath: string, + destinationMemberPath: string[], destination?: TDestination ) => (value: unknown) => void, isMapArray = false @@ -203,7 +202,7 @@ function map< ] = mapping; // initialize an array of keys that have already been configured - const configuredKeys: string[] = []; + const configuredKeys: string[][] = []; // deconstruct MapOptions const { @@ -222,8 +221,7 @@ function map< } // map - let i = propsToMap.length; - while (i--) { + for (let i = 0; i < propsToMap.length; i++) { // Destructure a props on Mapping which is [propertyKey, MappingProperty, nested?] const [ destinationMemberPath, diff --git a/packages/core/src/lib/map/specs/set.spec.ts b/packages/core/src/lib/map/specs/set.spec.ts deleted file mode 100644 index 0ddceb7af..000000000 --- a/packages/core/src/lib/map/specs/set.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { set } from '../set.util'; - -describe('set', () => { - it('should set nested property', () => { - const result = set({ foo: { bar: 'foo' } }, 'foo.bar', 'baz'); - expect(result).toEqual({ foo: { bar: 'baz' } }); - }); - - it('should set obj', () => { - const result = set({ foo: { bar: 'foo' } }, 'foo', { baz: 'foo' }); - expect(result).toEqual({ foo: { baz: 'foo' } }); - }); - - it('should add property to obj at unknown path', () => { - let result = set({ foo: { bar: 'foo' } }, 'bar', 'foo'); - expect(result).toEqual({ foo: { bar: 'foo' }, bar: 'foo' }); - - result = set({ foo: { bar: 'foo' } }, 'foo.baz', 'baz'); - expect(result).toEqual({ foo: { bar: 'foo', baz: 'baz' } }); - }); - - it('should add property to obj for empty and trailing dot paths', () => { - let result = set({}, '', 'foo'); - expect(result).toEqual({ '': 'foo' }); - - result = set({}, 'foo.', 'bar'); - expect(result).toEqual({ foo: { '': 'bar' } }); - - result = set({ foo: {} }, 'foo.', 'bar'); - expect(result).toEqual({ foo: { '': 'bar' } }); - }); -}); - -describe('setMutate', () => { - let obj: Record; - - beforeEach(() => { - obj = { foo: { bar: 'foo' } }; - }); - - it('should set nested property', () => { - set(obj, 'foo.bar', 'baz'); - expect(obj).toEqual({ foo: { bar: 'baz' } }); - }); - - it('should set obj', () => { - set(obj, 'foo', { baz: 'foo' }); - expect(obj).toEqual({ foo: { baz: 'foo' } }); - }); - - it('should add property to obj at unknown path', () => { - set(obj, 'bar', 'foo'); - expect(obj).toEqual({ foo: { bar: 'foo' }, bar: 'foo' }); - - set(obj, 'foo.baz', 'baz'); - expect(obj).toEqual({ foo: { bar: 'foo', baz: 'baz' }, bar: 'foo' }); - }); -}); diff --git a/packages/core/src/lib/member-map-functions/condition.ts b/packages/core/src/lib/member-map-functions/condition.ts index 49c13c561..fc9583b19 100644 --- a/packages/core/src/lib/member-map-functions/condition.ts +++ b/packages/core/src/lib/member-map-functions/condition.ts @@ -17,9 +17,9 @@ export function condition< ): ConditionReturn { return [ TransformationType.Condition, - (source, ...sourceMemberPaths) => { + (source, sourceMemberPaths) => { if (predicate(source)) { - return get(source, ...sourceMemberPaths) as TSelectorReturn; + return get(source, sourceMemberPaths) as TSelectorReturn; } return defaultValue as TSelectorReturn; diff --git a/packages/core/src/lib/member-map-functions/null-substitution.ts b/packages/core/src/lib/member-map-functions/null-substitution.ts index 939575003..eade3f935 100644 --- a/packages/core/src/lib/member-map-functions/null-substitution.ts +++ b/packages/core/src/lib/member-map-functions/null-substitution.ts @@ -15,8 +15,8 @@ export function nullSubstitution< ): NullSubstitutionReturn { return [ TransformationType.NullSubstitution, - (source, ...sourceMemberPaths) => { - const sourceValue = get(source, ...sourceMemberPaths) as TSelectorReturn; + (source, sourceMemberPath) => { + const sourceValue = get(source, sourceMemberPath) as TSelectorReturn; return sourceValue === null ? substitution : sourceValue; }, ]; diff --git a/packages/core/src/lib/member-map-functions/specs/condition.spec.ts b/packages/core/src/lib/member-map-functions/specs/condition.spec.ts index a1c2a12ad..d38277b98 100644 --- a/packages/core/src/lib/member-map-functions/specs/condition.spec.ts +++ b/packages/core/src/lib/member-map-functions/specs/condition.spec.ts @@ -17,25 +17,25 @@ describe('ConditionFunction', () => { it('should map to source.truthy when evaluated to true', () => { const conditionFn = condition(() => true); - const result = conditionFn[MapFnClassId.fn](source, 'toMap'); + const result = conditionFn[MapFnClassId.fn](source, ['toMap']); expect(result).toEqual(source.toMap); }); it('should map to source.truthy when evaluated to true regardless of defaultValue', () => { const conditionFn = condition(() => true, 'defaultValue'); - const result = conditionFn[MapFnClassId.fn](source, 'toMap'); + const result = conditionFn[MapFnClassId.fn](source, ['toMap']); expect(result).toEqual(source.toMap); }); it('should map to undefined when evaluated to false', () => { const conditionFn = condition(() => false); - const result = conditionFn[MapFnClassId.fn](source, 'toMap'); + const result = conditionFn[MapFnClassId.fn](source, ['toMap']); expect(result).toEqual(undefined); }); it('should map to defaultValue when evaluated to false and defaultValue is provided', () => { const conditionFn = condition(() => false, 'defaultValue'); - const result = conditionFn[MapFnClassId.fn](source, 'toMap'); + const result = conditionFn[MapFnClassId.fn](source, ['toMap']); expect(result).toEqual('defaultValue'); }); }); diff --git a/packages/core/src/lib/member-map-functions/specs/null-substitution.spec.ts b/packages/core/src/lib/member-map-functions/specs/null-substitution.spec.ts index 6ab79c5a7..ee5912d6c 100644 --- a/packages/core/src/lib/member-map-functions/specs/null-substitution.spec.ts +++ b/packages/core/src/lib/member-map-functions/specs/null-substitution.spec.ts @@ -13,14 +13,14 @@ describe('NullSubstitutionFunction', () => { it('should return source if source is not null', () => { const nullSubFn = nullSubstitution('subbed'); - const result = nullSubFn[MapFnClassId.fn]({ foo: 'bar' }, 'foo'); + const result = nullSubFn[MapFnClassId.fn]({ foo: 'bar' }, ['foo']); expect(result).toEqual('bar'); expect(result).not.toEqual('subbed'); }); it('should return subbed if source is null', () => { const nullSubFn = nullSubstitution('subbed'); - const result = nullSubFn[MapFnClassId.fn]({ foo: null }, 'foo'); + const result = nullSubFn[MapFnClassId.fn]({ foo: null }, ['foo']); expect(result).toEqual('subbed'); expect(result).not.toEqual(null); }); diff --git a/packages/core/src/lib/utils/get.util.ts b/packages/core/src/lib/utils/get.util.ts index 44afd7431..f6fd4addc 100644 --- a/packages/core/src/lib/utils/get.util.ts +++ b/packages/core/src/lib/utils/get.util.ts @@ -1,21 +1,14 @@ -export function get(object: T, ...paths: string[]): unknown { - if (!paths.length) { +export function get(object: T, path: (string | symbol)[] = []): unknown { + if (!path.length) { return; } - function _getInternal(innerObject: T, path: string) { - const _path = path.split('.').filter(Boolean); - return _path.reduce((obj: any, key) => obj && obj[key], innerObject); - } + let index: number + const length = path.length - let val = _getInternal(object, paths[0]); - for (let i = 1, len = paths.length; i < len; i++) { - if (val != null) { - val = _getInternal(val, paths[i]); - continue; - } - val = _getInternal(object, paths[i]); + for (index = 0; index < length && object != null; index++) { + object = object[path[index]]; } - return val; + return (index && index == length) ? object : undefined } diff --git a/packages/core/src/lib/utils/index.ts b/packages/core/src/lib/utils/index.ts index 4123226a5..f98084352 100644 --- a/packages/core/src/lib/utils/index.ts +++ b/packages/core/src/lib/utils/index.ts @@ -1,4 +1,6 @@ export * from './get.util'; +export * from './set.util'; +export * from './unique.util'; export * from './is-empty.util'; export * from './is-defined.util'; diff --git a/packages/core/src/lib/map/set.util.ts b/packages/core/src/lib/utils/set.util.ts similarity index 83% rename from packages/core/src/lib/map/set.util.ts rename to packages/core/src/lib/utils/set.util.ts index 43a3949ae..f42cd39c3 100644 --- a/packages/core/src/lib/map/set.util.ts +++ b/packages/core/src/lib/utils/set.util.ts @@ -1,6 +1,6 @@ export function set>( object: T, - path: string, + path: string[], value: unknown ): (T & { [p: string]: unknown }) | T { const { decomposedPath, base } = decomposePath(path); @@ -18,7 +18,7 @@ export function set>( ? value : set( object[base] as Record, - decomposedPath.slice(1).join('.'), + decomposedPath.slice(1), value ); @@ -27,7 +27,7 @@ export function set>( export function setMutate>( object: T, - path: string, + path: string[], value: unknown ): void { const { decomposedPath, base } = decomposePath(path); @@ -45,17 +45,20 @@ export function setMutate>( } else { setMutate( object[base] as Record, - decomposedPath.slice(1).join('.'), + decomposedPath.slice(1), value ); } } function decomposePath( - path: string + path: string[] ): { decomposedPath: string[]; base: string } { - const decomposedPath = path.split('.'); - const base = decomposedPath[0]; + if (path.length < 1) { + return { base: undefined, decomposedPath: undefined }; + } + const decomposedPath = path; + const base = path[0]; return { base, decomposedPath }; } diff --git a/packages/core/src/lib/utils/specs/get.spec.ts b/packages/core/src/lib/utils/specs/get.spec.ts index 4108ed0e5..770293cd0 100644 --- a/packages/core/src/lib/utils/specs/get.spec.ts +++ b/packages/core/src/lib/utils/specs/get.spec.ts @@ -1,25 +1,33 @@ import { get } from '../get.util'; describe('get', () => { - const obj = { foo: { bar: 'bar' } }; + const obj = { + foo: { bar: 'bar' }, + ['.startDot']: { ['mid.Dot']: { ['endDot.']: 'custom' } } + }; it('should return bar', () => { - const result = get(obj, 'foo', 'bar'); + const result = get(obj, ['foo', 'bar']); expect(result).toEqual('bar'); }); + it('should handle dotted path', () => { + const result = get(obj, ['.startDot', 'mid.Dot', 'endDot.']); + expect(result).toEqual('custom'); + }); + it('should return null', () => { - const result = get({ foo: { bar: null } }, 'foo', 'bar'); + const result = get({ foo: { bar: null } }, ['foo', 'bar']); expect(result).toEqual(null); }); it('should return undefined for unknown path', () => { - const result = get(obj, 'foo', 'baz'); + const result = get(obj, ['foo', 'baz']); expect(result).toEqual(undefined); }); it('should return object', () => { - const result = get(obj, 'foo'); + const result = get(obj, ['foo']); expect(result).toEqual({ bar: 'bar' }); }); diff --git a/packages/core/src/lib/utils/specs/set.spec.ts b/packages/core/src/lib/utils/specs/set.spec.ts new file mode 100644 index 000000000..9dffaad53 --- /dev/null +++ b/packages/core/src/lib/utils/specs/set.spec.ts @@ -0,0 +1,83 @@ +import { set, setMutate } from '../set.util'; + +describe('set', () => { + it('should set nested property', () => { + const result = set({ foo: { bar: 'foo' } }, ['foo', 'bar'], 'baz'); + expect(result).toEqual({ foo: { bar: 'baz' } }); + }); + + it('should set obj', () => { + const result = set({ foo: { bar: 'foo' } }, ['foo'], { baz: 'foo' }); + expect(result).toEqual({ foo: { baz: 'foo' } }); + }); + + it('should add property to obj at unknown path', () => { + let result = set({ foo: { bar: 'foo' } }, ['bar'], 'foo'); + expect(result).toEqual({ foo: { bar: 'foo' }, bar: 'foo' }); + + result = set({ foo: { bar: 'foo' } }, ['foo', 'baz'], 'baz'); + expect(result).toEqual({ foo: { bar: 'foo', baz: 'baz' } }); + }); + + it('should add property to obj for empty and trailing dot paths', () => { + let result = set({}, [''], 'foo'); + expect(result).toEqual({ '': 'foo' }); + + result = set({}, ['foo', ''], 'bar'); + expect(result).toEqual({ foo: { '': 'bar' } }); + + result = set({ foo: {} }, ['foo', ''], 'bar'); + expect(result).toEqual({ foo: { '': 'bar' } }); + }); + + it('should add property to obj at path contains dot', () => { + let result = set({}, ['.startDot'], 'foo'); + expect(result).toEqual({ ['.startDot']: 'foo' }); + + result = set({}, ['mid.Dot', '.startDot'], 'bar'); + expect(result).toEqual({ ['mid.Dot']: { ['.startDot']: 'bar' } }); + + result = set({ ['endDot.']: {} }, ['endDot.', ''], 'bar'); + expect(result).toEqual({ ['endDot.']: { '': 'bar' } }); + }); +}); + +describe('setMutate', () => { + let obj: Record; + + beforeEach(() => { + obj = { foo: { bar: 'foo' } }; + }); + + it('should set nested property', () => { + setMutate(obj, ['foo', 'bar'], 'baz'); + expect(obj).toEqual({ foo: { bar: 'baz' } }); + }); + + it('should set obj', () => { + setMutate(obj, ['foo'], { baz: 'foo' }); + expect(obj).toEqual({ foo: { baz: 'foo' } }); + }); + + it('should add property to obj at unknown path', () => { + setMutate(obj, ['bar'], 'foo'); + expect(obj).toEqual({ foo: { bar: 'foo' }, bar: 'foo' }); + + setMutate(obj, ['foo', 'baz'], 'baz'); + expect(obj).toEqual({ foo: { bar: 'foo', baz: 'baz' }, bar: 'foo' }); + }); + + it('should add property to obj at path contains dot', () => { + setMutate(obj, ['.startDot'], 'foo'); + expect(obj).toEqual({ foo: { bar: 'foo' }, ['.startDot']: 'foo' }); + + setMutate(obj, ['foo', 'mid.Dot', '.startDot'], 'bar'); + expect(obj).toEqual({ foo: { bar: 'foo', ['mid.Dot']: { ['.startDot']: 'bar' } }, ['.startDot']: 'foo' }); + + setMutate(obj, ['foo', 'endDot.', ''], 'bar'); + expect(obj).toEqual({ + foo: { bar: 'foo', ['mid.Dot']: { ['.startDot']: 'bar' }, ['endDot.']: { ['']: 'bar' } }, + ['.startDot']: 'foo' + }); + }); +}); diff --git a/packages/core/src/lib/utils/unique.util.ts b/packages/core/src/lib/utils/unique.util.ts new file mode 100644 index 000000000..bba5c4982 --- /dev/null +++ b/packages/core/src/lib/utils/unique.util.ts @@ -0,0 +1,22 @@ +export function uniquePaths(paths: string[][]): string[][] { + const result: string[][] = []; + for (let i = 0; i < paths.length; i++) { + const value = paths[i]; + if (!result.some(item => isSamePath(item, value))) { + result.push(value); + } + } + return result; +} + +export function isSamePath(target: string[], value: string[]): boolean { + if (target.length !== value.length) { + return false; + } + for (let i = 0; i < target.length; i++) { + if (target[i] !== value[i]) { + return false; + } + } + return true; +} diff --git a/packages/integration-test/src/lib/with-classes/deep-nest-override.spec.ts b/packages/integration-test/src/lib/with-classes/deep-nest-override.spec.ts new file mode 100644 index 000000000..1fec9337a --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/deep-nest-override.spec.ts @@ -0,0 +1,45 @@ +import { setupClasses } from '../setup.spec'; +import { + Foo, + FooDto, + FooFoo, FooFooDto, + FooFooFoo, FooFooFooDto +} from './fixtures/models/deep-nest'; +import { + deepNestedFooFooFooProfile +} from './fixtures/profiles/deep-nest-override.profile'; + +describe('Override Deep Nest models', () => { + const [mapper] = setupClasses('deep-nest-override'); + + it('should map forMember override properly', () => { + mapper.addProfile(deepNestedFooFooFooProfile); + + const foo = new FooFooFoo(); + foo.foo = 'some value'; + + const vm = mapper.map(foo, FooFooFooDto, FooFooFoo); + expect(vm).toBeTruthy(); + expect(vm.foo).toEqual('FooFooFoo Custom Value'); + }); + + it('should map inner forMember override properly', () => { + mapper.addProfile(deepNestedFooFooFooProfile); + + const foo = new FooFoo(); + foo.foo = new FooFooFoo(); + foo.foo.foo = 'some value'; + + const vm = mapper.map(foo, FooFooDto, FooFoo); + expect(vm).toBeTruthy(); + expect(vm.foo.foo).toEqual('FooFoo Custom Value'); + }); + + it('should map deep inner forMember override properly', () => { + mapper.addProfile(deepNestedFooFooFooProfile); + + const vm = mapper.map({ foo: { foo: { foo: 'some value' } } }, FooDto, Foo); + expect(vm).toBeTruthy(); + expect(vm.foo.foo.foo).toEqual('Foo Custom Value'); + }); +}); diff --git a/packages/integration-test/src/lib/with-classes/fixtures/models/custom-keys.ts b/packages/integration-test/src/lib/with-classes/fixtures/models/custom-keys.ts new file mode 100644 index 000000000..dc51b34ee --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/models/custom-keys.ts @@ -0,0 +1,33 @@ +import { AutoMap } from '@automapper/classes'; + +export class CustomKeyBar { + @AutoMap({ typeFn: () => String }) + ['.startDot']!: string | null; +} + +export class CustomKeyBarVm { + @AutoMap({ typeFn: () => String }) + ['.startDot']!: string | null; +} + +export class CustomKeyFoo { + @AutoMap() + ['.startDot']!: string; + @AutoMap({ typeFn: () => CustomKeyBar }) + ['mid.Dot']!: CustomKeyBar; + @AutoMap({ typeFn: () => Number }) + ['endDot.']!: number | null; + @AutoMap({ typeFn: () => String }) + normalKey!: string +} + +export class CustomKeyFooVm { + @AutoMap() + ['.startDot']!: string; + @AutoMap({ typeFn: () => CustomKeyBarVm }) + ['mid.Dot']!: CustomKeyBarVm; + @AutoMap({ typeFn: () => Number }) + ['endDot.']!: number | null; + @AutoMap({ typeFn: () => String }) + normalKey!: string +} diff --git a/packages/integration-test/src/lib/with-classes/fixtures/profiles/custom-key.profile.ts b/packages/integration-test/src/lib/with-classes/fixtures/profiles/custom-key.profile.ts new file mode 100644 index 000000000..eee5bc8f3 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/profiles/custom-key.profile.ts @@ -0,0 +1,12 @@ +import type { MappingProfile } from '@automapper/types'; +import { + CustomKeyBar, + CustomKeyBarVm, + CustomKeyFoo, + CustomKeyFooVm +} from '../models/custom-keys'; + +export const customKeyProfile: MappingProfile = (mapper) => { + mapper.createMap(CustomKeyBar, CustomKeyBarVm); + mapper.createMap(CustomKeyFoo, CustomKeyFooVm); +}; diff --git a/packages/integration-test/src/lib/with-classes/fixtures/profiles/deep-nest-override.profile.ts b/packages/integration-test/src/lib/with-classes/fixtures/profiles/deep-nest-override.profile.ts new file mode 100644 index 000000000..a29d50475 --- /dev/null +++ b/packages/integration-test/src/lib/with-classes/fixtures/profiles/deep-nest-override.profile.ts @@ -0,0 +1,35 @@ +import type { MappingProfile } from '@automapper/types'; +import { + Foo, FooBar, FooBarBaz, FooBarBazDto, + FooBarBazQux, + FooBarBazQuxDto, FooBarDto, + FooDto, + FooFoo, + FooFooDto, + FooFooFoo, + FooFooFooDto +} from '../models/deep-nest'; +import { fromValue } from '@automapper/core'; + + +export const deepNestedFooFooFooProfile: MappingProfile = (mapper) => { + mapper.createMap(FooFooFoo, FooFooFooDto).forMember( + (dest) => dest.foo, + fromValue('FooFooFoo Custom Value') + ); + mapper.createMap(FooFoo, FooFooDto).forMember( + (dest) => dest.foo.foo, + fromValue('FooFoo Custom Value') + ); + mapper.createMap(Foo, FooDto).forMember( + (dest) => dest.foo.foo.foo, + fromValue('Foo Custom Value') + ); +}; + + +export const deepNestedFooBarBazQuxProfile: MappingProfile = (mapper) => { + mapper.createMap(FooBarBazQux, FooBarBazQuxDto); + mapper.createMap(FooBarBaz, FooBarBazDto); + mapper.createMap(FooBar, FooBarDto); +}; diff --git a/packages/integration-test/src/lib/with-classes/map.spec.ts b/packages/integration-test/src/lib/with-classes/map.spec.ts index 14300b493..43842918f 100644 --- a/packages/integration-test/src/lib/with-classes/map.spec.ts +++ b/packages/integration-test/src/lib/with-classes/map.spec.ts @@ -3,6 +3,7 @@ import { setupClasses } from '../setup.spec'; import { Doctor, DoctorDto } from './fixtures/models/doctor'; import { User, UserVm } from './fixtures/models/user'; import { PascalUser, PascalUserVm } from './fixtures/models/user-pascal'; +import { CustomKeyBar, CustomKeyFoo, CustomKeyFooVm } from './fixtures/models/custom-keys'; import { addressProfile, pascalAddressProfile, @@ -23,6 +24,7 @@ import { userProfile, } from './fixtures/profiles/user.profile'; import { getPascalUser, getUser } from './utils/get-user'; +import { customKeyProfile } from './fixtures/profiles/custom-key.profile'; describe('Map - Non Flattening', () => { const [mapper] = setupClasses('map'); @@ -224,4 +226,21 @@ describe('Map - Non Flattening', () => { expect(dto.name).toEqual(doctor.name); expect(dto.titleTags).toEqual(doctor.titleTags); }); + + it('should map custom keys', () => { + mapper.addProfile(customKeyProfile); + const foo = new CustomKeyFoo(); + + foo.normalKey = 'Normal'; + foo['.startDot'] = 'Foo'; + foo['endDot.'] = 123; + foo['mid.Dot'] = new CustomKeyBar(); + foo['mid.Dot']['.startDot'] = 'Bar'; + + const vm = mapper.map(foo, CustomKeyFooVm, CustomKeyFoo); + expect(vm.normalKey).toEqual(foo.normalKey); + expect(vm['.startDot']).toEqual(foo['.startDot']); + expect(vm['mid.Dot']['.startDot']).toEqual(foo['mid.Dot']['.startDot']); + expect(vm['endDot.']).toEqual(foo['endDot.']); + }); }); diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/custom-keys.interface.ts b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/custom-keys.interface.ts new file mode 100644 index 000000000..09c4f4de3 --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/interfaces/custom-keys.interface.ts @@ -0,0 +1,47 @@ +import { createMetadataMap } from '@automapper/pojos'; + +export interface CustomKeyBar { + '.startDot': string | null +} + +export interface CustomKeyBarVm { + '.startDot': string | null; +} + +export interface CustomKeyFoo { + '.startDot': string; + 'mid.Dot': CustomKeyBar; + 'endDot.': number | null; + normalKey: string +} + +export interface CustomKeyFooVm { + '.startDot': string; + 'mid.Dot': CustomKeyBarVm; + 'endDot.': number | null; + normalKey: string +} + +export function createCustomKeyFooMetadata() { + createMetadataMap('CustomKeyBar', { + '.startDot': String, + }); + + createMetadataMap('CustomKeyBarVm', { + '.startDot': String, + }); + + createMetadataMap('CustomKeyFoo', { + '.startDot': String, + 'mid.Dot': 'CustomKeyBar', + 'endDot.': Number, + normalKey: String, + }); + + createMetadataMap('CustomKeyFooVm', { + '.startDot': String, + 'mid.Dot': 'CustomKeyBarVm', + 'endDot.': Number, + normalKey: String, + }); +} diff --git a/packages/integration-test/src/lib/with-pojos/fixtures/profiles/custom-keys.profile.ts b/packages/integration-test/src/lib/with-pojos/fixtures/profiles/custom-keys.profile.ts new file mode 100644 index 000000000..5e6caf8be --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/fixtures/profiles/custom-keys.profile.ts @@ -0,0 +1,23 @@ +import { MappingProfile } from '@automapper/types'; +import { + createCustomKeyFooMetadata, + CustomKeyBar, + CustomKeyBarVm, + CustomKeyFoo, CustomKeyFooVm +} from '../interfaces/custom-keys.interface'; + +export const customKeyProfile: MappingProfile = (mapper) => { + createCustomKeyFooMetadata(); + + mapper + .createMap( + 'CustomKeyFoo', + 'CustomKeyFooVm' + ); + + mapper + .createMap( + 'CustomKeyBar', + 'CustomKeyBarVm' + ); +} diff --git a/packages/integration-test/src/lib/with-pojos/map.spec.ts b/packages/integration-test/src/lib/with-pojos/map.spec.ts index 1760ae1a9..f7154ef12 100644 --- a/packages/integration-test/src/lib/with-pojos/map.spec.ts +++ b/packages/integration-test/src/lib/with-pojos/map.spec.ts @@ -24,6 +24,9 @@ import { userProfile, } from './fixtures/profiles/user.profile'; import { getPascalUser, getUser } from './utils/get-user'; +import { getCustomKeyFoo } from './utils/get-custom-key'; +import { CustomKeyFoo, CustomKeyFooVm } from './fixtures/interfaces/custom-keys.interface'; +import { customKeyProfile } from './fixtures/profiles/custom-keys.profile'; describe('Map - Non Flattening', () => { const [mapper] = setupPojos('map'); @@ -177,4 +180,18 @@ describe('Map - Non Flattening', () => { ); expect(vm).toBeTruthy(); }); + + it('should map custom keys', () => { + mapper + .addProfile(customKeyProfile); + + const foo = getCustomKeyFoo(); + + const vm = mapper.map( + foo, + 'CustomKeyFooVm', + 'CustomKeyFoo' + ); + expect(vm).toBeTruthy(); + }); }); diff --git a/packages/integration-test/src/lib/with-pojos/utils/get-custom-key.ts b/packages/integration-test/src/lib/with-pojos/utils/get-custom-key.ts new file mode 100644 index 000000000..45f525cdf --- /dev/null +++ b/packages/integration-test/src/lib/with-pojos/utils/get-custom-key.ts @@ -0,0 +1,20 @@ +import { CustomKeyBar, CustomKeyFoo } from '../fixtures/interfaces/custom-keys.interface'; + +export function getCustomKeyFoo( + partials: { + foo?: Partial; + bar?: Partial; + } = {} +) { + const bar: CustomKeyBar = { + '.startDot': 'Internet', + ...(partials.bar ?? {}), + }; + + return { + '.startDot': 'Chau', + 'mid.Dot': bar, + 'endDot.': 5, + ...(partials.foo ?? {}), + } as CustomKeyFoo; +} diff --git a/packages/nestjs-integration-test/tsconfig.spec.json b/packages/nestjs-integration-test/tsconfig.spec.json index 29efa430b..aacc4a35c 100644 --- a/packages/nestjs-integration-test/tsconfig.spec.json +++ b/packages/nestjs-integration-test/tsconfig.spec.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "../../dist/out-tsc", "module": "commonjs", - "types": ["jest", "node"] + "types": ["jest", "node", "reflect-metadata"] }, "include": ["**/*.spec.ts", "**/*.d.ts"] } diff --git a/packages/pojos/src/lib/pojos.ts b/packages/pojos/src/lib/pojos.ts index e8fb87f11..77412021e 100644 --- a/packages/pojos/src/lib/pojos.ts +++ b/packages/pojos/src/lib/pojos.ts @@ -90,7 +90,7 @@ function exploreMetadata( if (!metadataStorage.has(key)) { const metadataList = pojosSymbolStorage.get(Symbol.for(key)); for (const [propertyKey, metadata] of metadataList) { - metadataStorage.addMetadata(key, [propertyKey, () => metadata]); + metadataStorage.addMetadata(key, [[propertyKey], () => metadata]); } } }); diff --git a/packages/pojos/src/lib/storages/pojos-metadata.storage.ts b/packages/pojos/src/lib/storages/pojos-metadata.storage.ts index e7e39feb2..7ecfd2990 100644 --- a/packages/pojos/src/lib/storages/pojos-metadata.storage.ts +++ b/packages/pojos/src/lib/storages/pojos-metadata.storage.ts @@ -1,4 +1,5 @@ import type { Metadata, MetadataStorage } from '@automapper/types'; +import { isSamePath } from '@automapper/core'; /** * Internal PojosMetadataStorage @@ -13,7 +14,7 @@ export class PojosMetadataStorage implements MetadataStorage { const exists = this.storage.get(metaKey) ?? []; // if already exists, break - if (exists.some(([existKey]) => existKey === metadata[0])) { + if (exists.some(([existKey]) => isSamePath(existKey, metadata[0]))) { return; } @@ -22,18 +23,17 @@ export class PojosMetadataStorage implements MetadataStorage { getMetadata(metaKey: string): Array> { const metadataList = this.storage.get(metaKey) ?? []; - let i = metadataList.length; // empty metadata - if (!i) { + if (!metadataList.length) { return []; } const resultMetadataList: Array> = []; - while (i--) { + for (let i = 0; i < metadataList.length; i++) { const metadata = metadataList[i]; // skip existing - if (resultMetadataList.some(([key]) => key === metadata[0])) { + if (resultMetadataList.some(([key]) => isSamePath(key, metadata[0]))) { continue; } resultMetadataList.push(metadataList[i]); @@ -44,10 +44,10 @@ export class PojosMetadataStorage implements MetadataStorage { getMetadataForKey( metaKey: string, - key: string + key: string[] ): Metadata | undefined { return this.getMetadata(metaKey).find( - ([innerMetaKey]) => innerMetaKey === key + ([innerMetaKey]) => isSamePath(innerMetaKey, key) ); } diff --git a/packages/pojos/src/lib/utils/instantiate.util.ts b/packages/pojos/src/lib/utils/instantiate.util.ts index 2fc306a32..9966c375e 100644 --- a/packages/pojos/src/lib/utils/instantiate.util.ts +++ b/packages/pojos/src/lib/utils/instantiate.util.ts @@ -1,4 +1,6 @@ import { + get, + setMutate, isDateConstructor, isDefined, isEmpty, @@ -20,22 +22,23 @@ export function instantiate>( } const nestedMetadataMap: unknown[] = []; - let i = metadata.length; - while (i--) { + for (let i = 0; i < metadata.length; i++) { const [key, meta] = metadata[i]; - const valueAtKey = (obj as Record)[key]; + const valueAtKey = get(obj as Record, key); const metaResult = meta(); if (isPrimitiveConstructor(metaResult) || metaResult === null) { - (obj as Record)[key] = isDefined(valueAtKey) + const value = isDefined(valueAtKey) ? valueAtKey : undefined; + setMutate(obj as Record, key, value); continue; } if (isDateConstructor(metaResult)) { - (obj as Record)[key] = isDefined(valueAtKey) + const value = isDefined(valueAtKey) ? new Date(valueAtKey as number) : undefined; + setMutate(obj as Record, key, value); continue; } @@ -45,10 +48,11 @@ export function instantiate>( nestedMetadataMap.push([key, metaResult]); if (Array.isArray(valueAtKey)) { - (obj as Record)[key] = valueAtKey.map((val) => { + const value = valueAtKey.map((val) => { const [childObj] = instantiate(metadataStorage, metaResult, val); return childObj; }); + setMutate(obj as Record, key, value); continue; } @@ -58,17 +62,17 @@ export function instantiate>( metaResult, valueAtKey as Dictionary ); - (obj as Record)[key] = instantiateResult; + setMutate(obj as Record, key, instantiateResult); continue; } if (isDefined(defaultValue)) { - (obj as Record)[key] = valueAtKey; + setMutate(obj as Record, key, valueAtKey); continue; } const [result] = instantiate(metadataStorage, metaResult); - (obj as Record)[key] = result; + setMutate(obj as Record, key, result); } return [obj, nestedMetadataMap]; diff --git a/packages/pojos/src/lib/utils/specs/instantiate.util.spec.ts b/packages/pojos/src/lib/utils/specs/instantiate.util.spec.ts index b02b301c5..659c27dbc 100644 --- a/packages/pojos/src/lib/utils/specs/instantiate.util.spec.ts +++ b/packages/pojos/src/lib/utils/specs/instantiate.util.spec.ts @@ -53,7 +53,7 @@ describe('instantiate', () => { const result = parameterizedInstantiate(); expect(result).toEqual([ { foo: undefined, bar: { bar: undefined, date: undefined } }, - [['bar', 'Bar']], + [[['bar'], 'Bar']], ]); }); }); @@ -81,7 +81,7 @@ describe('instantiate', () => { const result = parameterizedInstantiate(); - expect(result).toEqual([defaultFoo, [['bar', 'Bar']]]); + expect(result).toEqual([defaultFoo, [[['bar'], 'Bar']]]); }); }); @@ -95,15 +95,15 @@ describe('instantiate', () => { when(mockedMetadataStorage.getMetadata) .calledWith('Foo') .mockReturnValueOnce([ - ['foo', () => (String as unknown) as string], - ['bar', () => 'Bar'], + [['foo'], () => (String as unknown) as string], + [['bar'], () => 'Bar'], ]); when(mockedMetadataStorage.getMetadata) .calledWith('Bar') .mockReturnValueOnce([ - ['bar', () => (String as unknown) as string], - ['date', () => (Date as unknown) as Date], + [['bar'], () => (String as unknown) as string], + [['date'], () => (Date as unknown) as Date], ]); } }); diff --git a/packages/types/src/core.ts b/packages/types/src/core.ts index 6c9720685..8843ed5af 100644 --- a/packages/types/src/core.ts +++ b/packages/types/src/core.ts @@ -116,7 +116,7 @@ export type ConditionReturn< TSelectorReturn = SelectorReturn > = [ TransformationType.Condition, - (source: TSource, ...sourceMemberPaths: string[]) => TSelectorReturn + (source: TSource, sourceMemberPath: string[]) => TSelectorReturn ]; export type FromValueReturn< @@ -136,7 +136,7 @@ export type NullSubstitutionReturn< TSelectorReturn = SelectorReturn > = [ TransformationType.NullSubstitution, - (source: TSource, ...sourceMemberPaths: string[]) => TSelectorReturn + (source: TSource, sourceMemberPath: string[]) => TSelectorReturn ]; export type IgnoreReturn = [TransformationType.Ignore]; @@ -520,7 +520,7 @@ export type MappingProperty< TDestination extends Dictionary = any, TSelectorReturn = SelectorReturn > = readonly [ - [target: string, origin?: string], + [target: string[], origin?: string[]], MappingTransformation ]; @@ -531,7 +531,7 @@ export type Mapping< [source: TSource, destination: TDestination], Array< [ - path: string, + path: string[], mappingProperty: MappingProperty< TSource, TDestination, @@ -555,7 +555,7 @@ export interface Disposable { export interface MetadataStorage extends Disposable { getMetadata(metaKey: TKey): Array>; - getMetadataForKey(metaKey: TKey, key: string): Metadata | undefined; + getMetadataForKey(metaKey: TKey, key: string[]): Metadata | undefined; addMetadata(metaKey: TKey, metadata: Metadata): void; @@ -571,7 +571,7 @@ export interface MappingStorage extends Disposable { } export type Metadata = [ - string, + string[], () => String | Number | Boolean | Date | TMetaType, boolean? ];