Skip to content

Commit

Permalink
feat(classes): add logic to handle getter only properties
Browse files Browse the repository at this point in the history
  • Loading branch information
nartc committed Apr 7, 2021
1 parent 207b8b6 commit d081ac6
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 27 deletions.
107 changes: 85 additions & 22 deletions packages/classes/src/lib/decorators/automap.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,99 @@
import 'reflect-metadata';
import { AUTOMAP_PROPERTIES_METADATA_KEY } from '../constants';
import type { Constructible } from '../types';

export interface AutoMapOptions {
/**
* Type Function
*/
typeFn?: () => Constructible;
/**
* Depth for nested models
*/
depth?: number;
/**
* Is this property getter-only?
*/
isGetterOnly?: boolean;
}

/**
* AutoMap decorator to decorate fields in classes to store metadata of that field
* deprecated - AutoMap decorator to decorate fields in classes to store metadata of that field.
*
* @param {() => Constructible} [typeFn] - if this is a nested model, it should have a `typeFn`
* @param {number} [depth = 0] - how deep should this model gets instantiated
*
* @deprecated
* @see Use {@link AutoMapOptions} instead
*/
export const AutoMap = (
typeFn?: () => Constructible,
export function AutoMap(
typeFn: () => Constructible,
depth?: number
): PropertyDecorator;
/**
* AutoMap decorator to decorate fields in classes to store metadata of that field
*
* @param {AutoMapOptions} config
*/
export function AutoMap(config: AutoMapOptions): PropertyDecorator;
export function AutoMap(): PropertyDecorator;
export function AutoMap(
typeFnOrConfig?: AutoMapOptions | (() => Constructible),
depth = 0
): PropertyDecorator => (target, propertyKey) => {
// get existing metadata
const existingMetadataList =
Reflect.getMetadata(AUTOMAP_PROPERTIES_METADATA_KEY, target.constructor) ||
[];

// If `typeFn` is provided, classes plugin does not have to guess with Reflect and just `defineMetadata`
if (typeFn) {
Reflect.defineMetadata(
AUTOMAP_PROPERTIES_METADATA_KEY,
[...existingMetadataList, [propertyKey, { typeFn, depth }]],
target.constructor
);
} else {
const meta = Reflect.getMetadata('design:type', target, propertyKey);
if (meta) {
Reflect.defineMetadata(
): PropertyDecorator {
const { isGetterOnly, typeFn, depth: _depth = 0 } = getConfig(
typeFnOrConfig,
depth
);
return (target, propertyKey) => {
const newMetadata = { depth: _depth };

const existingMetadataList =
Reflect.getMetadata(
AUTOMAP_PROPERTIES_METADATA_KEY,
[...existingMetadataList, [propertyKey, { typeFn: () => meta }]],
target.constructor
) || [];

// Getting Type
if (typeFn) {
newMetadata['typeFn'] = typeFn;
} else {
const meta = Reflect.getMetadata('design:type', target, propertyKey);
if (meta) {
newMetadata['typeFn'] = () => meta;
}
}

// Getting Only-getter
if (isGetterOnly != null) {
newMetadata['isGetterOnly'] = isGetterOnly;
} else {
// paramtypes gives information about the setter.
// it will be null if this is not a getter
// it will be an [] if this is an getter-only
const paramsType = Reflect.getMetadata(
'design:paramtypes',
target,
propertyKey
);
newMetadata['isGetterOnly'] = paramsType && !(paramsType as []).length;
}

Reflect.defineMetadata(
AUTOMAP_PROPERTIES_METADATA_KEY,
[...existingMetadataList, [propertyKey, newMetadata]],
target.constructor
);
};
}

function getConfig(
typeFnOrConfig?: AutoMapOptions | (() => Constructible),
depth?: number
): AutoMapOptions {
if (typeof typeFnOrConfig === 'function') {
return { typeFn: typeFnOrConfig, depth: depth || 0 };
}
};

return typeFnOrConfig || {};
}
9 changes: 6 additions & 3 deletions packages/classes/src/lib/utils/explore-metadata.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ export function exploreMetadata(
// if no metadata, skip
if (!isDefined(metadataList)) continue;
// loop through metadata list
for (const [propertyKey, { typeFn, depth }] of metadataList) {
metadataStorage.addMetadata(model, [propertyKey, typeFn]);
for (const [
propertyKey,
{ typeFn, depth, isGetterOnly },
] of metadataList) {
metadataStorage.addMetadata(model, [propertyKey, typeFn, isGetterOnly]);
if (depth != null) {
instanceStorage.setDepth(model, propertyKey, depth);
}
Expand All @@ -33,7 +36,7 @@ export function exploreMetadata(

export function getMetadataList(
model: Constructible
): [string, { typeFn: any; depth?: number }][] {
): [string, { typeFn: any; depth?: number; isGetterOnly?: boolean }][] {
let metadataList =
Reflect.getMetadata(AUTOMAP_PROPERTIES_METADATA_KEY, model) || [];

Expand Down
7 changes: 6 additions & 1 deletion packages/classes/src/lib/utils/instantiate.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ export function instantiate<TModel extends Dictionary<TModel>>(
// reversed loop
while (i--) {
// destructure
const [key, meta] = metadata[i];
const [key, meta, isGetterOnly] = metadata[i];

// skip getter only completely
if (isGetterOnly) {
continue;
}

// get the value at the current key
const valueAtKey = (instance as Record<string, unknown>)[key];
Expand Down
3 changes: 2 additions & 1 deletion packages/types/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,8 @@ export interface MappingStorage<TKey> extends Disposable {

export type Metadata<TMetaType = unknown> = [
property: string,
metaTypeFn: () => String | Number | Boolean | Date | TMetaType
metaTypeFn: () => String | Number | Boolean | Date | TMetaType,
isGetterOnly?: boolean
];

export interface ErrorHandler {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ export const enum MapFnClassId {
export const enum MetadataClassId {
propertyKey,
metadataFn,
isGetterOnly,
}

0 comments on commit d081ac6

Please sign in to comment.