Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core) Add beforeMapArray and afterMapArray configs #618

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export * from './lib/mapping-configurations/type-converters';
export * from './lib/mapping-configurations/construct-using';
export * from './lib/mapping-configurations/before-map';
export * from './lib/mapping-configurations/after-map';
export * from './lib/mapping-configurations/before-map-array';
export * from './lib/mapping-configurations/after-map-array';
export * from './lib/mapping-configurations/extend';
export * from './lib/mapping-configurations/naming-conventions';
export * from './lib/mapping-configurations/auto-map';
Expand Down
35 changes: 23 additions & 12 deletions packages/core/src/lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
ModelIdentifier,
NamingConventionInput,
} from './types';
import { getBeforeAndAfterMap } from './utils/get-callbacks';
import { getMapping } from './utils/get-mapping';
import { AutoMapperLogger } from './utils/logger';

Expand Down Expand Up @@ -314,13 +315,18 @@ Mapper {} is an empty Object as a Proxy. The following methods are available to
);

const { beforeMap, afterMap, extraArgs } =
(mapOptions || {}) as MapOptions<
TSource[],
TDestination[]
>;
getBeforeAndAfterMap<TSource[], TDestination[]>(
mapping,
(mapOptions || {}) as MapOptions<
TSource[],
TDestination[]
>
);

const extraArguments = extraArgs?.(mapping, []);

if (beforeMap) {
beforeMap(sourceArray, []);
beforeMap(sourceArray, [], extraArguments);
}

const destinationArray: TDestination[] = [];
Expand Down Expand Up @@ -361,7 +367,7 @@ Mapper {} is an empty Object as a Proxy. The following methods are available to
}

if (afterMap) {
afterMap(sourceArray, destinationArray);
afterMap(sourceArray, destinationArray, extraArguments);
}

return destinationArray;
Expand Down Expand Up @@ -492,13 +498,18 @@ Mapper {} is an empty Object as a Proxy. The following methods are available to
);

const { beforeMap, afterMap, extraArgs } =
(mapOptions || {}) as MapOptions<
TSource[],
TDestination[]
>;
getBeforeAndAfterMap<TSource[], TDestination[]>(
mapping,
(mapOptions || {}) as MapOptions<
TSource[],
TDestination[]
>
);

const extraArguments = extraArgs?.(mapping, destinationArray);

if (beforeMap) {
beforeMap(sourceArray, destinationArray);
beforeMap(sourceArray, destinationArray, extraArguments);
}

for (
Expand Down Expand Up @@ -534,7 +545,7 @@ Mapper {} is an empty Object as a Proxy. The following methods are available to
}

if (afterMap) {
afterMap(sourceArray, destinationArray);
afterMap(sourceArray, destinationArray, extraArguments);
}
};
}
Expand Down
17 changes: 17 additions & 0 deletions packages/core/src/lib/mapping-configurations/after-map-array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Dictionary, MapCallback, MappingConfiguration } from '../types';
import { MappingCallbacksClassId, MappingClassId } from '../types';

export function afterMapArray<
TSource extends Dictionary<TSource>,
TDestination extends Dictionary<TDestination>
>(
cb: MapCallback<TSource[], TDestination[]>
): MappingConfiguration<TSource, TDestination> {
return (mapping) => {
if (!mapping[MappingClassId.callbacks]) {
mapping[MappingClassId.callbacks] = [];
}
mapping[MappingClassId.callbacks][MappingCallbacksClassId.afterMapArray] =
cb;
};
}
17 changes: 17 additions & 0 deletions packages/core/src/lib/mapping-configurations/before-map-array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Dictionary, MapCallback, MappingConfiguration } from '../types';
import { MappingCallbacksClassId, MappingClassId } from '../types';

export function beforeMapArray<
TSource extends Dictionary<TSource>,
TDestination extends Dictionary<TDestination>
>(
cb: MapCallback<TSource[], TDestination[]>
): MappingConfiguration<TSource, TDestination> {
return (mapping) => {
if (!mapping[MappingClassId.callbacks]) {
mapping[MappingClassId.callbacks] = [];
}
mapping[MappingClassId.callbacks][MappingCallbacksClassId.beforeMapArray] =
cb;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Mapping } from '../../types';
import { MappingCallbacksClassId, MappingClassId } from '../../types';
import { afterMapArray } from '../after-map-array';

describe(afterMapArray.name, () => {
it('should update mapping configuration with afterMapArray', () => {
const mapping = [] as unknown as Mapping;
const cb = jest.fn();
afterMapArray(cb)(mapping);
expect(
mapping[MappingClassId.callbacks]![MappingCallbacksClassId.afterMapArray]
).toBe(cb);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Mapping } from '../../types';
import { MappingCallbacksClassId, MappingClassId } from '../../types';
import { beforeMapArray } from '../before-map-array';

describe(beforeMapArray.name, () => {
it('should update mapping configuration with beforeMapArray', () => {
const mapping = [] as unknown as Mapping;
const cb = jest.fn();
beforeMapArray(cb)(mapping);
expect(
mapping[MappingClassId.callbacks]![MappingCallbacksClassId.beforeMapArray]
).toBe(cb);
});
});
14 changes: 7 additions & 7 deletions packages/core/src/lib/mappings/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
import { MapFnClassId, MetadataClassId, TransformationType } from '../types';
import { assertUnmappedProperties } from '../utils/assert-unmapped-properties';
import { get } from '../utils/get';
import { getBeforeAndAfterMap } from '../utils/get-callbacks';
import { getMapping } from '../utils/get-mapping';
import { isDateConstructor } from '../utils/is-date-constructor';
import { isEmpty } from '../utils/is-empty';
Expand Down Expand Up @@ -116,17 +117,12 @@ export function map<
,
mapper,
destinationConstructor,
,
[mappingBeforeCallback, mappingAfterCallback] = [],
] = mapping;

// deconstruct MapOptions
const {
beforeMap: mapBeforeCallback,
afterMap: mapAfterCallback,
destinationConstructor:
mapDestinationConstructor = destinationConstructor,
extraArgs,
} = options ?? {};

const errorHandler = getErrorHandler(mapper);
Expand All @@ -137,14 +133,19 @@ export function map<
destinationIdentifier
);

const { beforeMap, afterMap, extraArgs } =
getBeforeAndAfterMap<TSource, TDestination>(
mapping,
options || {}
);

// get extraArguments
const extraArguments = extraArgs?.(mapping, destination);

// initialize an array of keys that have already been configured
const configuredKeys: string[] = [];

if (!isMapArray) {
const beforeMap = mapBeforeCallback ?? mappingBeforeCallback;
if (beforeMap) {
beforeMap(sourceObject, destination, extraArguments);
}
Expand Down Expand Up @@ -354,7 +355,6 @@ Original error: ${originalError}`;
}

if (!isMapArray) {
const afterMap = mapAfterCallback ?? mappingAfterCallback;
if (afterMap) {
afterMap(sourceObject, destination, extraArguments);
}
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,8 @@ export const enum MappingPropertiesClassId {
export const enum MappingCallbacksClassId {
beforeMap,
afterMap,
beforeMapArray,
afterMapArray,
}

export const enum NestedMappingPairClassId {
Expand Down Expand Up @@ -564,7 +566,9 @@ export type Mapping<
>,
callbacks?: [
beforeMap?: MapCallback<TSource, TDestination>,
afterMap?: MapCallback<TSource, TDestination>
afterMap?: MapCallback<TSource, TDestination>,
beforeMapArray?: MapCallback<TSource[], TDestination[]>,
afterMapArray?: MapCallback<TSource[], TDestination[]>
],
namingConventions?: [
source: NamingConvention,
Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/lib/utils/get-callbacks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Dictionary, MapOptions, Mapping, MappingCallbacksClassId, MappingClassId } from "../types";

export function getBeforeAndAfterMap<
TSource extends Dictionary<TSource>,
TDestination extends Dictionary<TDestination>
>(
mapping: Mapping<TSource, TDestination>,
mapOptions?: MapOptions<TSource, TDestination>
): Pick<MapOptions<TSource, TDestination>, 'beforeMap' | 'afterMap' | 'extraArgs'> {
const callbacks = mapping?.[MappingClassId.callbacks];
const beforeMapCfg = callbacks?.[MappingCallbacksClassId.beforeMap];
const afterMapCfg = callbacks?.[MappingCallbacksClassId.afterMap];

const { beforeMap: beforeMapCb, afterMap: afterMapCb, extraArgs } =
mapOptions || {};

return {
beforeMap: beforeMapCb || beforeMapCfg,
afterMap: afterMapCb || afterMapCfg,
extraArgs,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,40 @@ custom_edit_url: null

## Enumeration members

### beforeMap

• **beforeMap** = `0`

#### Defined in

[lib/types.ts:487](https://github.com/nartc/mapper/blob/9d18866/packages/core/src/lib/types.ts#L487)

___

### afterMap

• **afterMap** = `1`

#### Defined in

[lib/types.ts:483](https://github.com/nartc/mapper/blob/efc4cb9d/packages/core/src/lib/types.ts#L483)
[lib/types.ts:488](https://github.com/nartc/mapper/blob/9d18866/packages/core/src/lib/types.ts#L488)

___

### beforeMap
### beforeMapArray

• **beforeMap** = `0`
• **beforeMapArray** = `2`

#### Defined in

[lib/types.ts:489](https://github.com/nartc/mapper/blob/9d18866/packages/core/src/lib/types.ts#L489)

___

### afterMapArray

• **afterMapArray** = `3`

#### Defined in

[lib/types.ts:482](https://github.com/nartc/mapper/blob/efc4cb9d/packages/core/src/lib/types.ts#L482)
[lib/types.ts:490](https://github.com/nartc/mapper/blob/9d18866/packages/core/src/lib/types.ts#L490)
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
id: after-map-array
title: AfterMapArray
sidebar_label: AfterMapArray
sidebar_position: 3
---

As the name suggests, `afterMapArray()` sets up a `MapCallback` to be called **after** the mapArray operation.

## Configure on `Mapping`

Pass `afterMapArray()` in `createMap()` to sets up the `MapCallback`

```ts
createMap(
mapper,
User,
UserDto,
afterMapArray((sources, destinations) => {
// destinations.map((destination, index) => Object.assign(destination, { prop: sources[index].prop }));
})
);
```

## Configure on `mapArray()`

Pass `afterMap` in `MapOptions` when calling `mapArray()` to sets up the `MapCallback`

```ts
mapper.mapArray(user, User, UserDto, {
afterMap: (sources, destinations) => {},
});
```

:::info

- `afterMap()` on `mapArray()` has precedence over `afterMapArray` on `Mapping`
- both `afterMap()` on `mapArray()` and `afterMapArray()` on `Mapping` will be invoked with `(sourceArray, destinationArray)` parameters

:::

## Async Mapping

One of the common use-cases of `afterMapArray` is to execute some asynchronous operation. Let's assume our `Destinations` have some property whose value can only be computed from an asynchronous operation, we can leverage `mapArrayAsync()` and `afterMapArray()` for it.

```ts
createMap(
mapper,
User,
UserDto,
// 👇 We are fetching the "fullName" manually
// 👇 👇 so we need to ignore it
forMember((d) => d.fullName, ignore()),
afterMapArray(async (sources, destinations) => {
await Promise.all(
sources.map(async (source, index) => {
const fullName = await fetchFullName(source);
Object.assign(destinations[index], { fullName });
})
);
})
);

// 👇 mapArrayAsync is needed if we use the above "trick" with afterMapArray
const dto = await mapper.mapArrayAsync([user], User, UserDto);
```

:::caution

Simple asynchronous operations should be fine with this approach. However due to [Fake Async](../misc/fake-async), we should **NOT** use AutoMapper for a particular pair of models if those models require some heavy and complex asynchronous operations.

:::

## What about `postMap`?

When create the `Mapper`, we can customize the `postMap` function on the `MappingStrategy`. The differences between `postMap` and `afterMapArray` are:

- `postMap` runs after every **map** operation
- There is only one `postMap` per `Mapper`
- `postMap` runs **BEFORE** `afterMapArray`
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ mapper.map(user, User, UserDto, {
:::info

- `afterMap()` on `map()` has precedence over `Mapping`
- For `mapArray` (and its variants), `afterMap()` on `Mapping` is **ignored** because it would be bad for performance if we run `afterMap` for each and every item of the array. `afterMap()` on `mapArray()` will be invoked with `(sourceArray, destinationArray)` instead
- For `mapArray` (and its variants), `afterMap()` on `Mapping` is **ignored** because it would be bad for performance if we run `afterMap` for each and every item of the array. Use `afterMapArray()` on `Mapping` instead and it will be invoked with `(sourceArray, destinationArray)` params

:::

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
id: auto-map
title: AutoMap
sidebar_label: AutoMap
sidebar_position: 3
sidebar_position: 4
---

`autoMap()` is an alternative to the the `@AutoMap()` decorator. It trivially maps a property with the **same name and type** on the `Source` and `Destination` objects.
Expand Down
Loading