Strip Any Value
- Dominant language
- JavaScript
- Stars
- 21.2k
- Forks
- 1.5k
- Avg merge
- 4h 57m
- Merged PRs (30d)
- 14
Description
### Runtime
browser
### Runtime version
125
### Module version
17.13.0
### Used with
standalone
### Any other relevant information
_No response_
### What problem are you trying to solve?
I'd like to have a convenient way to remove a key from an object regardless of validation status for that object. This becomes necessary when using `when`, `alter`, `fork`, etc. Any time you want to alter an existing schema to strip a value from a schema - especially when the alteration is because you no longer care what was in it, just that it's removed.
This has been asked many times: https://github.com/hapijs/joi/issues/1385, https://github.com/hapijs/joi/issues/2372, https://github.com/hapijs/joi/issues/2533, https://github.com/hapijs/joi/issues/2423. It's been a source of confusion with no solution that I could find while searching for a few hours today.
The best approach I've come up with so far without new functionality within Joi is `Joi.optional().strip().empty(Joi.any())`. This will override the key by making everything empty so that it is `valid` to be stripped. However, it fails if an `empty` was already applied to the key. If you need to use `empty`, then you have to apply it in the non-stripped case of the conditional so that it doesn't prevent the stripping from working.
-----
For context, what I am doing is trying to strip two keys when they should be hidden due to business logic (two separate fields with specific values). The simple case of the values of the keys being either fully valid or completely empty is simple since you can use `Joi.any().strip()`. It becomes problematic when you have invalid data in the keys you wish to strip.
I've gone through several iterations of solutions as I attempted to work through the problem, and this is the most complete and least verbose one so far. Unfortunately, it is not without its issues. As mentioned, you can't use `empty` outside of the `when` statements, which makes it so you have to nest the `when`s inside of each other in order to be able to apply an `empty` when all the conditions to hide it fail. These solutions become more unwieldy/nested if you end up needing to use more fields ORed against each other to remove a key.
```js
Joi.object({
change: Joi.object({
isImmediateRisk: Joi.boolean(),
nested: Joi.object({
critical: Joi.boolean(),
}),
}).required(),
overallImplementation: Joi.object({ value: Joi.valid(5).required() })
.when('change.isImmediateRisk', {
is: true,
then: Joi.optional().strip().empty(Joi.any()),
otherwise: Joi.when('change.nested.critical', {
is: true,
then: Joi.optional().strip().empty(Joi.any()),
otherwise: Joi.any().empty([null]),
}),
}),
});
```
When this is run with
```js
{
change:{
isImmediateRisk: false,
nested: {
critical: true
}
},
overallImplementation: {}
}
```
it results in
```js
{
"change": {
"isImmediateRisk": false,
"nested": {
"critical": true
}
}
}
```
If you just try the `when`s without nesting them, then it will fail if you apply a `.empty` either to the start or the end of the chain (after the `.when`s). The following will fail with `"overallImplementation.value" is required`, but it will work if you remove the `.empty` unless the `overallImplementation` value is `null`, in which case it will fail if both conditions evaluate to `false`, instead of being `undefined` like I want it to be. (I'm using `presence: 'required'` and `optional` to allow saving as a draft but keeping validations of the actual data itself valid.)
```js
Joi.object({
change: Joi.object({
isImmediateRisk: Joi.boolean(),
nested: Joi.object({
critical: Joi.boolean(),
}),
}).required(),
overallImplementation: Joi.object({ value: Joi.valid(5).required() })
.empty([null])
.when('change.isImmediateRisk', {
is: true,
then: Joi.optional().strip().empty(Joi.any()),
})
.when('change.nested.critical', {
is: true,
then: Joi.optional().strip().empty(Joi.any()),
}),
})
```
Similarly, using `otherwise` in both `when` conditions makes the stripping only work when both conditions evaluate to `true`:
```js
.when('change.isImmediateRisk', {
is: true,
then: Joi.optional().strip().empty(Joi.any()),
otherwise: Joi.any().empty([null])
})
.when('change.nested.critical', {
is: true,
then: Joi.optional().strip().empty(Joi.any()),
otherwise: Joi.any().empty([null])
})
```
### Do you have a new or modified API suggestion to solve the problem?
1. A new Symbol called something akin to `removeKey` that when merged with an object removes the key from the object - allowing `stripUnknown` to work on that key due to it no-longer existing.
2. An `AnySchema.forceStrip()` function or an option passable to `Any.strip(true, {force: true})` that could modify a schema to be removed no matter what.
3. `AnySchema.replace(schema)` which would either act as just the `schema`, but would replace whatever it merges *into* with the `schema`.
```js
// This would remove the key from the resulting schema as a whole
Joi.object({toIgnore: Joi.string()}).concat(Joi.object({toIgnore: Joi.replace(undefined)}))
// This would make `toIgnore` be `Joi.any().strip()`
Joi.object({toIgnore: Joi.string()}).concat(Joi.object({toIgnore: Joi.replace(Joi.any().strip())}))
// This would make `toIgnore` be `Joi.number()`
Joi.object({toIgnore: Joi.string()}).concat(Joi.object({toIgnore: Joi.replace(Joi.number())}))
// Similarly, this would do the exact same thing - The last merged replacement would win out.
Joi.object({toIgnore: Joi.replace(Joi.string())}).concat(Joi.object({toIgnore: Joi.replace(Joi.number())}))
```
Contributor guide
Assessment
This issue has not been assessed yet.