I want to write a sanitizer decorator which I can put on all user-input string fields. This simply replaces the standard .set(newValue) with .set( sanitize(newValue) ). However I have found the below code only works for one instance. A second instance of the same class ends up sharing the currentValue. After further reading this is actually expected, but I can't work out how to make it per-instance.
import "reflect-metadata";
export const Sanitize = () => {
return (target: any, propertyKey: string | symbol) => {
let currentValue: any = sanitiseString(options, `${target[propertyKey] || ''}`);
Reflect.deleteProperty(target, propertyKey);
Reflect.defineProperty(target, propertyKey, {
get: () => currentValue,
set: (newValue: string) => {
currentValue = sanitiseString(newValue);
},
});
}
}
Edit 1: Minimum reproducible example:
import "reflect-metadata";
const sanitiseString = (valToSanitise: string) => {
// do some stuff, return clean value
return valToSanitise;
}
const Sanitize = () => {
return (target: any, propertyKey: string | symbol) => {
let currentValue: any = sanitiseString(`${target[propertyKey] || ''}`);
Reflect.deleteProperty(target, propertyKey);
Reflect.defineProperty(target, propertyKey, {
get: () => currentValue,
set: (newValue: string) => {
currentValue = sanitiseString(newValue);
},
});
}
}
class UserInput {
constructor(propOne: string, propTwo: string, propThree: number) {
this.propOne = propOne;
this.propTwo = propTwo;
this.propThree = propThree;
}
@Sanitize() propOne: string
@Sanitize() propTwo: string
propThree: number
}
const inputOne = new UserInput('input 1, prop 1', 'input 1, prop 2', 1)
const inputTwo = new UserInput('input 2, prop 1', 'input 2, prop 2', 2)
console.log(inputOne)
console.log(inputTwo)
// expected output:
// [LOG]: UserInput: {
// "propOne": "input 1, prop 1",
// "propTwo": "input 1, prop 2",
// "propThree": 1
// }
// [LOG]: UserInput: {
// "propOne": "input 2, prop 1",
// "propTwo": "input 2, prop 2",
// "propThree": 2
// }
//
// actual output:
//
// [LOG]: UserInput: {
// "propThree": 1
// }
// [LOG]: UserInput: {
// "propThree": 2
// }
// When you remove @Sanitize() the fields appear in console.log. When you add @Sanitize() the fields disappear.
// Further, forcing console.log(inputOne.propOne) returns [LOG]: "input 2, prop 1"
// indicating that the property is being written for the class proto and not per instance
console.log(inputOne.propOne)
The main issue here is that
Sanitize()gets called once per decorated class property declaration, and so for any given class property there will be only onecurrentValue. Meaning that two instances of the class will share the samecurrentValue. If you want to store one value per decorated class property per class instance, then you need access to class instances, and you will have to store the value either in those instances (via a property key that isn't going to interfere with any other properties), or in some map whose key is those instances. In the following I will show how to store the value in the class instances, and to avoid worrying about property name collision I will use thesymboloutput of theSymbolfunction, which is guaranteed to be unique.Also note that
Sanitize()is passed the class prototype as thetargetargument, so any manipulation you perform ontargetwill affect the prototype and not any instance of the class. When you writetarget[propertyKey], you are looking up the property in the class prototype, andstring-valued properties will almost certainly not be set in the prototype. So this is probably not necessary or useful, and we should get rid of it.So if you only have direct access to the class prototype, how do you do anything with class instances? Well, to do this, you should use the
thiscontext of thegetmethod andsetmethod of the accessor property descriptor you pass todefineProperty(). And that meansgetandsetneed to be methods or at leastfunctionexpressions, and not arrow function expressions which have no distinctthiscontext.Okay, enough explanation, here's the code:
And let's make sure it works as you expect. Let's have
sanitiseStringactually do something:And now for our class:
And finally let's see if it works:
Looks good. Each instance of
UserInputhas its own sanitizedpropOneandpropTwoproperties.Playground link to code