TransformOperationExecutor.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. import { defaultMetadataStorage } from './storage';
  2. import { TransformationType } from './enums';
  3. import { getGlobal, isPromise } from './utils';
  4. function instantiateArrayType(arrayType) {
  5. const array = new arrayType();
  6. if (!(array instanceof Set) && !('push' in array)) {
  7. return [];
  8. }
  9. return array;
  10. }
  11. export class TransformOperationExecutor {
  12. // -------------------------------------------------------------------------
  13. // Constructor
  14. // -------------------------------------------------------------------------
  15. constructor(transformationType, options) {
  16. this.transformationType = transformationType;
  17. this.options = options;
  18. // -------------------------------------------------------------------------
  19. // Private Properties
  20. // -------------------------------------------------------------------------
  21. this.recursionStack = new Set();
  22. }
  23. // -------------------------------------------------------------------------
  24. // Public Methods
  25. // -------------------------------------------------------------------------
  26. transform(source, value, targetType, arrayType, isMap, level = 0) {
  27. if (Array.isArray(value) || value instanceof Set) {
  28. const newValue = arrayType && this.transformationType === TransformationType.PLAIN_TO_CLASS
  29. ? instantiateArrayType(arrayType)
  30. : [];
  31. value.forEach((subValue, index) => {
  32. const subSource = source ? source[index] : undefined;
  33. if (!this.options.enableCircularCheck || !this.isCircular(subValue)) {
  34. let realTargetType;
  35. if (typeof targetType !== 'function' &&
  36. targetType &&
  37. targetType.options &&
  38. targetType.options.discriminator &&
  39. targetType.options.discriminator.property &&
  40. targetType.options.discriminator.subTypes) {
  41. if (this.transformationType === TransformationType.PLAIN_TO_CLASS) {
  42. realTargetType = targetType.options.discriminator.subTypes.find(subType => subType.name === subValue[targetType.options.discriminator.property]);
  43. const options = { newObject: newValue, object: subValue, property: undefined };
  44. const newType = targetType.typeFunction(options);
  45. realTargetType === undefined ? (realTargetType = newType) : (realTargetType = realTargetType.value);
  46. if (!targetType.options.keepDiscriminatorProperty)
  47. delete subValue[targetType.options.discriminator.property];
  48. }
  49. if (this.transformationType === TransformationType.CLASS_TO_CLASS) {
  50. realTargetType = subValue.constructor;
  51. }
  52. if (this.transformationType === TransformationType.CLASS_TO_PLAIN) {
  53. subValue[targetType.options.discriminator.property] = targetType.options.discriminator.subTypes.find(subType => subType.value === subValue.constructor).name;
  54. }
  55. }
  56. else {
  57. realTargetType = targetType;
  58. }
  59. const value = this.transform(subSource, subValue, realTargetType, undefined, subValue instanceof Map, level + 1);
  60. if (newValue instanceof Set) {
  61. newValue.add(value);
  62. }
  63. else {
  64. newValue.push(value);
  65. }
  66. }
  67. else if (this.transformationType === TransformationType.CLASS_TO_CLASS) {
  68. if (newValue instanceof Set) {
  69. newValue.add(subValue);
  70. }
  71. else {
  72. newValue.push(subValue);
  73. }
  74. }
  75. });
  76. return newValue;
  77. }
  78. else if (targetType === String && !isMap) {
  79. if (value === null || value === undefined)
  80. return value;
  81. return String(value);
  82. }
  83. else if (targetType === Number && !isMap) {
  84. if (value === null || value === undefined)
  85. return value;
  86. return Number(value);
  87. }
  88. else if (targetType === Boolean && !isMap) {
  89. if (value === null || value === undefined)
  90. return value;
  91. return Boolean(value);
  92. }
  93. else if ((targetType === Date || value instanceof Date) && !isMap) {
  94. if (value instanceof Date) {
  95. return new Date(value.valueOf());
  96. }
  97. if (value === null || value === undefined)
  98. return value;
  99. return new Date(value);
  100. }
  101. else if (!!getGlobal().Buffer && (targetType === Buffer || value instanceof Buffer) && !isMap) {
  102. if (value === null || value === undefined)
  103. return value;
  104. return Buffer.from(value);
  105. }
  106. else if (isPromise(value) && !isMap) {
  107. return new Promise((resolve, reject) => {
  108. value.then((data) => resolve(this.transform(undefined, data, targetType, undefined, undefined, level + 1)), reject);
  109. });
  110. }
  111. else if (!isMap && value !== null && typeof value === 'object' && typeof value.then === 'function') {
  112. // Note: We should not enter this, as promise has been handled above
  113. // This option simply returns the Promise preventing a JS error from happening and should be an inaccessible path.
  114. return value; // skip promise transformation
  115. }
  116. else if (typeof value === 'object' && value !== null) {
  117. // try to guess the type
  118. if (!targetType && value.constructor !== Object /* && TransformationType === TransformationType.CLASS_TO_PLAIN*/)
  119. if (!Array.isArray(value) && value.constructor === Array) {
  120. // Somebody attempts to convert special Array like object to Array, eg:
  121. // const evilObject = { '100000000': '100000000', __proto__: [] };
  122. // This could be used to cause Denial-of-service attack so we don't allow it.
  123. // See prevent-array-bomb.spec.ts for more details.
  124. }
  125. else {
  126. // We are good we can use the built-in constructor
  127. targetType = value.constructor;
  128. }
  129. if (!targetType && source)
  130. targetType = source.constructor;
  131. if (this.options.enableCircularCheck) {
  132. // add transformed type to prevent circular references
  133. this.recursionStack.add(value);
  134. }
  135. const keys = this.getKeys(targetType, value, isMap);
  136. let newValue = source ? source : {};
  137. if (!source &&
  138. (this.transformationType === TransformationType.PLAIN_TO_CLASS ||
  139. this.transformationType === TransformationType.CLASS_TO_CLASS)) {
  140. if (isMap) {
  141. newValue = new Map();
  142. }
  143. else if (targetType) {
  144. newValue = new targetType();
  145. }
  146. else {
  147. newValue = {};
  148. }
  149. }
  150. // traverse over keys
  151. for (const key of keys) {
  152. if (key === '__proto__' || key === 'constructor') {
  153. continue;
  154. }
  155. const valueKey = key;
  156. let newValueKey = key, propertyName = key;
  157. if (!this.options.ignoreDecorators && targetType) {
  158. if (this.transformationType === TransformationType.PLAIN_TO_CLASS) {
  159. const exposeMetadata = defaultMetadataStorage.findExposeMetadataByCustomName(targetType, key);
  160. if (exposeMetadata) {
  161. propertyName = exposeMetadata.propertyName;
  162. newValueKey = exposeMetadata.propertyName;
  163. }
  164. }
  165. else if (this.transformationType === TransformationType.CLASS_TO_PLAIN ||
  166. this.transformationType === TransformationType.CLASS_TO_CLASS) {
  167. const exposeMetadata = defaultMetadataStorage.findExposeMetadata(targetType, key);
  168. if (exposeMetadata && exposeMetadata.options && exposeMetadata.options.name) {
  169. newValueKey = exposeMetadata.options.name;
  170. }
  171. }
  172. }
  173. // get a subvalue
  174. let subValue = undefined;
  175. if (this.transformationType === TransformationType.PLAIN_TO_CLASS) {
  176. /**
  177. * This section is added for the following report:
  178. * https://github.com/typestack/class-transformer/issues/596
  179. *
  180. * We should not call functions or constructors when transforming to class.
  181. */
  182. subValue = value[valueKey];
  183. }
  184. else {
  185. if (value instanceof Map) {
  186. subValue = value.get(valueKey);
  187. }
  188. else if (value[valueKey] instanceof Function) {
  189. subValue = value[valueKey]();
  190. }
  191. else {
  192. subValue = value[valueKey];
  193. }
  194. }
  195. // determine a type
  196. let type = undefined, isSubValueMap = subValue instanceof Map;
  197. if (targetType && isMap) {
  198. type = targetType;
  199. }
  200. else if (targetType) {
  201. const metadata = defaultMetadataStorage.findTypeMetadata(targetType, propertyName);
  202. if (metadata) {
  203. const options = { newObject: newValue, object: value, property: propertyName };
  204. const newType = metadata.typeFunction ? metadata.typeFunction(options) : metadata.reflectedType;
  205. if (metadata.options &&
  206. metadata.options.discriminator &&
  207. metadata.options.discriminator.property &&
  208. metadata.options.discriminator.subTypes) {
  209. if (!(value[valueKey] instanceof Array)) {
  210. if (this.transformationType === TransformationType.PLAIN_TO_CLASS) {
  211. type = metadata.options.discriminator.subTypes.find(subType => {
  212. if (subValue && subValue instanceof Object && metadata.options.discriminator.property in subValue) {
  213. return subType.name === subValue[metadata.options.discriminator.property];
  214. }
  215. });
  216. type === undefined ? (type = newType) : (type = type.value);
  217. if (!metadata.options.keepDiscriminatorProperty) {
  218. if (subValue && subValue instanceof Object && metadata.options.discriminator.property in subValue) {
  219. delete subValue[metadata.options.discriminator.property];
  220. }
  221. }
  222. }
  223. if (this.transformationType === TransformationType.CLASS_TO_CLASS) {
  224. type = subValue.constructor;
  225. }
  226. if (this.transformationType === TransformationType.CLASS_TO_PLAIN) {
  227. if (subValue) {
  228. subValue[metadata.options.discriminator.property] = metadata.options.discriminator.subTypes.find(subType => subType.value === subValue.constructor).name;
  229. }
  230. }
  231. }
  232. else {
  233. type = metadata;
  234. }
  235. }
  236. else {
  237. type = newType;
  238. }
  239. isSubValueMap = isSubValueMap || metadata.reflectedType === Map;
  240. }
  241. else if (this.options.targetMaps) {
  242. // try to find a type in target maps
  243. this.options.targetMaps
  244. .filter(map => map.target === targetType && !!map.properties[propertyName])
  245. .forEach(map => (type = map.properties[propertyName]));
  246. }
  247. else if (this.options.enableImplicitConversion &&
  248. this.transformationType === TransformationType.PLAIN_TO_CLASS) {
  249. // if we have no registererd type via the @Type() decorator then we check if we have any
  250. // type declarations in reflect-metadata (type declaration is emited only if some decorator is added to the property.)
  251. const reflectedType = Reflect.getMetadata('design:type', targetType.prototype, propertyName);
  252. if (reflectedType) {
  253. type = reflectedType;
  254. }
  255. }
  256. }
  257. // if value is an array try to get its custom array type
  258. const arrayType = Array.isArray(value[valueKey])
  259. ? this.getReflectedType(targetType, propertyName)
  260. : undefined;
  261. // const subValueKey = TransformationType === TransformationType.PLAIN_TO_CLASS && newKeyName ? newKeyName : key;
  262. const subSource = source ? source[valueKey] : undefined;
  263. // if its deserialization then type if required
  264. // if we uncomment this types like string[] will not work
  265. // if (this.transformationType === TransformationType.PLAIN_TO_CLASS && !type && subValue instanceof Object && !(subValue instanceof Date))
  266. // throw new Error(`Cannot determine type for ${(targetType as any).name }.${propertyName}, did you forget to specify a @Type?`);
  267. // if newValue is a source object that has method that match newKeyName then skip it
  268. if (newValue.constructor.prototype) {
  269. const descriptor = Object.getOwnPropertyDescriptor(newValue.constructor.prototype, newValueKey);
  270. if ((this.transformationType === TransformationType.PLAIN_TO_CLASS ||
  271. this.transformationType === TransformationType.CLASS_TO_CLASS) &&
  272. // eslint-disable-next-line @typescript-eslint/unbound-method
  273. ((descriptor && !descriptor.set) || newValue[newValueKey] instanceof Function))
  274. // || TransformationType === TransformationType.CLASS_TO_CLASS
  275. continue;
  276. }
  277. if (!this.options.enableCircularCheck || !this.isCircular(subValue)) {
  278. const transformKey = this.transformationType === TransformationType.PLAIN_TO_CLASS ? newValueKey : key;
  279. let finalValue;
  280. if (this.transformationType === TransformationType.CLASS_TO_PLAIN) {
  281. // Get original value
  282. finalValue = value[transformKey];
  283. // Apply custom transformation
  284. finalValue = this.applyCustomTransformations(finalValue, targetType, transformKey, value, this.transformationType);
  285. // If nothing change, it means no custom transformation was applied, so use the subValue.
  286. finalValue = value[transformKey] === finalValue ? subValue : finalValue;
  287. // Apply the default transformation
  288. finalValue = this.transform(subSource, finalValue, type, arrayType, isSubValueMap, level + 1);
  289. }
  290. else {
  291. if (subValue === undefined && this.options.exposeDefaultValues) {
  292. // Set default value if nothing provided
  293. finalValue = newValue[newValueKey];
  294. }
  295. else {
  296. finalValue = this.transform(subSource, subValue, type, arrayType, isSubValueMap, level + 1);
  297. finalValue = this.applyCustomTransformations(finalValue, targetType, transformKey, value, this.transformationType);
  298. }
  299. }
  300. if (finalValue !== undefined || this.options.exposeUnsetFields) {
  301. if (newValue instanceof Map) {
  302. newValue.set(newValueKey, finalValue);
  303. }
  304. else {
  305. newValue[newValueKey] = finalValue;
  306. }
  307. }
  308. }
  309. else if (this.transformationType === TransformationType.CLASS_TO_CLASS) {
  310. let finalValue = subValue;
  311. finalValue = this.applyCustomTransformations(finalValue, targetType, key, value, this.transformationType);
  312. if (finalValue !== undefined || this.options.exposeUnsetFields) {
  313. if (newValue instanceof Map) {
  314. newValue.set(newValueKey, finalValue);
  315. }
  316. else {
  317. newValue[newValueKey] = finalValue;
  318. }
  319. }
  320. }
  321. }
  322. if (this.options.enableCircularCheck) {
  323. this.recursionStack.delete(value);
  324. }
  325. return newValue;
  326. }
  327. else {
  328. return value;
  329. }
  330. }
  331. applyCustomTransformations(value, target, key, obj, transformationType) {
  332. let metadatas = defaultMetadataStorage.findTransformMetadatas(target, key, this.transformationType);
  333. // apply versioning options
  334. if (this.options.version !== undefined) {
  335. metadatas = metadatas.filter(metadata => {
  336. if (!metadata.options)
  337. return true;
  338. return this.checkVersion(metadata.options.since, metadata.options.until);
  339. });
  340. }
  341. // apply grouping options
  342. if (this.options.groups && this.options.groups.length) {
  343. metadatas = metadatas.filter(metadata => {
  344. if (!metadata.options)
  345. return true;
  346. return this.checkGroups(metadata.options.groups);
  347. });
  348. }
  349. else {
  350. metadatas = metadatas.filter(metadata => {
  351. return !metadata.options || !metadata.options.groups || !metadata.options.groups.length;
  352. });
  353. }
  354. metadatas.forEach(metadata => {
  355. value = metadata.transformFn({ value, key, obj, type: transformationType, options: this.options });
  356. });
  357. return value;
  358. }
  359. // preventing circular references
  360. isCircular(object) {
  361. return this.recursionStack.has(object);
  362. }
  363. getReflectedType(target, propertyName) {
  364. if (!target)
  365. return undefined;
  366. const meta = defaultMetadataStorage.findTypeMetadata(target, propertyName);
  367. return meta ? meta.reflectedType : undefined;
  368. }
  369. getKeys(target, object, isMap) {
  370. // determine exclusion strategy
  371. let strategy = defaultMetadataStorage.getStrategy(target);
  372. if (strategy === 'none')
  373. strategy = this.options.strategy || 'exposeAll'; // exposeAll is default strategy
  374. // get all keys that need to expose
  375. let keys = [];
  376. if (strategy === 'exposeAll' || isMap) {
  377. if (object instanceof Map) {
  378. keys = Array.from(object.keys());
  379. }
  380. else {
  381. keys = Object.keys(object);
  382. }
  383. }
  384. if (isMap) {
  385. // expose & exclude do not apply for map keys only to fields
  386. return keys;
  387. }
  388. /**
  389. * If decorators are ignored but we don't want the extraneous values, then we use the
  390. * metadata to decide which property is needed, but doesn't apply the decorator effect.
  391. */
  392. if (this.options.ignoreDecorators && this.options.excludeExtraneousValues && target) {
  393. const exposedProperties = defaultMetadataStorage.getExposedProperties(target, this.transformationType);
  394. const excludedProperties = defaultMetadataStorage.getExcludedProperties(target, this.transformationType);
  395. keys = [...exposedProperties, ...excludedProperties];
  396. }
  397. if (!this.options.ignoreDecorators && target) {
  398. // add all exposed to list of keys
  399. let exposedProperties = defaultMetadataStorage.getExposedProperties(target, this.transformationType);
  400. if (this.transformationType === TransformationType.PLAIN_TO_CLASS) {
  401. exposedProperties = exposedProperties.map(key => {
  402. const exposeMetadata = defaultMetadataStorage.findExposeMetadata(target, key);
  403. if (exposeMetadata && exposeMetadata.options && exposeMetadata.options.name) {
  404. return exposeMetadata.options.name;
  405. }
  406. return key;
  407. });
  408. }
  409. if (this.options.excludeExtraneousValues) {
  410. keys = exposedProperties;
  411. }
  412. else {
  413. keys = keys.concat(exposedProperties);
  414. }
  415. // exclude excluded properties
  416. const excludedProperties = defaultMetadataStorage.getExcludedProperties(target, this.transformationType);
  417. if (excludedProperties.length > 0) {
  418. keys = keys.filter(key => {
  419. return !excludedProperties.includes(key);
  420. });
  421. }
  422. // apply versioning options
  423. if (this.options.version !== undefined) {
  424. keys = keys.filter(key => {
  425. const exposeMetadata = defaultMetadataStorage.findExposeMetadata(target, key);
  426. if (!exposeMetadata || !exposeMetadata.options)
  427. return true;
  428. return this.checkVersion(exposeMetadata.options.since, exposeMetadata.options.until);
  429. });
  430. }
  431. // apply grouping options
  432. if (this.options.groups && this.options.groups.length) {
  433. keys = keys.filter(key => {
  434. const exposeMetadata = defaultMetadataStorage.findExposeMetadata(target, key);
  435. if (!exposeMetadata || !exposeMetadata.options)
  436. return true;
  437. return this.checkGroups(exposeMetadata.options.groups);
  438. });
  439. }
  440. else {
  441. keys = keys.filter(key => {
  442. const exposeMetadata = defaultMetadataStorage.findExposeMetadata(target, key);
  443. return (!exposeMetadata ||
  444. !exposeMetadata.options ||
  445. !exposeMetadata.options.groups ||
  446. !exposeMetadata.options.groups.length);
  447. });
  448. }
  449. }
  450. // exclude prefixed properties
  451. if (this.options.excludePrefixes && this.options.excludePrefixes.length) {
  452. keys = keys.filter(key => this.options.excludePrefixes.every(prefix => {
  453. return key.substr(0, prefix.length) !== prefix;
  454. }));
  455. }
  456. // make sure we have unique keys
  457. keys = keys.filter((key, index, self) => {
  458. return self.indexOf(key) === index;
  459. });
  460. return keys;
  461. }
  462. checkVersion(since, until) {
  463. let decision = true;
  464. if (decision && since)
  465. decision = this.options.version >= since;
  466. if (decision && until)
  467. decision = this.options.version < until;
  468. return decision;
  469. }
  470. checkGroups(groups) {
  471. if (!groups)
  472. return true;
  473. return this.options.groups.some(optionGroup => groups.includes(optionGroup));
  474. }
  475. }
  476. //# sourceMappingURL=TransformOperationExecutor.js.map