Skip to content

Commit 3bb7abf

Browse files
committed
feat: handles stackable expose decorator (typestack#378)
1 parent 261b437 commit 3bb7abf

9 files changed

+526
-138
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
]
5353
},
5454
"devDependencies": {
55-
"@rollup/plugin-commonjs": "^22.0.0",
55+
"@rollup/plugin-commonjs": "^22.0.1",
5656
"@rollup/plugin-node-resolve": "^13.3.0",
5757
"@types/jest": "^27.5.0",
5858
"@types/node": "^18.0.0",

src/MetadataStorage.ts

Lines changed: 145 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { TypeMetadata, ExposeMetadata, ExcludeMetadata, TransformMetadata } from './interfaces';
22
import { TransformationType } from './enums';
3+
import { checkVersion, flatten, onlyUnique } from './utils';
34

45
/**
56
* Storage all library metadata.
@@ -11,10 +12,32 @@ export class MetadataStorage {
1112

1213
private _typeMetadatas = new Map<Function, Map<string, TypeMetadata>>();
1314
private _transformMetadatas = new Map<Function, Map<string, TransformMetadata[]>>();
14-
private _exposeMetadatas = new Map<Function, Map<string, ExposeMetadata>>();
15-
private _excludeMetadatas = new Map<Function, Map<string, ExcludeMetadata>>();
15+
private _exposeMetadatas = new Map<Function, Map<string, ExposeMetadata[]>>();
16+
private _excludeMetadatas = new Map<Function, Map<string, ExcludeMetadata[]>>();
1617
private _ancestorsMap = new Map<Function, Function[]>();
1718

19+
// -------------------------------------------------------------------------
20+
// Static Methods
21+
// -------------------------------------------------------------------------
22+
23+
private static checkMetadataTransformationType<
24+
T extends { options?: { toClassOnly?: boolean; toPlainOnly?: boolean } }
25+
>(transformationType: TransformationType, metadata: T): boolean {
26+
if (!metadata.options) return true;
27+
if (metadata.options.toClassOnly === true && metadata.options.toPlainOnly === true) return true;
28+
29+
if (metadata.options.toClassOnly === true) {
30+
return (
31+
transformationType === TransformationType.CLASS_TO_CLASS ||
32+
transformationType === TransformationType.PLAIN_TO_CLASS
33+
);
34+
}
35+
if (metadata.options.toPlainOnly === true) {
36+
return transformationType === TransformationType.CLASS_TO_PLAIN;
37+
}
38+
return true;
39+
}
40+
1841
// -------------------------------------------------------------------------
1942
// Adder Methods
2043
// -------------------------------------------------------------------------
@@ -37,17 +60,88 @@ export class MetadataStorage {
3760
}
3861

3962
addExposeMetadata(metadata: ExposeMetadata): void {
63+
const { toPlainOnly, toClassOnly, name = metadata.propertyName } = metadata.options || {};
64+
65+
/**
66+
* check if toPlainOnly and toClassOnly used correctly.
67+
*/
68+
if (
69+
metadata.propertyName &&
70+
!(toPlainOnly === true || toClassOnly === true || (toClassOnly === undefined && toPlainOnly === undefined))
71+
) {
72+
throw Error(
73+
`${metadata.propertyName}: At least one of "toPlainOnly" and "toClassOnly" options must be "true" or both must be "undefined"`
74+
);
75+
}
76+
4077
if (!this._exposeMetadatas.has(metadata.target)) {
41-
this._exposeMetadatas.set(metadata.target, new Map<string, ExposeMetadata>());
78+
this._exposeMetadatas.set(metadata.target, new Map<string, ExposeMetadata[]>());
4279
}
43-
this._exposeMetadatas.get(metadata.target).set(metadata.propertyName, metadata);
80+
if (!this._exposeMetadatas.get(metadata.target).has(metadata.propertyName)) {
81+
this._exposeMetadatas.get(metadata.target).set(metadata.propertyName, []);
82+
}
83+
const exposeArray = this._exposeMetadatas.get(metadata.target).get(metadata.propertyName);
84+
85+
/**
86+
* check if the current @expose does not conflict with the former decorators.
87+
*/
88+
const conflictedItemIndex = exposeArray!.findIndex(m => {
89+
const { name: n = m.propertyName, since: s, until: u, toPlainOnly: tpo, toClassOnly: tco } = m.options ?? {};
90+
91+
/**
92+
* check whether the intervals intersect or not.
93+
*/
94+
const s1 = s ?? Number.NEGATIVE_INFINITY;
95+
const u1 = u ?? Number.POSITIVE_INFINITY;
96+
const s2 = metadata.options?.since ?? Number.NEGATIVE_INFINITY;
97+
const u2 = metadata.options?.until ?? Number.POSITIVE_INFINITY;
98+
99+
const intervalIntersection = s1 < u2 && s2 < u1;
100+
101+
/**
102+
* check whether the current decorator's transformation types,
103+
* means "toPlainOnly" and "toClassOnly" options,
104+
* are common with the previous decorators or not.
105+
*/
106+
const mType = tpo === undefined && tco === undefined ? 3 : (tpo ? 1 : 0) + (tco ? 2 : 0);
107+
const currentType =
108+
toPlainOnly === undefined && toClassOnly === undefined ? 3 : (toPlainOnly ? 1 : 0) + (toClassOnly ? 2 : 0);
109+
const commonInType = !!(mType & currentType);
110+
111+
/**
112+
* check if the current "name" option
113+
* is different with the imported decorators or not.
114+
*/
115+
const differentName = n !== name;
116+
117+
return intervalIntersection && commonInType && differentName;
118+
});
119+
if (conflictedItemIndex !== -1) {
120+
const conflictedItem = exposeArray![conflictedItemIndex];
121+
throw Error(
122+
`"${metadata.propertyName ?? ''}" property:
123+
The current decorator (decorator #${
124+
exposeArray!.length
125+
}) conflicts with the decorator #${conflictedItemIndex}.
126+
If the stacked decorators intersect, the name option must be the same.
127+
128+
@Expose(${JSON.stringify(metadata.options || {})})
129+
conflicts with
130+
@Expose(${JSON.stringify(conflictedItem.options || {})})`
131+
);
132+
}
133+
134+
exposeArray?.push(metadata);
44135
}
45136

46137
addExcludeMetadata(metadata: ExcludeMetadata): void {
47138
if (!this._excludeMetadatas.has(metadata.target)) {
48-
this._excludeMetadatas.set(metadata.target, new Map<string, ExcludeMetadata>());
139+
this._excludeMetadatas.set(metadata.target, new Map<string, ExcludeMetadata[]>());
140+
}
141+
if (!this._excludeMetadatas.get(metadata.target).has(metadata.propertyName)) {
142+
this._excludeMetadatas.get(metadata.target).set(metadata.propertyName, []);
49143
}
50-
this._excludeMetadatas.get(metadata.target).set(metadata.propertyName, metadata);
144+
this._excludeMetadatas.get(metadata.target).get(metadata.propertyName).push(metadata);
51145
}
52146

53147
// -------------------------------------------------------------------------
@@ -59,34 +153,30 @@ export class MetadataStorage {
59153
propertyName: string,
60154
transformationType: TransformationType
61155
): TransformMetadata[] {
62-
return this.findMetadatas(this._transformMetadatas, target, propertyName).filter(metadata => {
63-
if (!metadata.options) return true;
64-
if (metadata.options.toClassOnly === true && metadata.options.toPlainOnly === true) return true;
65-
66-
if (metadata.options.toClassOnly === true) {
67-
return (
68-
transformationType === TransformationType.CLASS_TO_CLASS ||
69-
transformationType === TransformationType.PLAIN_TO_CLASS
70-
);
71-
}
72-
if (metadata.options.toPlainOnly === true) {
73-
return transformationType === TransformationType.CLASS_TO_PLAIN;
74-
}
75-
76-
return true;
77-
});
156+
const typeChecker = MetadataStorage.checkMetadataTransformationType.bind(this, transformationType);
157+
return this.findMetadatas(this._transformMetadatas, target, propertyName).filter(typeChecker);
78158
}
79159

80-
findExcludeMetadata(target: Function, propertyName: string): ExcludeMetadata {
81-
return this.findMetadata(this._excludeMetadatas, target, propertyName);
160+
findExcludeMetadatas(
161+
target: Function,
162+
propertyName: string,
163+
transformationType: TransformationType
164+
): ExcludeMetadata[] {
165+
const typeChecker = MetadataStorage.checkMetadataTransformationType.bind(this, transformationType);
166+
return this.findMetadatas(this._excludeMetadatas, target, propertyName).filter(typeChecker);
82167
}
83168

84-
findExposeMetadata(target: Function, propertyName: string): ExposeMetadata {
85-
return this.findMetadata(this._exposeMetadatas, target, propertyName);
169+
findExposeMetadatas(
170+
target: Function,
171+
propertyName: string,
172+
transformationType: TransformationType
173+
): ExposeMetadata[] {
174+
const typeChecker = MetadataStorage.checkMetadataTransformationType.bind(this, transformationType);
175+
return this.findMetadatas(this._exposeMetadatas, target, propertyName).filter(typeChecker);
86176
}
87177

88-
findExposeMetadataByCustomName(target: Function, name: string): ExposeMetadata {
89-
return this.getExposedMetadatas(target).find(metadata => {
178+
findExposeMetadatasByCustomName(target: Function, name: string): ExposeMetadata[] {
179+
return this.getExposedMetadatas(target).filter(metadata => {
90180
return metadata.options && metadata.options.name === name;
91181
});
92182
}
@@ -112,46 +202,25 @@ export class MetadataStorage {
112202
return this.getMetadata(this._excludeMetadatas, target);
113203
}
114204

115-
getExposedProperties(target: Function, transformationType: TransformationType): string[] {
116-
return this.getExposedMetadatas(target)
117-
.filter(metadata => {
118-
if (!metadata.options) return true;
119-
if (metadata.options.toClassOnly === true && metadata.options.toPlainOnly === true) return true;
120-
121-
if (metadata.options.toClassOnly === true) {
122-
return (
123-
transformationType === TransformationType.CLASS_TO_CLASS ||
124-
transformationType === TransformationType.PLAIN_TO_CLASS
125-
);
126-
}
127-
if (metadata.options.toPlainOnly === true) {
128-
return transformationType === TransformationType.CLASS_TO_PLAIN;
129-
}
130-
131-
return true;
132-
})
133-
.map(metadata => metadata.propertyName);
205+
getExposedProperties(
206+
target: Function,
207+
transformationType: TransformationType,
208+
options: { version?: number } = {}
209+
): string[] {
210+
const typeChecker = MetadataStorage.checkMetadataTransformationType.bind(this, transformationType);
211+
const { version } = options;
212+
let array = this.getExposedMetadatas(target).filter(typeChecker);
213+
if (version) {
214+
array = array.filter(metadata => checkVersion(version, metadata?.options?.since, metadata?.options?.until));
215+
}
216+
return array.map(metadata => metadata.propertyName!).filter(onlyUnique);
134217
}
135218

136219
getExcludedProperties(target: Function, transformationType: TransformationType): string[] {
220+
const typeChecker = MetadataStorage.checkMetadataTransformationType.bind(this, transformationType);
137221
return this.getExcludedMetadatas(target)
138-
.filter(metadata => {
139-
if (!metadata.options) return true;
140-
if (metadata.options.toClassOnly === true && metadata.options.toPlainOnly === true) return true;
141-
142-
if (metadata.options.toClassOnly === true) {
143-
return (
144-
transformationType === TransformationType.CLASS_TO_CLASS ||
145-
transformationType === TransformationType.PLAIN_TO_CLASS
146-
);
147-
}
148-
if (metadata.options.toPlainOnly === true) {
149-
return transformationType === TransformationType.CLASS_TO_PLAIN;
150-
}
151-
152-
return true;
153-
})
154-
.map(metadata => metadata.propertyName);
222+
.filter(typeChecker)
223+
.map(metadata => metadata.propertyName!);
155224
}
156225

157226
clear(): void {
@@ -165,26 +234,28 @@ export class MetadataStorage {
165234
// Private Methods
166235
// -------------------------------------------------------------------------
167236

168-
private getMetadata<T extends { target: Function; propertyName: string }>(
169-
metadatas: Map<Function, Map<string, T>>,
237+
private getMetadata<T extends { target: Function; propertyName: string | undefined }>(
238+
metadatas: Map<Function, Map<string, T[]>>,
170239
target: Function
171240
): T[] {
172241
const metadataFromTargetMap = metadatas.get(target);
173-
let metadataFromTarget: T[];
242+
let metadataFromTarget: T[] = [];
174243
if (metadataFromTargetMap) {
175-
metadataFromTarget = Array.from(metadataFromTargetMap.values()).filter(meta => meta.propertyName !== undefined);
244+
metadataFromTarget = flatten(Array.from(metadataFromTargetMap.values())).filter(
245+
meta => meta.propertyName !== undefined
246+
);
176247
}
177248
const metadataFromAncestors: T[] = [];
178249
for (const ancestor of this.getAncestors(target)) {
179250
const ancestorMetadataMap = metadatas.get(ancestor);
180251
if (ancestorMetadataMap) {
181-
const metadataFromAncestor = Array.from(ancestorMetadataMap.values()).filter(
252+
const metadataFromAncestor = flatten(Array.from(ancestorMetadataMap.values())).filter(
182253
meta => meta.propertyName !== undefined
183254
);
184255
metadataFromAncestors.push(...metadataFromAncestor);
185256
}
186257
}
187-
return metadataFromAncestors.concat(metadataFromTarget || []);
258+
return metadataFromAncestors.concat(metadataFromTarget);
188259
}
189260

190261
private findMetadata<T extends { target: Function; propertyName: string }>(
@@ -211,15 +282,15 @@ export class MetadataStorage {
211282
return undefined;
212283
}
213284

214-
private findMetadatas<T extends { target: Function; propertyName: string }>(
285+
private findMetadatas<T extends { target: Function; propertyName: string | undefined }>(
215286
metadatas: Map<Function, Map<string, T[]>>,
216287
target: Function,
217288
propertyName: string
218289
): T[] {
219290
const metadataFromTargetMap = metadatas.get(target);
220-
let metadataFromTarget: T[];
291+
let metadataFromTarget: T[] = [];
221292
if (metadataFromTargetMap) {
222-
metadataFromTarget = metadataFromTargetMap.get(propertyName);
293+
metadataFromTarget = metadataFromTargetMap.get(propertyName) ?? [];
223294
}
224295
const metadataFromAncestorsTarget: T[] = [];
225296
for (const ancestor of this.getAncestors(target)) {
@@ -230,10 +301,7 @@ export class MetadataStorage {
230301
}
231302
}
232303
}
233-
return metadataFromAncestorsTarget
234-
.slice()
235-
.reverse()
236-
.concat((metadataFromTarget || []).slice().reverse());
304+
return metadataFromAncestorsTarget.slice().reverse().concat(metadataFromTarget.slice().reverse());
237305
}
238306

239307
private getAncestors(target: Function): Function[] {

0 commit comments

Comments
 (0)