This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
authentik/web/src/common/merge.ts
Ken Sternberg 02ef4365d4 This application fixes the bug with respect to the wizard-level context being updated incorrectly.
Understandings:

- To use uncontrolled inputs, which I prefer, the context object should not be a state or property
  at the level of consumers; it should not automatically re-render with every keystroke, i.e. "The
  React Way."  We're using Web Components, [client-side
  validation](https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation) exists on the
  platform already, and live-validation is problematic for any number of reasons.
- The trade-off is that it is now necessary to re-render the target page of the wizard de-novo, but
  that's not really as big a deal as it sounds. Lit is ready to do that... and then nothing else
  until we request a change-of-page. Excellent.
- The top level context *must* be a state, but it's better if it's a state never actually used by
  the top-level context container. The debate about whether or not to make that container a dumb one
  (`<slot></slot>`) or to merge it with the top-level object continues; here, I've merged it with
  the top-level wizard object, but that object does not refer to the state variable being managed in
  its render pass, so changes to it do not cause a re-render of the whole wizard. The purpose of the
  top-level page is to manage the *steps*, not the *content of any step*. A step may change
  dynamically based on the content of a step, but that's the same thing as *which step*. Lesson:
  always know what your state is *about*.
- Deep merging is a complex subject, but here it's appropriate to our needs.
2023-08-21 12:12:55 -07:00

121 lines
3.5 KiB
TypeScript

/** Taken from: https://github.com/zellwk/javascript/tree/master
*
* We have added some typescript annotations, but this is such a rich feature with deep nesting
* we'll just have to watch it closely for any issues. So far there don't seem to be any.
*
*/
function objectType<T>(value: T) {
return Object.prototype.toString.call(value);
}
// Creates a deep clone for each value
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function cloneDescriptorValue(value: any) {
// Arrays
if (objectType(value) === "[object Array]") {
const array = [];
for (let v of value) {
v = cloneDescriptorValue(v);
array.push(v);
}
return array;
}
// Objects
if (objectType(value) === "[object Object]") {
const obj = {};
const props = Object.keys(value);
for (const prop of props) {
const descriptor = Object.getOwnPropertyDescriptor(value, prop);
if (!descriptor) {
continue;
}
if (descriptor.value) {
descriptor.value = cloneDescriptorValue(descriptor.value);
}
Object.defineProperty(obj, prop, descriptor);
}
return obj;
}
// Other Types of Objects
if (objectType(value) === "[object Date]") {
return new Date(value.getTime());
}
if (objectType(value) === "[object Map]") {
const map = new Map();
for (const entry of value) {
map.set(entry[0], cloneDescriptorValue(entry[1]));
}
return map;
}
if (objectType(value) === "[object Set]") {
const set = new Set();
for (const entry of value.entries()) {
set.add(cloneDescriptorValue(entry[0]));
}
return set;
}
// Types we don't need to clone or cannot clone.
// Examples:
// - Primitives don't need to clone
// - Functions cannot clone
return value;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function _merge(output: Record<string, any>, input: Record<string, any>) {
const props = Object.keys(input);
for (const prop of props) {
// Prevents Prototype Pollution
if (prop === "__proto__") continue;
const descriptor = Object.getOwnPropertyDescriptor(input, prop);
if (!descriptor) {
continue;
}
const value = descriptor.value;
if (value) descriptor.value = cloneDescriptorValue(value);
// If don't have prop => Define property
// [ken@goauthentik] Using `hasOwn` is preferable over
// the basic identity test, according to Typescript.
if (!Object.hasOwn(output, prop)) {
Object.defineProperty(output, prop, descriptor);
continue;
}
// If have prop, but type is not object => Overwrite by redefining property
if (typeof output[prop] !== "object") {
Object.defineProperty(output, prop, descriptor);
continue;
}
// If have prop, but type is Object => Concat the arrays together.
if (objectType(descriptor.value) === "[object Array]") {
output[prop] = output[prop].concat(descriptor.value);
continue;
}
// If have prop, but type is Object => Merge.
_merge(output[prop], descriptor.value);
}
}
export function merge(...sources: Array<object>) {
const result = {};
for (const source of sources) {
_merge(result, source);
}
return result;
}
export default merge;