-
Notifications
You must be signed in to change notification settings - Fork 103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: component set maps treat encoded and decoded keys as the same #1138
Changes from all commits
ed756b9
9d24d82
acec124
8c4a544
ec7ec72
8f9e7f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
/* | ||
* Copyright (c) 2023, salesforce.com, inc. | ||
* All rights reserved. | ||
* Licensed under the BSD 3-Clause license. | ||
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
*/ | ||
|
||
/** | ||
* This is an extension of the Map class that can match keys whether they are encoded or decoded. | ||
* Decoding the key can solve some edge cases in component fullNames such as Layouts and Profiles. | ||
* See: https://github.com/forcedotcom/cli/issues/1683 | ||
* | ||
* Examples: | ||
* | ||
* Given a map with entries: | ||
* ```javascript | ||
* 'layout#Layout__Broker__c-v1.1 Broker Layout' : {...} | ||
* 'layout#Layout__Broker__c-v9%2E2 Broker Layout' : {...} | ||
* ``` | ||
* | ||
* `decodeableMap.has('layout#Layout__Broker__c-v1%2E1 Broker Layout')` --> returns `true` | ||
* `decodeableMap.has('layout#Layout__Broker__c-v9.2 Broker Layout')` --> returns `true` | ||
*/ | ||
export class DecodeableMap<K extends string, V> extends Map<string, V> { | ||
// Internal map of decoded keys to encoded keys. | ||
// E.g., { 'foo-v1.3 Layout': 'foo-v1%2E3 Layout' } | ||
// This is initialized in the `keysMap` getter due to the constructor calling | ||
// `super` before the initialization happens, and it needs to be initialized | ||
// before `this.set()` is called or `TypeErrors` will be thrown. | ||
private internalkeysMap!: Map<string, string>; | ||
|
||
private get keysMap(): Map<string, string> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if you init at declaration, you also wouldn't have to have a getter for a private prop--you could just use the prop. |
||
if (!this.internalkeysMap) { | ||
this.internalkeysMap = new Map(); | ||
} | ||
return this.internalkeysMap; | ||
} | ||
|
||
/** | ||
* boolean indicating whether an element with the specified key (matching decoded) exists or not. | ||
*/ | ||
public has(key: K): boolean { | ||
return !!this.getExistingKey(key); | ||
} | ||
|
||
/** | ||
* Returns a specified element from the Map object. If the value that is associated to | ||
* the provided key (matching decoded) is an object, then you will get a reference to | ||
* that object and any change made to that object will effectively modify it inside the Map. | ||
*/ | ||
public get(key: K): V | undefined { | ||
const existingKey = this.getExistingKey(key); | ||
return existingKey ? super.get(existingKey) : undefined; | ||
} | ||
|
||
/** | ||
* Adds a new element with a specified key and value to the Map. If an element with the | ||
* same key (encoded or decoded) already exists, the element will be updated. | ||
*/ | ||
public set(key: K, value: V): this { | ||
return super.set(this.getExistingKey(key, true) ?? key, value); | ||
} | ||
|
||
/** | ||
* true if an element in the Map existed (matching encoded or decoded key) and has been | ||
* removed, or false if the element does not exist. | ||
*/ | ||
public delete(key: K): boolean { | ||
const existingKey = this.getExistingKey(key); | ||
return existingKey ? super.delete(existingKey) : false; | ||
} | ||
|
||
// Optimistically looks for an existing key. If the key is not found, detect if the | ||
// key is encoded. If encoded, try using the decoded key. If decoded, look for an | ||
// encoded entry in the internal map to use for the lookup. | ||
private getExistingKey(key: K, setInKeysMap = false): string | undefined { | ||
if (super.has(key)) { | ||
return key; | ||
} else { | ||
const decodedKey = decodeURIComponent(key); | ||
if (key !== decodedKey) { | ||
// The key is encoded; If this is part of a set operation, | ||
// set the { decodedKey : encodedKey } in the internal map. | ||
if (setInKeysMap) { | ||
this.keysMap.set(decodedKey, key); | ||
} | ||
if (super.has(decodedKey)) { | ||
return decodedKey; | ||
} | ||
} else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perf, maybe? you could try checking the keysMap 2nd instead of 3rd to defer the decode operation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It needs to be done either way with the internal Map implementation. |
||
const encodedKey = this.keysMap.get(decodedKey); | ||
if (encodedKey && super.has(encodedKey)) { | ||
return encodedKey; | ||
} | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not init it during declaration?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I originally had the Map initialization done during declaration and it would fail during construction due to the way
super
works.super
must be the first call in a constructor, which means the map init was done after that. So if you construct a new map by passing in a nested array, it will callset()
which needs the internal map but it wasn't initialized until later. To solve this problem I used the getter and conditionally initialized it there.I'll add a comment.