mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
# Backport This will backport the following commits from `main` to `8.18`: - [Add strip unkowns to nested objects in maps, arrays and records (#214978)](https://github.com/elastic/kibana/pull/214978) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Jesus Wahrman","email":"41008968+jesuswr@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-03-19T10:38:31Z","message":"Add strip unkowns to nested objects in maps, arrays and records (#214978)\n\n## Summary\n\nResolves https://github.com/elastic/kibana/issues/210617\n\nAdded strip unkowns to nested objects in map, array and record. Added a\nlot of test cases to cover things like objects inside maps, objects\ninside records, objects inside maps inside records, ...\n\nOne thing to note is that we can't apply `stripUnkowns` to\n`schema.oneOf` since it's using `joi.alternatives` and you can't use it\nthere.\n\n\n### Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [x] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"e14369edabf3d4160dc777e3ab190c8a62aeab7e","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Core","release_note:skip","backport:prev-major","kbn/config-schema","backport:current-major","v9.1.0"],"title":"Add strip unkowns to nested objects in maps, arrays and records","number":214978,"url":"https://github.com/elastic/kibana/pull/214978","mergeCommit":{"message":"Add strip unkowns to nested objects in maps, arrays and records (#214978)\n\n## Summary\n\nResolves https://github.com/elastic/kibana/issues/210617\n\nAdded strip unkowns to nested objects in map, array and record. Added a\nlot of test cases to cover things like objects inside maps, objects\ninside records, objects inside maps inside records, ...\n\nOne thing to note is that we can't apply `stripUnkowns` to\n`schema.oneOf` since it's using `joi.alternatives` and you can't use it\nthere.\n\n\n### Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [x] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"e14369edabf3d4160dc777e3ab190c8a62aeab7e"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/214978","number":214978,"mergeCommit":{"message":"Add strip unkowns to nested objects in maps, arrays and records (#214978)\n\n## Summary\n\nResolves https://github.com/elastic/kibana/issues/210617\n\nAdded strip unkowns to nested objects in map, array and record. Added a\nlot of test cases to cover things like objects inside maps, objects\ninside records, objects inside maps inside records, ...\n\nOne thing to note is that we can't apply `stripUnkowns` to\n`schema.oneOf` since it's using `joi.alternatives` and you can't use it\nthere.\n\n\n### Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [x] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"e14369edabf3d4160dc777e3ab190c8a62aeab7e"}}]}] BACKPORT--> Co-authored-by: Jesus Wahrman <41008968+jesuswr@users.noreply.github.com>
This commit is contained in:
parent
428edb7fc5
commit
b4c5457906
11 changed files with 568 additions and 30 deletions
|
@ -227,6 +227,7 @@ __Options:__
|
|||
* `validate: (value: TValue[]) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details.
|
||||
* `minSize: number` - defines a minimum size the array should have.
|
||||
* `maxSize: number` - defines a maximum size the array should have.
|
||||
* `unknowns: 'ignore' | 'forbid'` - indicates whether unknown properties in nested objects should be ignored or forbidden. It is `forbid` by default unless the global validation option `stripUnknownKeys` is set to `true` when calling `validate()`.
|
||||
|
||||
__Usage:__
|
||||
```typescript
|
||||
|
@ -273,6 +274,7 @@ __Output type:__ `Record<TKey, TValue>`
|
|||
__Options:__
|
||||
* `defaultValue: Record<TKey, TValue> | Reference<Record<TKey, TValue>> | (() => Record<TKey, TValue>)` - defines a default value, see [Default values](#default-values) section for more details.
|
||||
* `validate: (value: Record<TKey, TValue>) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details.
|
||||
* `unknowns: 'ignore' | 'forbid'` - indicates whether unknown properties in nested objects should be ignored or forbidden. It is `forbid` by default unless the global validation option `stripUnknownKeys` is set to `true` when calling `validate()`.
|
||||
|
||||
__Usage:__
|
||||
```typescript
|
||||
|
@ -292,6 +294,7 @@ __Output type:__ `Map<TKey, TValue>`
|
|||
__Options:__
|
||||
* `defaultValue: Map<TKey, TValue> | Reference<Map<TKey, TValue>> | (() => Map<TKey, TValue>)` - defines a default value, see [Default values](#default-values) section for more details.
|
||||
* `validate: (value: Map<TKey, TValue>) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details.
|
||||
* `unknowns: 'ignore' | 'forbid'` - indicates whether unknown properties in nested objects should be ignored or forbidden. It is `forbid` by default unless the global validation option `stripUnknownKeys` is set to `true` when calling `validate()`.
|
||||
|
||||
__Usage:__
|
||||
```typescript
|
||||
|
@ -345,6 +348,7 @@ const valueSchema = schema.oneOf([schema.literal('∞'), schema.number()]);
|
|||
|
||||
__Notes:__
|
||||
* Since the result data type is a type union you should use various TypeScript type guards to get the exact type.
|
||||
* Can't use the `unknowns` option since this is implemented on top of `joi.alternatives()`, and it doesn't accept this option.
|
||||
|
||||
### `schema.any()`
|
||||
|
||||
|
|
|
@ -312,19 +312,25 @@ export const internals: JoiRoot = Joi.extend(
|
|||
method(key, value) {
|
||||
return this.$_addRule({ name: 'entries', args: { key, value } });
|
||||
},
|
||||
validate(value, { error }, args, options) {
|
||||
validate(value, { error, prefs }, args, options) {
|
||||
const result = new Map();
|
||||
for (const [entryKey, entryValue] of value) {
|
||||
let validatedEntryKey: any;
|
||||
try {
|
||||
validatedEntryKey = Joi.attempt(entryKey, args.key, { presence: 'required' });
|
||||
validatedEntryKey = Joi.attempt(entryKey, args.key, {
|
||||
presence: 'required',
|
||||
stripUnknown: prefs.stripUnknown,
|
||||
});
|
||||
} catch (e) {
|
||||
return error('map.key', { entryKey, reason: e });
|
||||
}
|
||||
|
||||
let validatedEntryValue: any;
|
||||
try {
|
||||
validatedEntryValue = Joi.attempt(entryValue, args.value, { presence: 'required' });
|
||||
validatedEntryValue = Joi.attempt(entryValue, args.value, {
|
||||
presence: 'required',
|
||||
stripUnknown: prefs.stripUnknown,
|
||||
});
|
||||
} catch (e) {
|
||||
return error('map.value', { entryKey, reason: e });
|
||||
}
|
||||
|
@ -381,19 +387,25 @@ export const internals: JoiRoot = Joi.extend(
|
|||
method(key, value) {
|
||||
return this.$_addRule({ name: 'entries', args: { key, value } });
|
||||
},
|
||||
validate(value, { error }, args) {
|
||||
validate(value, { error, prefs }, args) {
|
||||
const result = {} as Record<string, any>;
|
||||
for (const [entryKey, entryValue] of Object.entries(value)) {
|
||||
let validatedEntryKey: any;
|
||||
try {
|
||||
validatedEntryKey = Joi.attempt(entryKey, args.key, { presence: 'required' });
|
||||
validatedEntryKey = Joi.attempt(entryKey, args.key, {
|
||||
presence: 'required',
|
||||
stripUnknown: prefs.stripUnknown,
|
||||
});
|
||||
} catch (e) {
|
||||
return error('record.key', { entryKey, reason: e });
|
||||
}
|
||||
|
||||
let validatedEntryValue: any;
|
||||
try {
|
||||
validatedEntryValue = Joi.attempt(entryValue, args.value, { presence: 'required' });
|
||||
validatedEntryValue = Joi.attempt(entryValue, args.value, {
|
||||
presence: 'required',
|
||||
stripUnknown: prefs.stripUnknown,
|
||||
});
|
||||
} catch (e) {
|
||||
return error('record.value', { entryKey, reason: e });
|
||||
}
|
||||
|
|
|
@ -216,3 +216,105 @@ describe('#extendsDeep', () => {
|
|||
).toThrowErrorMatchingInlineSnapshot(`"[0.bar]: definition for this key is missing"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested unknowns with arrays', () => {
|
||||
test('should strip unknown nested keys if stripUnkownKeys is true in validate', () => {
|
||||
const type = schema.arrayOf(
|
||||
schema.object({
|
||||
a: schema.string(),
|
||||
})
|
||||
);
|
||||
|
||||
expect(
|
||||
type.validate(
|
||||
[
|
||||
{ a: '123', b: 'should be stripped' },
|
||||
{ a: '324', x: 'should be stripped' },
|
||||
],
|
||||
void 0,
|
||||
void 0,
|
||||
{
|
||||
stripUnknownKeys: true,
|
||||
}
|
||||
)
|
||||
).toStrictEqual([{ a: '123' }, { a: '324' }]);
|
||||
});
|
||||
|
||||
test('should strip unknown nested keys if unknowns is ignore in the schema', () => {
|
||||
const type = schema.arrayOf(
|
||||
schema.object({
|
||||
a: schema.string(),
|
||||
}),
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
|
||||
expect(
|
||||
type.validate(
|
||||
[
|
||||
{ a: '123', b: 'should be stripped' },
|
||||
{ a: '324', x: 'should be stripped' },
|
||||
],
|
||||
void 0,
|
||||
void 0,
|
||||
{}
|
||||
)
|
||||
).toStrictEqual([{ a: '123' }, { a: '324' }]);
|
||||
});
|
||||
|
||||
test('should strip unknown keys in object inside map inside array when stripUnkownKeys is true', () => {
|
||||
const type = schema.arrayOf(
|
||||
schema.mapOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
a: schema.string(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const value = [
|
||||
new Map([
|
||||
['key1', { a: '123', b: 'should be stripped' }],
|
||||
['key2', { a: '456', extra: 'remove this' }],
|
||||
]),
|
||||
];
|
||||
|
||||
const expected = [
|
||||
new Map([
|
||||
['key1', { a: '123' }],
|
||||
['key2', { a: '456' }],
|
||||
]),
|
||||
];
|
||||
|
||||
expect(type.validate(value, void 0, void 0, { stripUnknownKeys: true })).toStrictEqual(
|
||||
expected
|
||||
);
|
||||
});
|
||||
|
||||
test('should strip unknown keys in object inside map inside array when unknowns is ignore', () => {
|
||||
const type = schema.arrayOf(
|
||||
schema.mapOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
a: schema.string(),
|
||||
})
|
||||
),
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
|
||||
const value = [
|
||||
new Map([
|
||||
['key1', { a: '123', b: 'should be stripped' }],
|
||||
['key2', { a: '456', extra: 'remove this' }],
|
||||
]),
|
||||
];
|
||||
|
||||
const expected = [
|
||||
new Map([
|
||||
['key1', { a: '123' }],
|
||||
['key2', { a: '456' }],
|
||||
]),
|
||||
];
|
||||
|
||||
expect(type.validate(value, void 0, void 0, {})).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,12 +9,13 @@
|
|||
|
||||
import typeDetect from 'type-detect';
|
||||
import { internals } from '../internals';
|
||||
import { Type, TypeOptions, ExtendsDeepOptions } from './type';
|
||||
import { Type, TypeOptions, ExtendsDeepOptions, UnknownOptions } from './type';
|
||||
|
||||
export type ArrayOptions<T> = TypeOptions<T[]> & {
|
||||
minSize?: number;
|
||||
maxSize?: number;
|
||||
};
|
||||
export type ArrayOptions<T> = TypeOptions<T[]> &
|
||||
UnknownOptions & {
|
||||
minSize?: number;
|
||||
maxSize?: number;
|
||||
};
|
||||
|
||||
export class ArrayType<T> extends Type<T[]> {
|
||||
private readonly arrayType: Type<T>;
|
||||
|
@ -31,6 +32,12 @@ export class ArrayType<T> extends Type<T[]> {
|
|||
schema = schema.max(options.maxSize);
|
||||
}
|
||||
|
||||
// Only set stripUnknown if we have an explicit value of unknowns
|
||||
const { unknowns } = options;
|
||||
if (unknowns) {
|
||||
schema = schema.options({ stripUnknown: { objects: unknowns === 'ignore' } });
|
||||
}
|
||||
|
||||
super(schema, options);
|
||||
this.arrayType = type;
|
||||
this.arrayOptions = options;
|
||||
|
|
|
@ -223,3 +223,142 @@ describe('#extendsDeep', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested unknowns', () => {
|
||||
// we don't allow strip unknowns in oneOf for now because joi
|
||||
// doesn't allow it in joi.alternatives and we use that for oneOf
|
||||
test('cant strip unknown keys in oneOf so it should throw an error', () => {
|
||||
const type = schema.mapOf(
|
||||
schema.oneOf([schema.literal('a'), schema.literal('b')]),
|
||||
schema.string()
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
type.validate(
|
||||
{
|
||||
a: 'abc',
|
||||
x: 'def',
|
||||
},
|
||||
void 0,
|
||||
void 0,
|
||||
{ stripUnknownKeys: true }
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[key(\\"x\\")]: types that failed validation:
|
||||
- [0]: expected value to equal [a]
|
||||
- [1]: expected value to equal [b]"
|
||||
`);
|
||||
});
|
||||
|
||||
test('should strip unknown nested keys if stripUnkownKeys is true in validate', () => {
|
||||
const type = schema.mapOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
a: schema.string(),
|
||||
})
|
||||
);
|
||||
|
||||
const value = {
|
||||
x: {
|
||||
a: '123',
|
||||
b: 'should be stripped',
|
||||
},
|
||||
};
|
||||
const expected = new Map([['x', { a: '123' }]]);
|
||||
|
||||
expect(type.validate(value, void 0, void 0, { stripUnknownKeys: true })).toStrictEqual(
|
||||
expected
|
||||
);
|
||||
});
|
||||
|
||||
test('should strip unknown nested keys if unknowns is ignore in the schema', () => {
|
||||
const type = schema.mapOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
a: schema.string(),
|
||||
}),
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
|
||||
const value = {
|
||||
x: {
|
||||
a: '123',
|
||||
b: 'should be stripped',
|
||||
},
|
||||
};
|
||||
const expected = new Map([['x', { a: '123' }]]);
|
||||
|
||||
expect(type.validate(value, void 0, void 0, {})).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
test('should strip unknown keys in object inside record inside map when stripUnkownKeys is true', () => {
|
||||
const type = schema.mapOf(
|
||||
schema.string(),
|
||||
schema.recordOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
a: schema.string(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const value = new Map([
|
||||
[
|
||||
'key1',
|
||||
{
|
||||
record1: { a: '123', b: 'should be stripped' },
|
||||
record2: { a: '456', extra: 'remove this' },
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const expected = new Map([
|
||||
[
|
||||
'key1',
|
||||
{
|
||||
record1: { a: '123' },
|
||||
record2: { a: '456' },
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(type.validate(value, void 0, void 0, { stripUnknownKeys: true })).toStrictEqual(
|
||||
expected
|
||||
);
|
||||
});
|
||||
|
||||
test('should strip unknown keys in object inside record inside map when unkowns is ignore', () => {
|
||||
const type = schema.mapOf(
|
||||
schema.string(),
|
||||
schema.recordOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
a: schema.string(),
|
||||
})
|
||||
),
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
|
||||
const value = new Map([
|
||||
[
|
||||
'key1',
|
||||
{
|
||||
record1: { a: '123', b: 'should be stripped' },
|
||||
record2: { a: '456', extra: 'remove this' },
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const expected = new Map([
|
||||
[
|
||||
'key1',
|
||||
{
|
||||
record1: { a: '123' },
|
||||
record2: { a: '456' },
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(type.validate(value, void 0, void 0, {})).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,9 +11,9 @@ import typeDetect from 'type-detect';
|
|||
import { SchemaTypeError, SchemaTypesError } from '../errors';
|
||||
import { internals } from '../internals';
|
||||
import { META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES } from '../oas_meta_fields';
|
||||
import { Type, TypeOptions, ExtendsDeepOptions } from './type';
|
||||
import { Type, TypeOptions, ExtendsDeepOptions, UnknownOptions } from './type';
|
||||
|
||||
export type MapOfOptions<K, V> = TypeOptions<Map<K, V>>;
|
||||
export type MapOfOptions<K, V> = TypeOptions<Map<K, V>> & UnknownOptions;
|
||||
|
||||
export class MapOfType<K, V> extends Type<Map<K, V>> {
|
||||
private readonly keyType: Type<K>;
|
||||
|
@ -22,13 +22,19 @@ export class MapOfType<K, V> extends Type<Map<K, V>> {
|
|||
|
||||
constructor(keyType: Type<K>, valueType: Type<V>, options: MapOfOptions<K, V> = {}) {
|
||||
const defaultValue = options.defaultValue;
|
||||
const schema = internals
|
||||
let schema = internals
|
||||
.map()
|
||||
.entries(keyType.getSchema(), valueType.getSchema())
|
||||
.meta({
|
||||
[META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES]: () => valueType.getSchema(),
|
||||
});
|
||||
|
||||
// Only set stripUnknown if we have an explicit value of unknowns
|
||||
const { unknowns } = options;
|
||||
if (unknowns) {
|
||||
schema = schema.options({ stripUnknown: { objects: unknowns === 'ignore' } });
|
||||
}
|
||||
|
||||
super(schema, {
|
||||
...options,
|
||||
// Joi clones default values with `Hoek.clone`, and there is bug in cloning
|
||||
|
|
|
@ -551,6 +551,127 @@ describe('nested unknowns', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should strip unknown keys in object inside record inside map inside object when stripUnkownKeys is true', () => {
|
||||
const type = schema.object({
|
||||
rootMap: schema.mapOf(
|
||||
schema.string(),
|
||||
schema.recordOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
a: schema.string(),
|
||||
})
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
const value = {
|
||||
rootMap: new Map([
|
||||
[
|
||||
'key1',
|
||||
{
|
||||
record1: { a: '123', b: 'should be stripped' },
|
||||
record2: { a: '456', extra: 'remove this' },
|
||||
},
|
||||
],
|
||||
]),
|
||||
anotherKey: 'should also be stripped',
|
||||
};
|
||||
|
||||
const expected = {
|
||||
rootMap: new Map([
|
||||
[
|
||||
'key1',
|
||||
{
|
||||
record1: { a: '123' },
|
||||
record2: { a: '456' },
|
||||
},
|
||||
],
|
||||
]),
|
||||
};
|
||||
|
||||
expect(type.validate(value, void 0, void 0, { stripUnknownKeys: true })).toStrictEqual(
|
||||
expected
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should strip unknown keys in object inside record inside map inside object when unknowns is ignore', () => {
|
||||
const type = schema.object(
|
||||
{
|
||||
rootMap: schema.mapOf(
|
||||
schema.string(),
|
||||
schema.recordOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
a: schema.string(),
|
||||
})
|
||||
)
|
||||
),
|
||||
},
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
|
||||
const value = {
|
||||
rootMap: new Map([
|
||||
[
|
||||
'key1',
|
||||
{
|
||||
record1: { a: '123', b: 'should be stripped' },
|
||||
record2: { a: '456', extra: 'remove this' },
|
||||
},
|
||||
],
|
||||
]),
|
||||
anotherKey: 'should also be stripped',
|
||||
};
|
||||
|
||||
const expected = {
|
||||
rootMap: new Map([
|
||||
[
|
||||
'key1',
|
||||
{
|
||||
record1: { a: '123' },
|
||||
record2: { a: '456' },
|
||||
},
|
||||
],
|
||||
]),
|
||||
};
|
||||
|
||||
expect(type.validate(value, void 0, void 0, {})).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
test('should strip unknown keys inside schema.oneOf with stripUnknownKeys for both objects at highest level', () => {
|
||||
const type = schema.oneOf([
|
||||
schema.object({
|
||||
a: schema.string(),
|
||||
}),
|
||||
schema.object({
|
||||
b: schema.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const value1 = {
|
||||
a: 'testA',
|
||||
c: 'should be stripped',
|
||||
};
|
||||
const value2 = {
|
||||
b: 'testB',
|
||||
d: 'should be stripped',
|
||||
};
|
||||
const expected1 = {
|
||||
a: 'testA',
|
||||
};
|
||||
const expected2 = {
|
||||
b: 'testB',
|
||||
};
|
||||
|
||||
expect(type.validate(value1, void 0, void 0, { stripUnknownKeys: true })).toStrictEqual(
|
||||
expected1
|
||||
);
|
||||
|
||||
expect(type.validate(value2, void 0, void 0, { stripUnknownKeys: true })).toStrictEqual(
|
||||
expected2
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
import type { AnySchema } from 'joi';
|
||||
import typeDetect from 'type-detect';
|
||||
import { internals } from '../internals';
|
||||
import { Type, TypeOptions, ExtendsDeepOptions, OptionsForUnknowns } from './type';
|
||||
import { Type, TypeOptions, ExtendsDeepOptions, UnknownOptions } from './type';
|
||||
import { ValidationError } from '../errors';
|
||||
|
||||
export type Props = Record<string, Type<any>>;
|
||||
|
@ -66,10 +66,6 @@ type ExtendedObjectTypeOptions<P extends Props, NP extends NullableProps> = Obje
|
|||
ExtendedProps<P, NP>
|
||||
>;
|
||||
|
||||
interface UnknownOptions {
|
||||
unknowns?: OptionsForUnknowns;
|
||||
}
|
||||
|
||||
interface ObjectTypeOptionsMeta {
|
||||
/**
|
||||
* A string that uniquely identifies this schema. Used when generating OAS
|
||||
|
|
|
@ -63,10 +63,10 @@ test('fails when not receiving expected key type', () => {
|
|||
};
|
||||
|
||||
expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[key(\\"name\\")]: types that failed validation:
|
||||
- [0]: expected value to equal [nickName]
|
||||
- [1]: expected value to equal [lastName]"
|
||||
`);
|
||||
"[key(\\"name\\")]: types that failed validation:
|
||||
- [0]: expected value to equal [nickName]
|
||||
- [1]: expected value to equal [lastName]"
|
||||
`);
|
||||
});
|
||||
|
||||
test('fails after parsing when not receiving expected key type', () => {
|
||||
|
@ -78,10 +78,10 @@ test('fails after parsing when not receiving expected key type', () => {
|
|||
const value = `{"name": "foo"}`;
|
||||
|
||||
expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[key(\\"name\\")]: types that failed validation:
|
||||
- [0]: expected value to equal [nickName]
|
||||
- [1]: expected value to equal [lastName]"
|
||||
`);
|
||||
"[key(\\"name\\")]: types that failed validation:
|
||||
- [0]: expected value to equal [nickName]
|
||||
- [1]: expected value to equal [lastName]"
|
||||
`);
|
||||
});
|
||||
|
||||
test('includes namespace in failure when wrong top-level type', () => {
|
||||
|
@ -211,6 +211,147 @@ describe('#extendsDeep', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('nested unknowns', () => {
|
||||
// we don't allow strip unknowns in oneOf for now because joi
|
||||
// doesn't allow it in joi.alternatives and we use that for oneOf
|
||||
test('cant strip unknown keys in oneOf so it should throw an error', () => {
|
||||
const type = schema.recordOf(
|
||||
schema.oneOf([schema.literal('a'), schema.literal('b')]),
|
||||
schema.string()
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
type.validate(
|
||||
{
|
||||
a: 'abc',
|
||||
x: 'def',
|
||||
},
|
||||
void 0,
|
||||
void 0,
|
||||
{ stripUnknownKeys: true }
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[key(\\"x\\")]: types that failed validation:
|
||||
- [0]: expected value to equal [a]
|
||||
- [1]: expected value to equal [b]"
|
||||
`);
|
||||
});
|
||||
|
||||
test('should strip unknown nested keys if stripUnkownKeys is true in validate', () => {
|
||||
const type = schema.recordOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
a: schema.string(),
|
||||
})
|
||||
);
|
||||
|
||||
expect(
|
||||
type.validate(
|
||||
{
|
||||
x: {
|
||||
a: '123',
|
||||
b: 'should be stripped',
|
||||
},
|
||||
},
|
||||
void 0,
|
||||
void 0,
|
||||
{ stripUnknownKeys: true }
|
||||
)
|
||||
).toStrictEqual({
|
||||
x: {
|
||||
a: '123',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should strip unknown nested keys if unknowns is ignore in the schema', () => {
|
||||
const type = schema.recordOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
a: schema.string(),
|
||||
}),
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
|
||||
expect(
|
||||
type.validate(
|
||||
{
|
||||
x: {
|
||||
a: '123',
|
||||
b: 'should be stripped',
|
||||
},
|
||||
},
|
||||
void 0,
|
||||
void 0,
|
||||
{}
|
||||
)
|
||||
).toStrictEqual({
|
||||
x: {
|
||||
a: '123',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should strip unknown keys in object inside map inside record when stripUnkownKeys is true', () => {
|
||||
const type = schema.recordOf(
|
||||
schema.string(),
|
||||
schema.mapOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
a: schema.string(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const value = {
|
||||
record1: new Map([
|
||||
['key1', { a: '123', b: 'should be stripped' }],
|
||||
['key2', { a: '456', extra: 'remove this' }],
|
||||
]),
|
||||
};
|
||||
|
||||
const expected = {
|
||||
record1: new Map([
|
||||
['key1', { a: '123' }],
|
||||
['key2', { a: '456' }],
|
||||
]),
|
||||
};
|
||||
|
||||
expect(type.validate(value, void 0, void 0, { stripUnknownKeys: true })).toStrictEqual(
|
||||
expected
|
||||
);
|
||||
});
|
||||
|
||||
test('should strip unknown keys in object inside map inside record when unkowns is ignore', () => {
|
||||
const type = schema.recordOf(
|
||||
schema.string(),
|
||||
schema.mapOf(
|
||||
schema.string(),
|
||||
schema.object({
|
||||
a: schema.string(),
|
||||
})
|
||||
),
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
|
||||
const value = {
|
||||
record1: new Map([
|
||||
['key1', { a: '123', b: 'should be stripped' }],
|
||||
['key2', { a: '456', extra: 'remove this' }],
|
||||
]),
|
||||
};
|
||||
|
||||
const expected = {
|
||||
record1: new Map([
|
||||
['key1', { a: '123' }],
|
||||
['key2', { a: '456' }],
|
||||
]),
|
||||
};
|
||||
|
||||
expect(type.validate(value, void 0, void 0, {})).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
test('meta', () => {
|
||||
const stringSchema = schema.string();
|
||||
const type = schema.mapOf(schema.string(), stringSchema);
|
||||
|
|
|
@ -10,10 +10,10 @@
|
|||
import typeDetect from 'type-detect';
|
||||
import { SchemaTypeError, SchemaTypesError } from '../errors';
|
||||
import { internals } from '../internals';
|
||||
import { Type, TypeOptions, ExtendsDeepOptions } from './type';
|
||||
import { Type, TypeOptions, ExtendsDeepOptions, UnknownOptions } from './type';
|
||||
import { META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES } from '../oas_meta_fields';
|
||||
|
||||
export type RecordOfOptions<K extends string, V> = TypeOptions<Record<K, V>>;
|
||||
export type RecordOfOptions<K extends string, V> = TypeOptions<Record<K, V>> & UnknownOptions;
|
||||
|
||||
export class RecordOfType<K extends string, V> extends Type<Record<K, V>> {
|
||||
private readonly keyType: Type<K>;
|
||||
|
@ -21,13 +21,19 @@ export class RecordOfType<K extends string, V> extends Type<Record<K, V>> {
|
|||
private readonly options: RecordOfOptions<K, V>;
|
||||
|
||||
constructor(keyType: Type<K>, valueType: Type<V>, options: RecordOfOptions<K, V> = {}) {
|
||||
const schema = internals
|
||||
let schema = internals
|
||||
.record()
|
||||
.entries(keyType.getSchema(), valueType.getSchema())
|
||||
.meta({
|
||||
[META_FIELD_X_OAS_GET_ADDITIONAL_PROPERTIES]: () => valueType.getSchema(),
|
||||
});
|
||||
|
||||
// Only set stripUnknown if we have an explicit value of unknowns
|
||||
const { unknowns } = options;
|
||||
if (unknowns) {
|
||||
schema = schema.options({ stripUnknown: { objects: unknowns === 'ignore' } });
|
||||
}
|
||||
|
||||
super(schema, options);
|
||||
this.keyType = keyType;
|
||||
this.valueType = valueType;
|
||||
|
|
|
@ -69,6 +69,10 @@ export interface SchemaValidationOptions {
|
|||
*/
|
||||
export type OptionsForUnknowns = 'allow' | 'ignore' | 'forbid';
|
||||
|
||||
export interface UnknownOptions {
|
||||
unknowns?: OptionsForUnknowns;
|
||||
}
|
||||
|
||||
export interface ExtendsDeepOptions {
|
||||
unknowns?: OptionsForUnknowns;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue