plainer.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import { isArray, isEmptyObject, isMap, isPlainObject, isPrimitive, isSet, } from './is.js';
  2. import { escapeKey, stringifyPath } from './pathstringifier.js';
  3. import { isInstanceOfRegisteredClass, transformValue, untransformValue, } from './transformer.js';
  4. import { includes, forEach } from './util.js';
  5. import { parsePath } from './pathstringifier.js';
  6. import { getDeep, setDeep } from './accessDeep.js';
  7. function traverse(tree, walker, origin = []) {
  8. if (!tree) {
  9. return;
  10. }
  11. if (!isArray(tree)) {
  12. forEach(tree, (subtree, key) => traverse(subtree, walker, [...origin, ...parsePath(key)]));
  13. return;
  14. }
  15. const [nodeValue, children] = tree;
  16. if (children) {
  17. forEach(children, (child, key) => {
  18. traverse(child, walker, [...origin, ...parsePath(key)]);
  19. });
  20. }
  21. walker(nodeValue, origin);
  22. }
  23. export function applyValueAnnotations(plain, annotations, superJson) {
  24. traverse(annotations, (type, path) => {
  25. plain = setDeep(plain, path, v => untransformValue(v, type, superJson));
  26. });
  27. return plain;
  28. }
  29. export function applyReferentialEqualityAnnotations(plain, annotations) {
  30. function apply(identicalPaths, path) {
  31. const object = getDeep(plain, parsePath(path));
  32. identicalPaths.map(parsePath).forEach(identicalObjectPath => {
  33. plain = setDeep(plain, identicalObjectPath, () => object);
  34. });
  35. }
  36. if (isArray(annotations)) {
  37. const [root, other] = annotations;
  38. root.forEach(identicalPath => {
  39. plain = setDeep(plain, parsePath(identicalPath), () => plain);
  40. });
  41. if (other) {
  42. forEach(other, apply);
  43. }
  44. }
  45. else {
  46. forEach(annotations, apply);
  47. }
  48. return plain;
  49. }
  50. const isDeep = (object, superJson) => isPlainObject(object) ||
  51. isArray(object) ||
  52. isMap(object) ||
  53. isSet(object) ||
  54. isInstanceOfRegisteredClass(object, superJson);
  55. function addIdentity(object, path, identities) {
  56. const existingSet = identities.get(object);
  57. if (existingSet) {
  58. existingSet.push(path);
  59. }
  60. else {
  61. identities.set(object, [path]);
  62. }
  63. }
  64. export function generateReferentialEqualityAnnotations(identitites, dedupe) {
  65. const result = {};
  66. let rootEqualityPaths = undefined;
  67. identitites.forEach(paths => {
  68. if (paths.length <= 1) {
  69. return;
  70. }
  71. // if we're not deduping, all of these objects continue existing.
  72. // putting the shortest path first makes it easier to parse for humans
  73. // if we're deduping though, only the first entry will still exist, so we can't do this optimisation.
  74. if (!dedupe) {
  75. paths = paths
  76. .map(path => path.map(String))
  77. .sort((a, b) => a.length - b.length);
  78. }
  79. const [representativePath, ...identicalPaths] = paths;
  80. if (representativePath.length === 0) {
  81. rootEqualityPaths = identicalPaths.map(stringifyPath);
  82. }
  83. else {
  84. result[stringifyPath(representativePath)] = identicalPaths.map(stringifyPath);
  85. }
  86. });
  87. if (rootEqualityPaths) {
  88. if (isEmptyObject(result)) {
  89. return [rootEqualityPaths];
  90. }
  91. else {
  92. return [rootEqualityPaths, result];
  93. }
  94. }
  95. else {
  96. return isEmptyObject(result) ? undefined : result;
  97. }
  98. }
  99. export const walker = (object, identities, superJson, dedupe, path = [], objectsInThisPath = [], seenObjects = new Map()) => {
  100. const primitive = isPrimitive(object);
  101. if (!primitive) {
  102. addIdentity(object, path, identities);
  103. const seen = seenObjects.get(object);
  104. if (seen) {
  105. // short-circuit result if we've seen this object before
  106. return dedupe
  107. ? {
  108. transformedValue: null,
  109. }
  110. : seen;
  111. }
  112. }
  113. if (!isDeep(object, superJson)) {
  114. const transformed = transformValue(object, superJson);
  115. const result = transformed
  116. ? {
  117. transformedValue: transformed.value,
  118. annotations: [transformed.type],
  119. }
  120. : {
  121. transformedValue: object,
  122. };
  123. if (!primitive) {
  124. seenObjects.set(object, result);
  125. }
  126. return result;
  127. }
  128. if (includes(objectsInThisPath, object)) {
  129. // prevent circular references
  130. return {
  131. transformedValue: null,
  132. };
  133. }
  134. const transformationResult = transformValue(object, superJson);
  135. const transformed = transformationResult?.value ?? object;
  136. const transformedValue = isArray(transformed) ? [] : {};
  137. const innerAnnotations = {};
  138. forEach(transformed, (value, index) => {
  139. if (index === '__proto__' ||
  140. index === 'constructor' ||
  141. index === 'prototype') {
  142. throw new Error(`Detected property ${index}. This is a prototype pollution risk, please remove it from your object.`);
  143. }
  144. const recursiveResult = walker(value, identities, superJson, dedupe, [...path, index], [...objectsInThisPath, object], seenObjects);
  145. transformedValue[index] = recursiveResult.transformedValue;
  146. if (isArray(recursiveResult.annotations)) {
  147. innerAnnotations[index] = recursiveResult.annotations;
  148. }
  149. else if (isPlainObject(recursiveResult.annotations)) {
  150. forEach(recursiveResult.annotations, (tree, key) => {
  151. innerAnnotations[escapeKey(index) + '.' + key] = tree;
  152. });
  153. }
  154. });
  155. const result = isEmptyObject(innerAnnotations)
  156. ? {
  157. transformedValue,
  158. annotations: !!transformationResult
  159. ? [transformationResult.type]
  160. : undefined,
  161. }
  162. : {
  163. transformedValue,
  164. annotations: !!transformationResult
  165. ? [transformationResult.type, innerAnnotations]
  166. : innerAnnotations,
  167. };
  168. if (!primitive) {
  169. seenObjects.set(object, result);
  170. }
  171. return result;
  172. };
  173. //# sourceMappingURL=plainer.js.map