[Security Solutions] (Phase 1) Copies io-ts shared utilities into kibana/packages (#98999)

## Summary

We are removing duplicated code in sections of plugins into the kibana/packages folder. This is phase 1 of 4+ where:

Phase 1: Lift and shift the io-ts code into `kibana/packages/kbn-securitysolution-io-ts-utils`
Phase 2: Deprecate the utils across plugins any copied code
Phase 3: Replace the deprecated types with the ones in here where we can. [Strangle pattern](https://martinfowler.com/bliki/StranglerFigApplication.html)
Phase 4+: (potentially) consolidating any duplication or everything altogether with the `kbn-io-ts-utils` project

### Checklist

- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
Frank Hassanabad 2021-05-04 13:19:10 -06:00 committed by GitHub
parent 9e2e8b9f19
commit bcde9e2b15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
182 changed files with 8279 additions and 0 deletions

View file

@ -76,6 +76,7 @@ yarn kbn watch-bazel
- @kbn/expect
- @kbn/legacy-logging
- @kbn/logging
- @kbn/securitysolution-io-ts-utils
- @kbn/std
- @kbn/tinymath
- @kbn/utility-types

View file

@ -135,6 +135,7 @@
"@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging/npm_module",
"@kbn/logging": "link:bazel-bin/packages/kbn-logging/npm_module",
"@kbn/monaco": "link:packages/kbn-monaco",
"@kbn/securitysolution-io-ts-utils": "link:bazel-bin/packages/kbn-securitysolution-io-ts-utils/npm_module",
"@kbn/server-http-tools": "link:packages/kbn-server-http-tools",
"@kbn/server-route-repository": "link:packages/kbn-server-route-repository",
"@kbn/std": "link:bazel-bin/packages/kbn-std/npm_module",

View file

@ -18,6 +18,7 @@ filegroup(
"//packages/kbn-expect:build",
"//packages/kbn-legacy-logging:build",
"//packages/kbn-logging:build",
"//packages/kbn-securitysolution-io-ts-utils:build",
"//packages/kbn-std:build",
"//packages/kbn-tinymath:build",
"//packages/kbn-utility-types:build",

View file

@ -0,0 +1,92 @@
load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
PKG_BASE_NAME = "kbn-securitysolution-io-ts-utils"
PKG_REQUIRE_NAME = "@kbn/securitysolution-io-ts-utils"
SOURCE_FILES = glob(
[
"src/**/*.ts",
],
exclude = [
"**/*.test.*",
"**/*.mock.*"
],
)
SRCS = SOURCE_FILES
filegroup(
name = "srcs",
srcs = SRCS,
)
NPM_MODULE_EXTRA_FILES = [
"package.json",
"README.md",
]
SRC_DEPS = [
"//packages/elastic-datemath",
"@npm//fp-ts",
"@npm//io-ts",
"@npm//lodash",
"@npm//moment",
"@npm//tslib",
"@npm//uuid",
]
TYPES_DEPS = [
"@npm//@types/flot",
"@npm//@types/jest",
"@npm//@types/lodash",
"@npm//@types/node",
"@npm//@types/uuid"
]
DEPS = SRC_DEPS + TYPES_DEPS
ts_config(
name = "tsconfig",
src = "tsconfig.json",
deps = [
"//:tsconfig.base.json",
],
)
ts_project(
name = "tsc",
args = ['--pretty'],
srcs = SRCS,
deps = DEPS,
declaration = True,
declaration_map = True,
incremental = True,
out_dir = "target",
source_map = True,
root_dir = "src",
tsconfig = ":tsconfig",
)
js_library(
name = PKG_BASE_NAME,
srcs = NPM_MODULE_EXTRA_FILES,
deps = [":tsc"] + DEPS,
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)
pkg_npm(
name = "npm_module",
deps = [
":%s" % PKG_BASE_NAME,
]
)
filegroup(
name = "build",
srcs = [
":npm_module",
],
visibility = ["//visibility:public"],
)

View file

@ -0,0 +1,10 @@
# kbn-securitysolution-io-ts-utils
Temporary location for all the io-ts-utils from security solutions. This is a lift-and-shift, where
we are moving them here for phase 1.
Phase 2 is deprecating across plugins any copied code or sharing of io-ts utils that are now in here.
Phase 3 is replacing those deprecated types with the ones in here.
Phase 4+ is (potentially) consolidating any duplication or everything altogether with the `kbn-io-ts-utils` project

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-securitysolution-io-ts-utils'],
};

View file

@ -0,0 +1,9 @@
{
"name": "@kbn/securitysolution-io-ts-utils",
"version": "1.0.0",
"description": "io ts utilities and types to be shared with plugins from the security solution project",
"license": "SSPL-1.0 OR Elastic License 2.0",
"main": "./target/index.js",
"types": "./target/index.d.ts",
"private": true
}

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import * as t from 'io-ts';
import { saved_object_attributes } from '../saved_object_attributes';
/**
* Params is an "object", since it is a type of AlertActionParams which is action templates.
* @see x-pack/plugins/alerting/common/alert.ts
*/
export const action_group = t.string;
export const action_id = t.string;
export const action_action_type_id = t.string;
export const action_params = saved_object_attributes;
export const action = t.exact(
t.type({
group: action_group,
id: action_id,
action_type_id: action_action_type_id,
params: action_params,
})
);
export const actions = t.array(action);
export type Actions = t.TypeOf<typeof actions>;
export const actionsCamel = t.array(
t.exact(
t.type({
group: action_group,
id: action_id,
actionTypeId: action_action_type_id,
params: action_params,
})
)
);
export type ActionsCamel = t.TypeOf<typeof actions>;

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const ENTRY_VALUE = 'some host name';
export const FIELD = 'host.name';
export const MATCH = 'match';
export const MATCH_ANY = 'match_any';
export const OPERATOR = 'included';
export const NESTED = 'nested';
export const NESTED_FIELD = 'parent.field';
export const LIST_ID = 'some-list-id';
export const LIST = 'list';
export const TYPE = 'ip';
export const EXISTS = 'exists';
export const WILDCARD = 'wildcard';
export const USER = 'some user';
export const DATE_NOW = '2020-04-20T15:25:31.830Z';
// Exception List specific
export const ID = 'uuid_here';

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/**
* This ID is used for _both_ the Saved Object ID and for the list_id
* for the single global space agnostic endpoint list
* TODO: Create a kbn-securitysolution-constants and add this to it.
* @deprecated Use the ENDPOINT_LIST_ID from the kbn-securitysolution-constants.
*/
export const ENDPOINT_LIST_ID = 'endpoint_list';
/**
* TODO: Create a kbn-securitysolution-constants and add this to it.
* @deprecated Use the DEFAULT_MAX_SIGNALS from the kbn-securitysolution-constants.
*/
export const DEFAULT_MAX_SIGNALS = 100;

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import * as t from 'io-ts';
export const created_at = t.string; // TODO: Make this into an ISO Date string check

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import * as t from 'io-ts';
export const created_by = t.string;

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultVersionNumber } from '../default_version_number';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_version_number', () => {
test('it should validate a version number', () => {
const payload = 5;
const decoded = DefaultVersionNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a 0', () => {
const payload = 0;
const decoded = DefaultVersionNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "0" supplied to "DefaultVersionNumber"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate a -1', () => {
const payload = -1;
const decoded = DefaultVersionNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "-1" supplied to "DefaultVersionNumber"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate a string', () => {
const payload = '5';
const decoded = DefaultVersionNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultVersionNumber"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default of 1', () => {
const payload = null;
const decoded = DefaultVersionNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(1);
});
});

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { version, Version } from '../version';
/**
* Types the DefaultVersionNumber as:
* - If null or undefined, then a default of the number 1 will be used
*/
export const DefaultVersionNumber = new t.Type<Version, Version | undefined, unknown>(
'DefaultVersionNumber',
version.is,
(input, context): Either<t.Errors, Version> =>
input == null ? t.success(1) : version.validate(input, context),
t.identity
);
export type DefaultVersionNumberDecoded = t.TypeOf<typeof DefaultVersionNumber>;

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { actions, Actions } from '../actions';
export const DefaultActionsArray = new t.Type<Actions, Actions | undefined, unknown>(
'DefaultActionsArray',
actions.is,
(input, context): Either<t.Errors, Actions> =>
input == null ? t.success([]) : actions.validate(input, context),
t.identity
);

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultArray } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
const testSchema = t.keyof({
valid: true,
also_valid: true,
});
type TestSchema = t.TypeOf<typeof testSchema>;
const defaultArraySchema = DefaultArray(testSchema);
describe('default_array', () => {
test('it should validate an empty array', () => {
const payload: string[] = [];
const decoded = defaultArraySchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of testSchema', () => {
const payload: TestSchema[] = ['valid'];
const decoded = defaultArraySchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of valid testSchema strings', () => {
const payload = ['valid', 'also_valid'];
const decoded = defaultArraySchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate an array with a number', () => {
const payload = ['valid', 123];
const decoded = defaultArraySchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "123" supplied to "DefaultArray"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate an array with an invalid string', () => {
const payload = ['valid', 'invalid'];
const decoded = defaultArraySchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "invalid" supplied to "DefaultArray"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default array entry', () => {
const payload = null;
const decoded = defaultArraySchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([]);
});
});

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the DefaultArray<C> as:
* - If undefined, then a default array will be set
* - If an array is sent in, then the array will be validated to ensure all elements are type C
*/
export const DefaultArray = <C extends t.Mixed>(codec: C) => {
const arrType = t.array(codec);
type ArrType = t.TypeOf<typeof arrType>;
return new t.Type<ArrType, ArrType | undefined, unknown>(
'DefaultArray',
arrType.is,
(input, context): Either<t.Errors, ArrType> =>
input == null ? t.success([]) : arrType.validate(input, context),
t.identity
);
};

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultBooleanFalse } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_boolean_false', () => {
test('it should validate a boolean false', () => {
const payload = false;
const decoded = DefaultBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate a boolean true', () => {
const payload = true;
const decoded = DefaultBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultBooleanFalse"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default false', () => {
const payload = null;
const decoded = DefaultBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(false);
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the DefaultBooleanFalse as:
* - If null or undefined, then a default false will be set
*/
export const DefaultBooleanFalse = new t.Type<boolean, boolean | undefined, unknown>(
'DefaultBooleanFalse',
t.boolean.is,
(input, context): Either<t.Errors, boolean> =>
input == null ? t.success(false) : t.boolean.validate(input, context),
t.identity
);

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultBooleanTrue } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_boolean_true', () => {
test('it should validate a boolean false', () => {
const payload = false;
const decoded = DefaultBooleanTrue.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate a boolean true', () => {
const payload = true;
const decoded = DefaultBooleanTrue.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultBooleanTrue.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultBooleanTrue"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default true', () => {
const payload = null;
const decoded = DefaultBooleanTrue.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(true);
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the DefaultBooleanTrue as:
* - If null or undefined, then a default true will be set
*/
export const DefaultBooleanTrue = new t.Type<boolean, boolean | undefined, unknown>(
'DefaultBooleanTrue',
t.boolean.is,
(input, context): Either<t.Errors, boolean> =>
input == null ? t.success(true) : t.boolean.validate(input, context),
t.identity
);

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultEmptyString } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_empty_string', () => {
test('it should validate a regular string', () => {
const payload = 'some string';
const decoded = DefaultEmptyString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultEmptyString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultEmptyString"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default of ""', () => {
const payload = null;
const decoded = DefaultEmptyString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual('');
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the DefaultEmptyString as:
* - If null or undefined, then a default of an empty string "" will be used
*/
export const DefaultEmptyString = new t.Type<string, string | undefined, unknown>(
'DefaultEmptyString',
t.string.is,
(input, context): Either<t.Errors, string> =>
input == null ? t.success('') : t.string.validate(input, context),
t.identity
);

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultExportFileName } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_export_file_name', () => {
test('it should validate a regular string', () => {
const payload = 'some string';
const decoded = DefaultExportFileName.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultExportFileName.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultExportFileName"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default of "export.ndjson"', () => {
const payload = null;
const decoded = DefaultExportFileName.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual('export.ndjson');
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the DefaultExportFileName as:
* - If null or undefined, then a default of "export.ndjson" will be used
*/
export const DefaultExportFileName = new t.Type<string, string | undefined, unknown>(
'DefaultExportFileName',
t.string.is,
(input, context): Either<t.Errors, string> =>
input == null ? t.success('export.ndjson') : t.string.validate(input, context),
t.identity
);

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultFromString } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_from_string', () => {
test('it should validate a from string', () => {
const payload = 'now-20m';
const decoded = DefaultFromString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultFromString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultFromString"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default of "now-6m"', () => {
const payload = null;
const decoded = DefaultFromString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual('now-6m');
});
});

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { from } from '../from';
/**
* Types the DefaultFromString as:
* - If null or undefined, then a default of the string "now-6m" will be used
*/
export const DefaultFromString = new t.Type<string, string | undefined, unknown>(
'DefaultFromString',
t.string.is,
(input, context): Either<t.Errors, string> => {
if (input == null) {
return t.success('now-6m');
}
return from.validate(input, context);
},
t.identity
);

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultIntervalString } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_interval_string', () => {
test('it should validate a interval string', () => {
const payload = '20m';
const decoded = DefaultIntervalString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultIntervalString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultIntervalString"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default of "5m"', () => {
const payload = null;
const decoded = DefaultIntervalString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual('5m');
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the DefaultIntervalString as:
* - If null or undefined, then a default of the string "5m" will be used
*/
export const DefaultIntervalString = new t.Type<string, string | undefined, unknown>(
'DefaultIntervalString',
t.string.is,
(input, context): Either<t.Errors, string> =>
input == null ? t.success('5m') : t.string.validate(input, context),
t.identity
);

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { Language } from '../language';
import { DefaultLanguageString } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_language_string', () => {
test('it should validate a string', () => {
const payload: Language = 'lucene';
const decoded = DefaultLanguageString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultLanguageString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultLanguageString"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default of "kuery"', () => {
const payload = null;
const decoded = DefaultLanguageString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual('kuery');
});
});

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { language } from '../language';
/**
* Types the DefaultLanguageString as:
* - If null or undefined, then a default of the string "kuery" will be used
*/
export const DefaultLanguageString = new t.Type<string, string | undefined, unknown>(
'DefaultLanguageString',
t.string.is,
(input, context): Either<t.Errors, string> =>
input == null ? t.success('kuery') : language.validate(input, context),
t.identity
);

View file

@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultMaxSignalsNumber } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
import { DEFAULT_MAX_SIGNALS } from '../constants';
describe('default_from_string', () => {
test('it should validate a max signal number', () => {
const payload = 5;
const decoded = DefaultMaxSignalsNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a string', () => {
const payload = '5';
const decoded = DefaultMaxSignalsNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultMaxSignals"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate a zero', () => {
const payload = 0;
const decoded = DefaultMaxSignalsNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "0" supplied to "DefaultMaxSignals"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate a negative number', () => {
const payload = -1;
const decoded = DefaultMaxSignalsNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "-1" supplied to "DefaultMaxSignals"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default of DEFAULT_MAX_SIGNALS', () => {
const payload = null;
const decoded = DefaultMaxSignalsNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(DEFAULT_MAX_SIGNALS);
});
});

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { max_signals } from '../max_signals';
import { DEFAULT_MAX_SIGNALS } from '../constants';
/**
* Types the default max signal:
* - Natural Number (positive integer and not a float),
* - greater than 1
* - If undefined then it will use DEFAULT_MAX_SIGNALS (100) as the default
*/
export const DefaultMaxSignalsNumber = new t.Type<number, number | undefined, unknown>(
'DefaultMaxSignals',
t.number.is,
(input, context): Either<t.Errors, number> => {
return input == null ? t.success(DEFAULT_MAX_SIGNALS) : max_signals.validate(input, context);
},
t.identity
);

View file

@ -0,0 +1,85 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultPage } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_page', () => {
test('it should validate a regular number greater than zero', () => {
const payload = 5;
const decoded = DefaultPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate a string of a number', () => {
const payload = '5';
const decoded = DefaultPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(5);
});
test('it should not validate a junk string', () => {
const payload = 'invalid-string';
const decoded = DefaultPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "NaN" supplied to "DefaultPerPage"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate an empty string', () => {
const payload = '';
const decoded = DefaultPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "NaN" supplied to "DefaultPerPage"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate a zero', () => {
const payload = 0;
const decoded = DefaultPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "0" supplied to "DefaultPerPage"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate a negative number', () => {
const payload = -1;
const decoded = DefaultPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "-1" supplied to "DefaultPerPage"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default of 20', () => {
const payload = null;
const decoded = DefaultPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(1);
});
});

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero';
/**
* Types the DefaultPerPage as:
* - If a string this will convert the string to a number
* - If null or undefined, then a default of 1 will be used
* - If the number is 0 or less this will not validate as it has to be a positive number greater than zero
*/
export const DefaultPage = new t.Type<number, number | undefined, unknown>(
'DefaultPerPage',
t.number.is,
(input, context): Either<t.Errors, number> => {
if (input == null) {
return t.success(1);
} else if (typeof input === 'string') {
return PositiveIntegerGreaterThanZero.validate(parseInt(input, 10), context);
} else {
return PositiveIntegerGreaterThanZero.validate(input, context);
}
},
t.identity
);

View file

@ -0,0 +1,85 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultPerPage } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_per_page', () => {
test('it should validate a regular number greater than zero', () => {
const payload = 5;
const decoded = DefaultPerPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate a string of a number', () => {
const payload = '5';
const decoded = DefaultPerPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(5);
});
test('it should not validate a junk string', () => {
const payload = 'invalid-string';
const decoded = DefaultPerPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "NaN" supplied to "DefaultPerPage"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate an empty string', () => {
const payload = '';
const decoded = DefaultPerPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "NaN" supplied to "DefaultPerPage"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate a zero', () => {
const payload = 0;
const decoded = DefaultPerPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "0" supplied to "DefaultPerPage"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate a negative number', () => {
const payload = -1;
const decoded = DefaultPerPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "-1" supplied to "DefaultPerPage"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default of 20', () => {
const payload = null;
const decoded = DefaultPerPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(20);
});
});

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero';
/**
* Types the DefaultPerPage as:
* - If a string this will convert the string to a number
* - If null or undefined, then a default of 20 will be used
* - If the number is 0 or less this will not validate as it has to be a positive number greater than zero
*/
export const DefaultPerPage = new t.Type<number, number | undefined, unknown>(
'DefaultPerPage',
t.number.is,
(input, context): Either<t.Errors, number> => {
if (input == null) {
return t.success(20);
} else if (typeof input === 'string') {
return PositiveIntegerGreaterThanZero.validate(parseInt(input, 10), context);
} else {
return PositiveIntegerGreaterThanZero.validate(input, context);
}
},
t.identity
);

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { RiskScoreMapping, risk_score_mapping } from '../risk_score_mapping';
/**
* Types the DefaultStringArray as:
* - If null or undefined, then a default risk_score_mapping array will be set
*/
export const DefaultRiskScoreMappingArray = new t.Type<
RiskScoreMapping,
RiskScoreMapping | undefined,
unknown
>(
'DefaultRiskScoreMappingArray',
risk_score_mapping.is,
(input, context): Either<t.Errors, RiskScoreMapping> =>
input == null ? t.success([]) : risk_score_mapping.validate(input, context),
t.identity
);

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { SeverityMapping, severity_mapping } from '../severity_mapping';
/**
* Types the DefaultStringArray as:
* - If null or undefined, then a default severity_mapping array will be set
*/
export const DefaultSeverityMappingArray = new t.Type<
SeverityMapping,
SeverityMapping | undefined,
unknown
>(
'DefaultSeverityMappingArray',
severity_mapping.is,
(input, context): Either<t.Errors, SeverityMapping> =>
input == null ? t.success([]) : severity_mapping.validate(input, context),
t.identity
);

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultStringArray } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_string_array', () => {
test('it should validate an empty array', () => {
const payload: string[] = [];
const decoded = DefaultStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of strings', () => {
const payload = ['value 1', 'value 2'];
const decoded = DefaultStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate an array with a number', () => {
const payload = ['value 1', 5];
const decoded = DefaultStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultStringArray"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default array entry', () => {
const payload = null;
const decoded = DefaultStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([]);
});
});

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the DefaultStringArray as:
* - If undefined, then a default array will be set
* - If an array is sent in, then the array will be validated to ensure all elements are a string
*/
export const DefaultStringArray = new t.Type<string[], string[] | undefined, unknown>(
'DefaultStringArray',
t.array(t.string).is,
(input, context): Either<t.Errors, string[]> =>
input == null ? t.success([]) : t.array(t.string).validate(input, context),
t.identity
);

View file

@ -0,0 +1,101 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultStringBooleanFalse } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_string_boolean_false', () => {
test('it should validate a boolean false', () => {
const payload = false;
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate a boolean true', () => {
const payload = true;
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultStringBooleanFalse"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default false', () => {
const payload = null;
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(false);
});
test('it should return a default false when given a string of "false"', () => {
const payload = 'false';
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(false);
});
test('it should return a default true when given a string of "true"', () => {
const payload = 'true';
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(true);
});
test('it should return a default true when given a string of "TruE"', () => {
const payload = 'TruE';
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(true);
});
test('it should not work with a string of junk "junk"', () => {
const payload = 'junk';
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "junk" supplied to "DefaultStringBooleanFalse"',
]);
expect(message.schema).toEqual({});
});
test('it should not work with an empty string', () => {
const payload = '';
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "" supplied to "DefaultStringBooleanFalse"',
]);
expect(message.schema).toEqual({});
});
});

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the DefaultStringBooleanFalse as:
* - If a string this will convert the string to a boolean
* - If null or undefined, then a default false will be set
*/
export const DefaultStringBooleanFalse = new t.Type<boolean, boolean | undefined | string, unknown>(
'DefaultStringBooleanFalse',
t.boolean.is,
(input, context): Either<t.Errors, boolean> => {
if (input == null) {
return t.success(false);
} else if (typeof input === 'string' && input.toLowerCase() === 'true') {
return t.success(true);
} else if (typeof input === 'string' && input.toLowerCase() === 'false') {
return t.success(false);
} else {
return t.boolean.validate(input, context);
}
},
t.identity
);
export type DefaultStringBooleanFalseC = typeof DefaultStringBooleanFalse;

View file

@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { Threats } from '../threat';
import { DefaultThreatArray } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_threat_null', () => {
test('it should validate an empty array', () => {
const payload: Threats = [];
const decoded = DefaultThreatArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of threats', () => {
const payload: Threats = [
{
framework: 'MITRE ATTACK',
technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }],
tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' },
},
];
const decoded = DefaultThreatArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate an array with a number', () => {
const payload = [
{
framework: 'MITRE ATTACK',
technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }],
tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' },
},
5,
];
const decoded = DefaultThreatArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultThreatArray"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default empty array if not provided a value', () => {
const payload = null;
const decoded = DefaultThreatArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([]);
});
});

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { threats, Threats } from '../threat';
/**
* Types the DefaultThreatArray as:
* - If null or undefined, then an empty array will be set
*/
export const DefaultThreatArray = new t.Type<Threats, Threats | undefined, unknown>(
'DefaultThreatArray',
threats.is,
(input, context): Either<t.Errors, Threats> =>
input == null ? t.success([]) : threats.validate(input, context),
t.identity
);

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { Throttle } from '../throttle';
import { DefaultThrottleNull } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_throttle_null', () => {
test('it should validate a throttle string', () => {
const payload: Throttle = 'some string';
const decoded = DefaultThrottleNull.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate an array with a number', () => {
const payload = 5;
const decoded = DefaultThrottleNull.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultThreatNull"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default "null" if not provided a value', () => {
const payload = undefined;
const decoded = DefaultThrottleNull.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(null);
});
});

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { throttle, ThrottleOrNull } from '../throttle';
/**
* Types the DefaultThrottleNull as:
* - If null or undefined, then a null will be set
*/
export const DefaultThrottleNull = new t.Type<ThrottleOrNull, ThrottleOrNull | undefined, unknown>(
'DefaultThreatNull',
throttle.is,
(input, context): Either<t.Errors, ThrottleOrNull> =>
input == null ? t.success(null) : throttle.validate(input, context),
t.identity
);

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultToString } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_to_string', () => {
test('it should validate a to string', () => {
const payload = 'now-5m';
const decoded = DefaultToString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultToString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultToString"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default of "now"', () => {
const payload = null;
const decoded = DefaultToString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual('now');
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the DefaultToString as:
* - If null or undefined, then a default of the string "now" will be used
*/
export const DefaultToString = new t.Type<string, string | undefined, unknown>(
'DefaultToString',
t.string.is,
(input, context): Either<t.Errors, string> =>
input == null ? t.success('now') : t.string.validate(input, context),
t.identity
);

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultUuid } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_uuid', () => {
test('it should validate a regular string', () => {
const payload = '1';
const decoded = DefaultUuid.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultUuid.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "DefaultUuid"']);
expect(message.schema).toEqual({});
});
test('it should return a default of a uuid', () => {
const payload = null;
const decoded = DefaultUuid.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i
);
});
});

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import uuid from 'uuid';
import { NonEmptyString } from '../non_empty_string';
/**
* Types the DefaultUuid as:
* - If null or undefined, then a default string uuid.v4() will be
* created otherwise it will be checked just against an empty string
*/
export const DefaultUuid = new t.Type<string, string | undefined, unknown>(
'DefaultUuid',
t.string.is,
(input, context): Either<t.Errors, string> =>
input == null ? t.success(uuid.v4()) : NonEmptyString.validate(input, context),
t.identity
);

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultVersionNumber } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('default_version_number', () => {
test('it should validate a version number', () => {
const payload = 5;
const decoded = DefaultVersionNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a 0', () => {
const payload = 0;
const decoded = DefaultVersionNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "0" supplied to "DefaultVersionNumber"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate a -1', () => {
const payload = -1;
const decoded = DefaultVersionNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "-1" supplied to "DefaultVersionNumber"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate a string', () => {
const payload = '5';
const decoded = DefaultVersionNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultVersionNumber"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default of 1', () => {
const payload = null;
const decoded = DefaultVersionNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(1);
});
});

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { version, Version } from '../version';
/**
* Types the DefaultVersionNumber as:
* - If null or undefined, then a default of the number 1 will be used
*/
export const DefaultVersionNumber = new t.Type<Version, Version | undefined, unknown>(
'DefaultVersionNumber',
version.is,
(input, context): Either<t.Errors, Version> =>
input == null ? t.success(1) : version.validate(input, context),
t.identity
);
export type DefaultVersionNumberDecoded = t.TypeOf<typeof DefaultVersionNumber>;

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
export const description = t.string;
export type Description = t.TypeOf<typeof description>;
export const descriptionOrUndefined = t.union([description, t.undefined]);
export type DescriptionOrUndefined = t.TypeOf<typeof descriptionOrUndefined>;

View file

@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { EmptyStringArray, EmptyStringArrayEncoded } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('empty_string_array', () => {
test('it should validate "null" and create an empty array', () => {
const payload: EmptyStringArrayEncoded = null;
const decoded = EmptyStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([]);
});
test('it should validate "undefined" and create an empty array', () => {
const payload: EmptyStringArrayEncoded = undefined;
const decoded = EmptyStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([]);
});
test('it should validate a single value of "a" into an array of size 1 of ["a"]', () => {
const payload: EmptyStringArrayEncoded = 'a';
const decoded = EmptyStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(['a']);
});
test('it should validate 2 values of "a,b" into an array of size 2 of ["a", "b"]', () => {
const payload: EmptyStringArrayEncoded = 'a,b';
const decoded = EmptyStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(['a', 'b']);
});
test('it should validate 3 values of "a,b,c" into an array of size 3 of ["a", "b", "c"]', () => {
const payload: EmptyStringArrayEncoded = 'a,b,c';
const decoded = EmptyStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(['a', 'b', 'c']);
});
test('it should FAIL validation of number', () => {
const payload: number = 5;
const decoded = EmptyStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "EmptyStringArray"',
]);
expect(message.schema).toEqual({});
});
test('it should validate 3 values of " a, b, c " into an array of size 3 of ["a", "b", "c"] even though they have spaces', () => {
const payload: EmptyStringArrayEncoded = ' a, b, c ';
const decoded = EmptyStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(['a', 'b', 'c']);
});
});

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the EmptyStringArray as:
* - A value that can be undefined, or null (which will be turned into an empty array)
* - A comma separated string that can turn into an array by splitting on it
* - Example input converted to output: undefined -> []
* - Example input converted to output: null -> []
* - Example input converted to output: "a,b,c" -> ["a", "b", "c"]
*/
export const EmptyStringArray = new t.Type<string[], string | undefined | null, unknown>(
'EmptyStringArray',
t.array(t.string).is,
(input, context): Either<t.Errors, string[]> => {
if (input == null) {
return t.success([]);
} else if (typeof input === 'string' && input.trim() !== '') {
const arrayValues = input
.trim()
.split(',')
.map((value) => value.trim());
const emptyValueFound = arrayValues.some((value) => value === '');
if (emptyValueFound) {
return t.failure(input, context);
} else {
return t.success(arrayValues);
}
} else {
return t.failure(input, context);
}
},
String
);
export type EmptyStringArrayEncoded = t.OutputOf<typeof EmptyStringArray>;
export type EmptyStringArrayDecoded = t.TypeOf<typeof EmptyStringArray>;

View file

@ -0,0 +1,177 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { left, right, Either } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import { exactCheck, findDifferencesRecursive } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('exact_check', () => {
test('it returns an error if given extra object properties', () => {
const someType = t.exact(
t.type({
a: t.string,
})
);
const payload = { a: 'test', b: 'test' };
const decoded = someType.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "b"']);
expect(message.schema).toEqual({});
});
test('it returns an error if the data type is not as expected', () => {
type UnsafeCastForTest = Either<
t.Errors,
{
a: number;
}
>;
const someType = t.exact(
t.type({
a: t.string,
})
);
const payload = { a: 1 };
const decoded = someType.decode(payload);
const checked = exactCheck(payload, decoded as UnsafeCastForTest);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "a"']);
expect(message.schema).toEqual({});
});
test('it does NOT return an error if given normal object properties', () => {
const someType = t.exact(
t.type({
a: t.string,
})
);
const payload = { a: 'test' };
const decoded = someType.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it will return an existing error and not validate', () => {
const payload = { a: 'test' };
const validationError: t.ValidationError = {
value: 'Some existing error',
context: [],
message: 'some error',
};
const error: t.Errors = [validationError];
const leftValue = left(error);
const checked = exactCheck(payload, leftValue);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['some error']);
expect(message.schema).toEqual({});
});
test('it will work with a regular "right" payload without any decoding', () => {
const payload = { a: 'test' };
const rightValue = right(payload);
const checked = exactCheck(payload, rightValue);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual({ a: 'test' });
});
test('it will work with decoding a null payload when the schema expects a null', () => {
const someType = t.union([
t.exact(
t.type({
a: t.string,
})
),
t.null,
]);
const payload = null;
const decoded = someType.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(null);
});
test('it should find no differences recursively with two empty objects', () => {
const difference = findDifferencesRecursive({}, {});
expect(difference).toEqual([]);
});
test('it should find a single difference with two objects with different keys', () => {
const difference = findDifferencesRecursive({ a: 1 }, { b: 1 });
expect(difference).toEqual(['a']);
});
test('it should find a two differences with two objects with multiple different keys', () => {
const difference = findDifferencesRecursive({ a: 1, c: 1 }, { b: 1 });
expect(difference).toEqual(['a', 'c']);
});
test('it should find no differences with two objects with the same keys', () => {
const difference = findDifferencesRecursive({ a: 1, b: 1 }, { a: 1, b: 1 });
expect(difference).toEqual([]);
});
test('it should find a difference with two deep objects with different same keys', () => {
const difference = findDifferencesRecursive({ a: 1, b: { c: 1 } }, { a: 1, b: { d: 1 } });
expect(difference).toEqual(['c']);
});
test('it should find a difference within an array', () => {
const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1, b: [{ a: 1 }] });
expect(difference).toEqual(['c']);
});
test('it should find a no difference when using arrays that are identical', () => {
const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1, b: [{ c: 1 }] });
expect(difference).toEqual([]);
});
test('it should find differences when one has an array and the other does not', () => {
const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1 });
expect(difference).toEqual(['b', '[{"c":1}]']);
});
test('it should find differences when one has an deep object and the other does not', () => {
const difference = findDifferencesRecursive({ a: 1, b: { c: 1 } }, { a: 1 });
expect(difference).toEqual(['b', '{"c":1}']);
});
test('it should find differences when one has a deep object with multiple levels and the other does not', () => {
const difference = findDifferencesRecursive({ a: 1, b: { c: { d: 1 } } }, { a: 1 });
expect(difference).toEqual(['b', '{"c":{"d":1}}']);
});
test('it tests two deep objects as the same with no key differences', () => {
const difference = findDifferencesRecursive(
{ a: 1, b: { c: { d: 1 } } },
{ a: 1, b: { c: { d: 1 } } }
);
expect(difference).toEqual([]);
});
test('it tests two deep objects with just one deep key difference', () => {
const difference = findDifferencesRecursive(
{ a: 1, b: { c: { d: 1 } } },
{ a: 1, b: { c: { e: 1 } } }
);
expect(difference).toEqual(['d']);
});
test('it should not find any differences when the original and decoded are both null', () => {
const difference = findDifferencesRecursive(null, null);
expect(difference).toEqual([]);
});
});

View file

@ -0,0 +1,94 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { left, Either, fold, right } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import { isObject, get } from 'lodash/fp';
/**
* Given an original object and a decoded object this will return an error
* if and only if the original object has additional keys that the decoded
* object does not have. If the original decoded already has an error, then
* this will return the error as is and not continue.
*
* NOTE: You MUST use t.exact(...) for this to operate correctly as your schema
* needs to remove additional keys before the compare
*
* You might not need this in the future if the below issue is solved:
* https://github.com/gcanti/io-ts/issues/322
*
* @param original The original to check if it has additional keys
* @param decoded The decoded either which has either an existing error or the
* decoded object which could have additional keys stripped from it.
*/
export const exactCheck = <T>(
original: unknown,
decoded: Either<t.Errors, T>
): Either<t.Errors, T> => {
const onLeft = (errors: t.Errors): Either<t.Errors, T> => left(errors);
const onRight = (decodedValue: T): Either<t.Errors, T> => {
const differences = findDifferencesRecursive(original, decodedValue);
if (differences.length !== 0) {
const validationError: t.ValidationError = {
value: differences,
context: [],
message: `invalid keys "${differences.join(',')}"`,
};
const error: t.Errors = [validationError];
return left(error);
} else {
return right(decodedValue);
}
};
return pipe(decoded, fold(onLeft, onRight));
};
export const findDifferencesRecursive = <T>(original: unknown, decodedValue: T): string[] => {
if (decodedValue === null && original === null) {
// both the decodedValue and the original are null which indicates that they are equal
// so do not report differences
return [];
} else if (decodedValue == null) {
try {
// It is null and painful when the original contains an object or an array
// the the decoded value does not have.
return [JSON.stringify(original)];
} catch (err) {
return ['circular reference'];
}
} else if (typeof original !== 'object' || original == null) {
// We are not an object or null so do not report differences
return [];
} else {
const decodedKeys = Object.keys(decodedValue);
const differences = Object.keys(original).flatMap((originalKey) => {
const foundKey = decodedKeys.some((key) => key === originalKey);
const topLevelKey = foundKey ? [] : [originalKey];
// I use lodash to cheat and get an any (not going to lie ;-))
const valueObjectOrArrayOriginal = get(originalKey, original);
const valueObjectOrArrayDecoded = get(originalKey, decodedValue);
if (isObject(valueObjectOrArrayOriginal)) {
return [
...topLevelKey,
...findDifferencesRecursive(valueObjectOrArrayOriginal, valueObjectOrArrayDecoded),
];
} else if (Array.isArray(valueObjectOrArrayOriginal)) {
return [
...topLevelKey,
...valueObjectOrArrayOriginal.flatMap((arrayElement, index) =>
findDifferencesRecursive(arrayElement, get(index, valueObjectOrArrayDecoded))
),
];
} else {
return topLevelKey;
}
});
return differences;
}
};

View file

@ -0,0 +1,189 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { formatErrors } from '.';
describe('utils', () => {
test('returns an empty error message string if there are no errors', () => {
const errors: t.Errors = [];
const output = formatErrors(errors);
expect(output).toEqual([]);
});
test('returns a single error message if given one', () => {
const validationError: t.ValidationError = {
value: 'Some existing error',
context: [],
message: 'some error',
};
const errors: t.Errors = [validationError];
const output = formatErrors(errors);
expect(output).toEqual(['some error']);
});
test('returns a two error messages if given two', () => {
const validationError1: t.ValidationError = {
value: 'Some existing error 1',
context: [],
message: 'some error 1',
};
const validationError2: t.ValidationError = {
value: 'Some existing error 2',
context: [],
message: 'some error 2',
};
const errors: t.Errors = [validationError1, validationError2];
const output = formatErrors(errors);
expect(output).toEqual(['some error 1', 'some error 2']);
});
test('it filters out duplicate error messages', () => {
const validationError1: t.ValidationError = {
value: 'Some existing error 1',
context: [],
message: 'some error 1',
};
const validationError2: t.ValidationError = {
value: 'Some existing error 1',
context: [],
message: 'some error 1',
};
const errors: t.Errors = [validationError1, validationError2];
const output = formatErrors(errors);
expect(output).toEqual(['some error 1']);
});
test('will use message before context if it is set', () => {
const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context;
const validationError1: t.ValidationError = {
value: 'Some existing error 1',
context,
message: 'I should be used first',
};
const errors: t.Errors = [validationError1];
const output = formatErrors(errors);
expect(output).toEqual(['I should be used first']);
});
test('will use context entry of a single string', () => {
const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context;
const validationError1: t.ValidationError = {
value: 'Some existing error 1',
context,
};
const errors: t.Errors = [validationError1];
const output = formatErrors(errors);
expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "some string key"']);
});
test('will use two context entries of two strings', () => {
const context: t.Context = ([
{ key: 'some string key 1' },
{ key: 'some string key 2' },
] as unknown) as t.Context;
const validationError1: t.ValidationError = {
value: 'Some existing error 1',
context,
};
const errors: t.Errors = [validationError1];
const output = formatErrors(errors);
expect(output).toEqual([
'Invalid value "Some existing error 1" supplied to "some string key 1,some string key 2"',
]);
});
test('will filter out and not use any strings of numbers', () => {
const context: t.Context = ([
{ key: '5' },
{ key: 'some string key 2' },
] as unknown) as t.Context;
const validationError1: t.ValidationError = {
value: 'Some existing error 1',
context,
};
const errors: t.Errors = [validationError1];
const output = formatErrors(errors);
expect(output).toEqual([
'Invalid value "Some existing error 1" supplied to "some string key 2"',
]);
});
test('will filter out and not use null', () => {
const context: t.Context = ([
{ key: null },
{ key: 'some string key 2' },
] as unknown) as t.Context;
const validationError1: t.ValidationError = {
value: 'Some existing error 1',
context,
};
const errors: t.Errors = [validationError1];
const output = formatErrors(errors);
expect(output).toEqual([
'Invalid value "Some existing error 1" supplied to "some string key 2"',
]);
});
test('will filter out and not use empty strings', () => {
const context: t.Context = ([
{ key: '' },
{ key: 'some string key 2' },
] as unknown) as t.Context;
const validationError1: t.ValidationError = {
value: 'Some existing error 1',
context,
};
const errors: t.Errors = [validationError1];
const output = formatErrors(errors);
expect(output).toEqual([
'Invalid value "Some existing error 1" supplied to "some string key 2"',
]);
});
test('will use a name context if it cannot find a keyContext', () => {
const context: t.Context = ([
{ key: '' },
{ key: '', type: { name: 'someName' } },
] as unknown) as t.Context;
const validationError1: t.ValidationError = {
value: 'Some existing error 1',
context,
};
const errors: t.Errors = [validationError1];
const output = formatErrors(errors);
expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "someName"']);
});
test('will return an empty string if name does not exist but type does', () => {
const context: t.Context = ([{ key: '' }, { key: '', type: {} }] as unknown) as t.Context;
const validationError1: t.ValidationError = {
value: 'Some existing error 1',
context,
};
const errors: t.Errors = [validationError1];
const output = formatErrors(errors);
expect(output).toEqual(['Invalid value "Some existing error 1" supplied to ""']);
});
test('will stringify an error value', () => {
const context: t.Context = ([
{ key: '' },
{ key: 'some string key 2' },
] as unknown) as t.Context;
const validationError1: t.ValidationError = {
value: { foo: 'some error' },
context,
};
const errors: t.Errors = [validationError1];
const output = formatErrors(errors);
expect(output).toEqual([
'Invalid value "{"foo":"some error"}" supplied to "some string key 2"',
]);
});
});

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { isObject } from 'lodash/fp';
export const formatErrors = (errors: t.Errors): string[] => {
const err = errors.map((error) => {
if (error.message != null) {
return error.message;
} else {
const keyContext = error.context
.filter(
(entry) => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== ''
)
.map((entry) => entry.key)
.join(',');
const nameContext = error.context.find((entry) => entry.type?.name?.length > 0);
const suppliedValue =
keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : '';
const value = isObject(error.value) ? JSON.stringify(error.value) : error.value;
return `Invalid value "${value}" supplied to "${suppliedValue}"`;
}
});
return [...new Set(err)];
};

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Either } from 'fp-ts/lib/Either';
import * as t from 'io-ts';
import { parseScheduleDates } from '../parse_schedule_dates';
const stringValidator = (input: unknown): input is string => typeof input === 'string';
export const from = new t.Type<string, string, unknown>(
'From',
t.string.is,
(input, context): Either<t.Errors, string> => {
if (stringValidator(input) && parseScheduleDates(input) == null) {
return t.failure(input, context, 'Failed to parse "from" on rule param');
}
return t.string.validate(input, context);
},
t.identity
);
export type From = t.TypeOf<typeof from>;

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { NonEmptyString } from '../non_empty_string';
export const id = NonEmptyString;
export type Id = t.TypeOf<typeof id>;
export const idOrUndefined = t.union([id, t.undefined]);
export type IdOrUndefined = t.TypeOf<typeof idOrUndefined>;

View file

@ -0,0 +1,74 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './format_errors';
export * from './actions';
export * from './constants';
export * from './created_at';
export * from './created_by';
export * from './default_version_number';
export * from './default_actions_array';
export * from './default_array';
export * from './default_boolean_false';
export * from './default_boolean_true';
export * from './default_empty_string';
export * from './default_export_file_name';
export * from './default_from_string';
export * from './default_interval_string';
export * from './default_language_string';
export * from './default_max_signals_number';
export * from './default_page';
export * from './default_per_page';
export * from './default_risk_score_mapping_array';
export * from './default_severity_mapping_array';
export * from './default_string_array';
export * from './default_string_boolean_false';
export * from './default_threat_array';
export * from './default_throttle_null';
export * from './default_to_string';
export * from './default_uuid';
export * from './default_version_number';
export * from './description';
export * from './empty_string_array';
export * from './exact_check';
export * from './format_errors';
export * from './from';
export * from './id';
export * from './iso_date_string';
export * from './language';
export * from './max_signals';
export * from './meta';
export * from './name';
export * from './non_empty_array';
export * from './non_empty_or_nullable_string_array';
export * from './non_empty_string';
export * from './normalized_ml_job_id';
export * from './only_false_allowed';
export * from './operator';
export * from './parse_schedule_dates';
export * from './positive_integer';
export * from './positive_integer_greater_than_zero';
export * from './references_default_array';
export * from './risk_score';
export * from './risk_score_mapping';
export * from './saved_object_attributes';
export * from './severity';
export * from './severity_mapping';
export * from './string_to_positive_number';
export * from './tags';
export * from './threat';
export * from './threat_mapping';
export * from './threat_subtechnique';
export * from './threat_tactic';
export * from './threat_technique';
export * from './throttle';
export * from './updated_at';
export * from './updated_by';
export * from './uuid';
export * from './validate';
export * from './version';

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { IsoDateString } from '.';
import { foldLeftRight, getPaths } from '../test_utils';
describe('ios_date_string', () => {
test('it should validate a iso string', () => {
const payload = '2020-02-26T00:32:34.541Z';
const decoded = IsoDateString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate an epoch number', () => {
const payload = '1582677283067';
const decoded = IsoDateString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1582677283067" supplied to "IsoDateString"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate a number such as 2000', () => {
const payload = '2000';
const decoded = IsoDateString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "2000" supplied to "IsoDateString"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate a UTC', () => {
const payload = 'Wed, 26 Feb 2020 00:36:20 GMT';
const decoded = IsoDateString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "Wed, 26 Feb 2020 00:36:20 GMT" supplied to "IsoDateString"',
]);
expect(message.schema).toEqual({});
});
});

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the IsoDateString as:
* - A string that is an ISOString
*/
export const IsoDateString = new t.Type<string, string, unknown>(
'IsoDateString',
t.string.is,
(input, context): Either<t.Errors, string> => {
if (typeof input === 'string') {
try {
const parsed = new Date(input);
if (parsed.toISOString() === input) {
return t.success(input);
} else {
return t.failure(input, context);
}
} catch (err) {
return t.failure(input, context);
}
} else {
return t.failure(input, context);
}
},
t.identity
);
export type IsoDateStringC = typeof IsoDateString;

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
export const language = t.keyof({ eql: null, kuery: null, lucene: null });
export type Language = t.TypeOf<typeof language>;

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Comment, CommentsArray } from '.';
import { DATE_NOW, ID, USER } from '../../constants/index.mock';
export const getCommentsMock = (): Comment => ({
comment: 'some old comment',
created_at: DATE_NOW,
created_by: USER,
id: ID,
});
export const getCommentsArrayMock = (): CommentsArray => [getCommentsMock(), getCommentsMock()];

View file

@ -0,0 +1,237 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { getCommentsArrayMock, getCommentsMock } from './index.mock';
import {
Comment,
comment,
CommentsArray,
commentsArray,
CommentsArrayOrUndefined,
commentsArrayOrUndefined,
} from '.';
import { foldLeftRight, getPaths } from '../../test_utils';
import { DATE_NOW } from '../../constants/index.mock';
describe('Comment', () => {
describe('comment', () => {
test('it fails validation when "id" is undefined', () => {
const payload = { ...getCommentsMock(), id: undefined };
const decoded = comment.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "id"',
]);
expect(message.schema).toEqual({});
});
test('it passes validation with a typical comment', () => {
const payload = getCommentsMock();
const decoded = comment.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it passes validation with "updated_at" and "updated_by" fields included', () => {
const payload = getCommentsMock();
payload.updated_at = DATE_NOW;
payload.updated_by = 'someone';
const decoded = comment.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it fails validation when undefined', () => {
const payload = undefined;
const decoded = comment.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)"',
]);
expect(message.schema).toEqual({});
});
test('it fails validation when "comment" is an empty string', () => {
const payload: Omit<Comment, 'comment'> & { comment: string } = {
...getCommentsMock(),
comment: '',
};
const decoded = comment.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "comment"']);
expect(message.schema).toEqual({});
});
test('it fails validation when "comment" is not a string', () => {
const payload: Omit<Comment, 'comment'> & { comment: string[] } = {
...getCommentsMock(),
comment: ['some value'],
};
const decoded = comment.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "["some value"]" supplied to "comment"',
]);
expect(message.schema).toEqual({});
});
test('it fails validation when "created_at" is not a string', () => {
const payload: Omit<Comment, 'created_at'> & { created_at: number } = {
...getCommentsMock(),
created_at: 1,
};
const decoded = comment.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "created_at"',
]);
expect(message.schema).toEqual({});
});
test('it fails validation when "created_by" is not a string', () => {
const payload: Omit<Comment, 'created_by'> & { created_by: number } = {
...getCommentsMock(),
created_by: 1,
};
const decoded = comment.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "created_by"',
]);
expect(message.schema).toEqual({});
});
test('it fails validation when "updated_at" is not a string', () => {
const payload: Omit<Comment, 'updated_at'> & { updated_at: number } = {
...getCommentsMock(),
updated_at: 1,
};
const decoded = comment.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "updated_at"',
]);
expect(message.schema).toEqual({});
});
test('it fails validation when "updated_by" is not a string', () => {
const payload: Omit<Comment, 'updated_by'> & { updated_by: number } = {
...getCommentsMock(),
updated_by: 1,
};
const decoded = comment.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "updated_by"',
]);
expect(message.schema).toEqual({});
});
test('it should strip out extra keys', () => {
const payload: Comment & {
extraKey?: string;
} = getCommentsMock();
payload.extraKey = 'some value';
const decoded = comment.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(getCommentsMock());
});
});
describe('commentsArray', () => {
test('it passes validation an array of Comment', () => {
const payload = getCommentsArrayMock();
const decoded = commentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it passes validation when a Comment includes "updated_at" and "updated_by"', () => {
const commentsPayload = getCommentsMock();
commentsPayload.updated_at = DATE_NOW;
commentsPayload.updated_by = 'someone';
const payload = [{ ...commentsPayload }, ...getCommentsArrayMock()];
const decoded = commentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it fails validation when undefined', () => {
const payload = undefined;
const decoded = commentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
]);
expect(message.schema).toEqual({});
});
test('it fails validation when array includes non Comment types', () => {
const payload = ([1] as unknown) as CommentsArray;
const decoded = commentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
]);
expect(message.schema).toEqual({});
});
});
describe('commentsArrayOrUndefined', () => {
test('it passes validation an array of Comment', () => {
const payload = getCommentsArrayMock();
const decoded = commentsArrayOrUndefined.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it passes validation when undefined', () => {
const payload = undefined;
const decoded = commentsArrayOrUndefined.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it fails validation when array includes non Comment types', () => {
const payload = ([1] as unknown) as CommentsArrayOrUndefined;
const decoded = commentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
]);
expect(message.schema).toEqual({});
});
});
});

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { NonEmptyString } from '../../non_empty_string';
import { created_at } from '../../created_at';
import { created_by } from '../../created_by';
import { id } from '../../id';
import { updated_at } from '../../updated_at';
import { updated_by } from '../../updated_by';
export const comment = t.intersection([
t.exact(
t.type({
comment: NonEmptyString,
created_at,
created_by,
id,
})
),
t.exact(
t.partial({
updated_at,
updated_by,
})
),
]);
export const commentsArray = t.array(comment);
export type CommentsArray = t.TypeOf<typeof commentsArray>;
export type Comment = t.TypeOf<typeof comment>;
export const commentsArrayOrUndefined = t.union([commentsArray, t.undefined]);
export type CommentsArrayOrUndefined = t.TypeOf<typeof commentsArrayOrUndefined>;

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { CreateComment, CreateCommentsArray } from '.';
export const getCreateCommentsMock = (): CreateComment => ({
comment: 'some comments',
});
export const getCreateCommentsArrayMock = (): CreateCommentsArray => [getCreateCommentsMock()];

View file

@ -0,0 +1,134 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { getCreateCommentsArrayMock, getCreateCommentsMock } from './index.mock';
import {
CreateComment,
createComment,
CreateCommentsArray,
createCommentsArray,
CreateCommentsArrayOrUndefined,
createCommentsArrayOrUndefined,
} from '.';
import { foldLeftRight, getPaths } from '../../test_utils';
describe('CreateComment', () => {
describe('createComment', () => {
test('it passes validation with a default comment', () => {
const payload = getCreateCommentsMock();
const decoded = createComment.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it fails validation when undefined', () => {
const payload = undefined;
const decoded = createComment.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "{| comment: NonEmptyString |}"',
]);
expect(message.schema).toEqual({});
});
test('it fails validation when "comment" is not a string', () => {
const payload: Omit<CreateComment, 'comment'> & { comment: string[] } = {
...getCreateCommentsMock(),
comment: ['some value'],
};
const decoded = createComment.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "["some value"]" supplied to "comment"',
]);
expect(message.schema).toEqual({});
});
test('it should strip out extra keys', () => {
const payload: CreateComment & {
extraKey?: string;
} = getCreateCommentsMock();
payload.extraKey = 'some value';
const decoded = createComment.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(getCreateCommentsMock());
});
});
describe('createCommentsArray', () => {
test('it passes validation an array of comments', () => {
const payload = getCreateCommentsArrayMock();
const decoded = createCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it fails validation when undefined', () => {
const payload = undefined;
const decoded = createCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "Array<{| comment: NonEmptyString |}>"',
]);
expect(message.schema).toEqual({});
});
test('it fails validation when array includes non comments types', () => {
const payload = ([1] as unknown) as CreateCommentsArray;
const decoded = createCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "Array<{| comment: NonEmptyString |}>"',
]);
expect(message.schema).toEqual({});
});
});
describe('createCommentsArrayOrUndefined', () => {
test('it passes validation an array of comments', () => {
const payload = getCreateCommentsArrayMock();
const decoded = createCommentsArrayOrUndefined.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it passes validation when undefined', () => {
const payload = undefined;
const decoded = createCommentsArrayOrUndefined.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it fails validation when array includes non comments types', () => {
const payload = ([1] as unknown) as CreateCommentsArrayOrUndefined;
const decoded = createCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "Array<{| comment: NonEmptyString |}>"',
]);
expect(message.schema).toEqual({});
});
});
});

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { NonEmptyString } from '../../non_empty_string';
export const createComment = t.exact(
t.type({
comment: NonEmptyString,
})
);
export type CreateComment = t.TypeOf<typeof createComment>;
export const createCommentsArray = t.array(createComment);
export type CreateCommentsArray = t.TypeOf<typeof createCommentsArray>;
export type CreateComments = t.TypeOf<typeof createComment>;
export const createCommentsArrayOrUndefined = t.union([createCommentsArray, t.undefined]);
export type CreateCommentsArrayOrUndefined = t.TypeOf<typeof createCommentsArrayOrUndefined>;

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { CommentsArray } from '../comment';
import { DefaultCommentsArray } from '.';
import { foldLeftRight, getPaths } from '../../test_utils';
import { getCommentsArrayMock } from '../comment/index.mock';
describe('default_comments_array', () => {
test('it should pass validation when supplied an empty array', () => {
const payload: CommentsArray = [];
const decoded = DefaultCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should pass validation when supplied an array of comments', () => {
const payload: CommentsArray = getCommentsArrayMock();
const decoded = DefaultCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should fail validation when supplied an array of numbers', () => {
const payload = [1];
const decoded = DefaultCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
]);
expect(message.schema).toEqual({});
});
test('it should fail validation when supplied an array of strings', () => {
const payload = ['some string'];
const decoded = DefaultCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default array entry', () => {
const payload = null;
const decoded = DefaultCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([]);
});
});

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { comment, CommentsArray } from '../comment';
/**
* Types the DefaultCommentsArray as:
* - If null or undefined, then a default array of type entry will be set
*/
export const DefaultCommentsArray = new t.Type<CommentsArray, CommentsArray, unknown>(
'DefaultCommentsArray',
t.array(comment).is,
(input): Either<t.Errors, CommentsArray> =>
input == null ? t.success([]) : t.array(comment).decode(input),
t.identity
);

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../test_utils';
import { CommentsArray } from '../comment';
import { DefaultCommentsArray } from '../default_comments_array';
import { getCommentsArrayMock } from '../comment/index.mock';
describe('default_comments_array', () => {
test('it should pass validation when supplied an empty array', () => {
const payload: CommentsArray = [];
const decoded = DefaultCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should pass validation when supplied an array of comments', () => {
const payload: CommentsArray = getCommentsArrayMock();
const decoded = DefaultCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should fail validation when supplied an array of numbers', () => {
const payload = [1];
const decoded = DefaultCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
]);
expect(message.schema).toEqual({});
});
test('it should fail validation when supplied an array of strings', () => {
const payload = ['some string'];
const decoded = DefaultCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default array entry', () => {
const payload = null;
const decoded = DefaultCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([]);
});
});

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { createComment, CreateCommentsArray } from '../create_comment';
/**
* Types the DefaultCreateComments as:
* - If null or undefined, then a default array of type entry will be set
*/
export const DefaultCreateCommentsArray = new t.Type<
CreateCommentsArray,
CreateCommentsArray,
unknown
>(
'DefaultCreateComments',
t.array(createComment).is,
(input): Either<t.Errors, CreateCommentsArray> =>
input == null ? t.success([]) : t.array(createComment).decode(input),
t.identity
);

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultNamespace } from '.';
import { foldLeftRight, getPaths } from '../../test_utils';
describe('default_namespace', () => {
test('it should validate "single"', () => {
const payload = 'single';
const decoded = DefaultNamespace.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate "agnostic"', () => {
const payload = 'agnostic';
const decoded = DefaultNamespace.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it defaults to "single" if "undefined"', () => {
const payload = undefined;
const decoded = DefaultNamespace.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual('single');
});
test('it defaults to "single" if "null"', () => {
const payload = null;
const decoded = DefaultNamespace.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual('single');
});
test('it should FAIL validation if not "single" or "agnostic"', () => {
const payload = 'something else';
const decoded = DefaultNamespace.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
`Invalid value "something else" supplied to "DefaultNamespace"`,
]);
expect(message.schema).toEqual({});
});
});

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
export const namespaceType = t.keyof({ agnostic: null, single: null });
export type NamespaceType = t.TypeOf<typeof namespaceType>;
/**
* Types the DefaultNamespace as:
* - If null or undefined, then a default string/enumeration of "single" will be used.
*/
export const DefaultNamespace = new t.Type<NamespaceType, NamespaceType | undefined, unknown>(
'DefaultNamespace',
namespaceType.is,
(input, context): Either<t.Errors, NamespaceType> =>
input == null ? t.success('single') : namespaceType.validate(input, context),
t.identity
);

View file

@ -0,0 +1,99 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { DefaultNamespaceArray, DefaultNamespaceArrayType } from '.';
import { foldLeftRight, getPaths } from '../../test_utils';
describe('default_namespace_array', () => {
test('it should validate "null" single item as an array with a "single" value', () => {
const payload: DefaultNamespaceArrayType = null;
const decoded = DefaultNamespaceArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(['single']);
});
test('it should FAIL validation of numeric value', () => {
const payload = 5;
const decoded = DefaultNamespaceArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "5" supplied to "DefaultNamespaceArray"',
]);
expect(message.schema).toEqual({});
});
test('it should validate "undefined" item as an array with a "single" value', () => {
const payload: DefaultNamespaceArrayType = undefined;
const decoded = DefaultNamespaceArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(['single']);
});
test('it should validate "single" as an array of a "single" value', () => {
const payload: DefaultNamespaceArrayType = 'single';
const decoded = DefaultNamespaceArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([payload]);
});
test('it should validate "agnostic" as an array of a "agnostic" value', () => {
const payload: DefaultNamespaceArrayType = 'agnostic';
const decoded = DefaultNamespaceArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([payload]);
});
test('it should validate "single,agnostic" as an array of 2 values of ["single", "agnostic"] values', () => {
const payload: DefaultNamespaceArrayType = 'agnostic,single';
const decoded = DefaultNamespaceArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(['agnostic', 'single']);
});
test('it should validate 3 elements of "single,agnostic,single" as an array of 3 values of ["single", "agnostic", "single"] values', () => {
const payload: DefaultNamespaceArrayType = 'single,agnostic,single';
const decoded = DefaultNamespaceArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(['single', 'agnostic', 'single']);
});
test('it should validate 3 elements of "single,agnostic, single" as an array of 3 values of ["single", "agnostic", "single"] values when there are spaces', () => {
const payload: DefaultNamespaceArrayType = ' single, agnostic, single ';
const decoded = DefaultNamespaceArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(['single', 'agnostic', 'single']);
});
test('it should FAIL validation when given 3 elements of "single,agnostic,junk" since the 3rd value is junk', () => {
const payload: DefaultNamespaceArrayType = 'single,agnostic,junk';
const decoded = DefaultNamespaceArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "junk" supplied to "DefaultNamespaceArray"',
]);
expect(message.schema).toEqual({});
});
});

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { namespaceType } from '../default_namespace';
export const namespaceTypeArray = t.array(namespaceType);
export type NamespaceTypeArray = t.TypeOf<typeof namespaceTypeArray>;
/**
* Types the DefaultNamespaceArray as:
* - If null or undefined, then a default string array of "single" will be used.
* - If it contains a string, then it is split along the commas and puts them into an array and validates it
*/
export const DefaultNamespaceArray = new t.Type<
NamespaceTypeArray,
string | undefined | null,
unknown
>(
'DefaultNamespaceArray',
namespaceTypeArray.is,
(input, context): Either<t.Errors, NamespaceTypeArray> => {
if (input == null) {
return t.success(['single']);
} else if (typeof input === 'string') {
const commaSeparatedValues = input
.trim()
.split(',')
.map((value) => value.trim());
return namespaceTypeArray.validate(commaSeparatedValues, context);
}
return t.failure(input, context);
},
String
);
export type DefaultNamespaceArrayType = t.OutputOf<typeof DefaultNamespaceArray>;
export type DefaultNamespaceArrayTypeDecoded = t.TypeOf<typeof DefaultNamespaceArray>;

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { UpdateCommentsArray } from '../update_comment';
import { DefaultUpdateCommentsArray } from '.';
import { foldLeftRight, getPaths } from '../../test_utils';
import { getUpdateCommentsArrayMock } from '../update_comment/index.mock';
describe('default_update_comments_array', () => {
test('it should pass validation when supplied an empty array', () => {
const payload: UpdateCommentsArray = [];
const decoded = DefaultUpdateCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should pass validation when supplied an array of comments', () => {
const payload: UpdateCommentsArray = getUpdateCommentsArrayMock();
const decoded = DefaultUpdateCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should fail validation when supplied an array of numbers', () => {
const payload = [1];
const decoded = DefaultUpdateCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"',
]);
expect(message.schema).toEqual({});
});
test('it should fail validation when supplied an array of strings', () => {
const payload = ['some string'];
const decoded = DefaultUpdateCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"',
]);
expect(message.schema).toEqual({});
});
test('it should return a default array entry', () => {
const payload = null;
const decoded = DefaultUpdateCommentsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([]);
});
});

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { updateCommentsArray, UpdateCommentsArray } from '../update_comment';
/**
* Types the DefaultCommentsUpdate as:
* - If null or undefined, then a default array of type entry will be set
*/
export const DefaultUpdateCommentsArray = new t.Type<
UpdateCommentsArray,
UpdateCommentsArray,
unknown
>(
'DefaultCreateComments',
updateCommentsArray.is,
(input): Either<t.Errors, UpdateCommentsArray> =>
input == null ? t.success([]) : updateCommentsArray.decode(input),
t.identity
);

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EndpointEntriesArray } from '.';
import { getEndpointEntryMatchMock } from '../entry_match/index.mock';
import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock';
import { getEndpointEntryNestedMock } from '../entry_nested/index.mock';
export const getEndpointEntriesArrayMock = (): EndpointEntriesArray => [
getEndpointEntryMatchMock(),
getEndpointEntryMatchAnyMock(),
getEndpointEntryNestedMock(),
];

View file

@ -0,0 +1,111 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { getEndpointEntryMatchMock } from '../entry_match/index.mock';
import {
endpointEntriesArray,
nonEmptyEndpointEntriesArray,
NonEmptyEndpointEntriesArray,
} from '.';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock';
import { getEndpointEntryNestedMock } from '../entry_nested/index.mock';
import { getEndpointEntriesArrayMock } from './index.mock';
import { getEntryListMock } from '../../entries_list/index.mock';
import { getEntryExistsMock } from '../../entries_exist/index.mock';
describe('Endpoint', () => {
describe('entriesArray', () => {
test('it should validate an array with match entry', () => {
const payload = [getEndpointEntryMatchMock()];
const decoded = endpointEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array with match_any entry', () => {
const payload = [getEndpointEntryMatchAnyMock()];
const decoded = endpointEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should NOT validate an empty array', () => {
const payload: NonEmptyEndpointEntriesArray = [];
const decoded = nonEmptyEndpointEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "[]" supplied to "NonEmptyEndpointEntriesArray"',
]);
expect(message.schema).toEqual({});
});
test('type guard for nonEmptyEndpointNestedEntries should allow array of endpoint entries', () => {
const payload: NonEmptyEndpointEntriesArray = [getEndpointEntryMatchAnyMock()];
const guarded = nonEmptyEndpointEntriesArray.is(payload);
expect(guarded).toBeTruthy();
});
test('type guard for nonEmptyEndpointNestedEntries should disallow empty arrays', () => {
const payload: NonEmptyEndpointEntriesArray = [];
const guarded = nonEmptyEndpointEntriesArray.is(payload);
expect(guarded).toBeFalsy();
});
test('it should NOT validate an array with exists entry', () => {
const payload = [getEntryExistsMock()];
const decoded = endpointEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "exists" supplied to "type"',
'Invalid value "undefined" supplied to "value"',
'Invalid value "undefined" supplied to "entries"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate an array with list entry', () => {
const payload = [getEntryListMock()];
const decoded = endpointEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "list" supplied to "type"',
'Invalid value "undefined" supplied to "value"',
'Invalid value "undefined" supplied to "entries"',
]);
expect(message.schema).toEqual({});
});
test('it should validate an array with nested entry', () => {
const payload = [getEndpointEntryNestedMock()];
const decoded = endpointEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array with all types of entries', () => {
const payload = getEndpointEntriesArrayMock();
const decoded = endpointEntriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
});
});

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { endpointEntryMatch } from '../entry_match';
import { endpointEntryMatchAny } from '../entry_match_any';
import { endpointEntryNested } from '../entry_nested';
export const endpointEntriesArray = t.array(
t.union([endpointEntryMatch, endpointEntryMatchAny, endpointEntryNested])
);
export type EndpointEntriesArray = t.TypeOf<typeof endpointEntriesArray>;
/**
* Types the nonEmptyEndpointEntriesArray as:
* - An array of entries of length 1 or greater
*
*/
export const nonEmptyEndpointEntriesArray = new t.Type<
EndpointEntriesArray,
EndpointEntriesArray,
unknown
>(
'NonEmptyEndpointEntriesArray',
(u: unknown): u is EndpointEntriesArray => endpointEntriesArray.is(u) && u.length > 0,
(input, context): Either<t.Errors, EndpointEntriesArray> => {
if (Array.isArray(input) && input.length === 0) {
return t.failure(input, context);
} else {
return endpointEntriesArray.validate(input, context);
}
},
t.identity
);
export type NonEmptyEndpointEntriesArray = t.OutputOf<typeof nonEmptyEndpointEntriesArray>;
export type NonEmptyEndpointEntriesArrayDecoded = t.TypeOf<typeof nonEmptyEndpointEntriesArray>;

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EndpointEntryMatch } from '.';
import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../../constants/index.mock';
export const getEndpointEntryMatchMock = (): EndpointEntryMatch => ({
field: FIELD,
operator: OPERATOR,
type: MATCH,
value: ENTRY_VALUE,
});

View file

@ -0,0 +1,102 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { getEndpointEntryMatchMock } from './index.mock';
import { EndpointEntryMatch, endpointEntryMatch } from '.';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { getEntryMatchMock } from '../../entry_match/index.mock';
describe('endpointEntryMatch', () => {
test('it should validate an entry', () => {
const payload = getEndpointEntryMatchMock();
const decoded = endpointEntryMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should NOT validate when "operator" is "excluded"', () => {
// Use the generic entry mock so we can test operator: excluded
const payload = getEntryMatchMock();
payload.operator = 'excluded';
const decoded = endpointEntryMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "excluded" supplied to "operator"',
]);
expect(message.schema).toEqual({});
});
test('it should FAIL validation when "field" is empty string', () => {
const payload: Omit<EndpointEntryMatch, 'field'> & { field: string } = {
...getEndpointEntryMatchMock(),
field: '',
};
const decoded = endpointEntryMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']);
expect(message.schema).toEqual({});
});
test('it should FAIL validation when "value" is not string', () => {
const payload: Omit<EndpointEntryMatch, 'value'> & { value: string[] } = {
...getEndpointEntryMatchMock(),
value: ['some value'],
};
const decoded = endpointEntryMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "["some value"]" supplied to "value"',
]);
expect(message.schema).toEqual({});
});
test('it should FAIL validation when "value" is empty string', () => {
const payload: Omit<EndpointEntryMatch, 'value'> & { value: string } = {
...getEndpointEntryMatchMock(),
value: '',
};
const decoded = endpointEntryMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "value"']);
expect(message.schema).toEqual({});
});
test('it should FAIL validation when "type" is not "match"', () => {
const payload: Omit<EndpointEntryMatch, 'type'> & { type: string } = {
...getEndpointEntryMatchMock(),
type: 'match_any',
};
const decoded = endpointEntryMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "match_any" supplied to "type"',
]);
expect(message.schema).toEqual({});
});
test('it should strip out extra keys', () => {
const payload: EndpointEntryMatch & {
extraKey?: string;
} = getEndpointEntryMatchMock();
payload.extraKey = 'some value';
const decoded = endpointEntryMatch.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(getEntryMatchMock());
});
});

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { operatorIncluded } from '../../../operator';
import { NonEmptyString } from '../../../non_empty_string';
export const endpointEntryMatch = t.exact(
t.type({
field: NonEmptyString,
operator: operatorIncluded,
type: t.keyof({ match: null }),
value: NonEmptyString,
})
);
export type EndpointEntryMatch = t.TypeOf<typeof endpointEntryMatch>;

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../../constants/index.mock';
import { EndpointEntryMatchAny } from '.';
export const getEndpointEntryMatchAnyMock = (): EndpointEntryMatchAny => ({
field: FIELD,
operator: OPERATOR,
type: MATCH_ANY,
value: [ENTRY_VALUE],
});

View file

@ -0,0 +1,100 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { getEndpointEntryMatchAnyMock } from './index.mock';
import { EndpointEntryMatchAny, endpointEntryMatchAny } from '.';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { getEntryMatchAnyMock } from '../../entry_match_any/index.mock';
describe('endpointEntryMatchAny', () => {
test('it should validate an entry', () => {
const payload = getEndpointEntryMatchAnyMock();
const decoded = endpointEntryMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should NOT validate when operator is "excluded"', () => {
// Use the generic entry mock so we can test operator: excluded
const payload = getEntryMatchAnyMock();
payload.operator = 'excluded';
const decoded = endpointEntryMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "excluded" supplied to "operator"',
]);
expect(message.schema).toEqual({});
});
test('it should FAIL validation when field is empty string', () => {
const payload: Omit<EndpointEntryMatchAny, 'field'> & { field: string } = {
...getEndpointEntryMatchAnyMock(),
field: '',
};
const decoded = endpointEntryMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']);
expect(message.schema).toEqual({});
});
test('it should FAIL validation when value is empty array', () => {
const payload: Omit<EndpointEntryMatchAny, 'value'> & { value: string[] } = {
...getEndpointEntryMatchAnyMock(),
value: [],
};
const decoded = endpointEntryMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "[]" supplied to "value"']);
expect(message.schema).toEqual({});
});
test('it should FAIL validation when value is not string array', () => {
const payload: Omit<EndpointEntryMatchAny, 'value'> & { value: string } = {
...getEndpointEntryMatchAnyMock(),
value: 'some string',
};
const decoded = endpointEntryMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "some string" supplied to "value"',
]);
expect(message.schema).toEqual({});
});
test('it should FAIL validation when "type" is not "match_any"', () => {
const payload: Omit<EndpointEntryMatchAny, 'type'> & { type: string } = {
...getEndpointEntryMatchAnyMock(),
type: 'match',
};
const decoded = endpointEntryMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']);
expect(message.schema).toEqual({});
});
test('it should strip out extra keys', () => {
const payload: EndpointEntryMatchAny & {
extraKey?: string;
} = getEndpointEntryMatchAnyMock();
payload.extraKey = 'some extra key';
const decoded = endpointEntryMatchAny.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(getEntryMatchAnyMock());
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { nonEmptyOrNullableStringArray } from '../../../non_empty_or_nullable_string_array';
import { operatorIncluded } from '../../../operator';
import { NonEmptyString } from '../../../non_empty_string';
export const endpointEntryMatchAny = t.exact(
t.type({
field: NonEmptyString,
operator: operatorIncluded,
type: t.keyof({ match_any: null }),
value: nonEmptyOrNullableStringArray,
})
);
export type EndpointEntryMatchAny = t.TypeOf<typeof endpointEntryMatchAny>;

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { operatorIncluded } from '../../../operator';
import { NonEmptyString } from '../../../non_empty_string';
export const endpointEntryMatchWildcard = t.exact(
t.type({
field: NonEmptyString,
operator: operatorIncluded,
type: t.keyof({ wildcard: null }),
value: NonEmptyString,
})
);
export type EndpointEntryMatchWildcard = t.TypeOf<typeof endpointEntryMatchWildcard>;

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EndpointEntryNested } from '.';
import { FIELD, NESTED } from '../../../constants/index.mock';
import { getEndpointEntryMatchMock } from '../entry_match/index.mock';
import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock';
export const getEndpointEntryNestedMock = (): EndpointEntryNested => ({
entries: [getEndpointEntryMatchMock(), getEndpointEntryMatchAnyMock()],
field: FIELD,
type: NESTED,
});

View file

@ -0,0 +1,137 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { EndpointEntryNested, endpointEntryNested } from '.';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { getEndpointEntryNestedMock } from './index.mock';
import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock';
import {
nonEmptyEndpointNestedEntriesArray,
NonEmptyEndpointNestedEntriesArray,
} from '../non_empty_nested_entries_array';
import { getEndpointEntryMatchMock } from '../entry_match/index.mock';
import { getEntryExistsMock } from '../../entries_exist/index.mock';
describe('endpointEntryNested', () => {
test('it should validate a nested entry', () => {
const payload = getEndpointEntryNestedMock();
const decoded = endpointEntryNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should FAIL validation when "type" is not "nested"', () => {
const payload: Omit<EndpointEntryNested, 'type'> & { type: 'match' } = {
...getEndpointEntryNestedMock(),
type: 'match',
};
const decoded = endpointEntryNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']);
expect(message.schema).toEqual({});
});
test('it should FAIL validation when "field" is empty string', () => {
const payload: Omit<EndpointEntryNested, 'field'> & {
field: string;
} = { ...getEndpointEntryNestedMock(), field: '' };
const decoded = endpointEntryNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']);
expect(message.schema).toEqual({});
});
test('it should FAIL validation when "field" is not a string', () => {
const payload: Omit<EndpointEntryNested, 'field'> & {
field: number;
} = { ...getEndpointEntryNestedMock(), field: 1 };
const decoded = endpointEntryNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "field"']);
expect(message.schema).toEqual({});
});
test('it should FAIL validation when "entries" is not an array', () => {
const payload: Omit<EndpointEntryNested, 'entries'> & {
entries: string;
} = { ...getEndpointEntryNestedMock(), entries: 'im a string' };
const decoded = endpointEntryNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "im a string" supplied to "entries"',
]);
expect(message.schema).toEqual({});
});
test('it should validate when "entries" contains an entry item that is type "match"', () => {
const payload = { ...getEndpointEntryNestedMock(), entries: [getEndpointEntryMatchAnyMock()] };
const decoded = endpointEntryNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual({
entries: [
{
field: 'host.name',
operator: 'included',
type: 'match_any',
value: ['some host name'],
},
],
field: 'host.name',
type: 'nested',
});
});
test('it should NOT validate when "entries" contains an entry item that is type "exists"', () => {
const payload = { ...getEndpointEntryNestedMock(), entries: [getEntryExistsMock()] };
const decoded = endpointEntryNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "exists" supplied to "entries,type"',
'Invalid value "undefined" supplied to "entries,value"',
]);
expect(message.schema).toEqual({});
});
test('it should strip out extra keys', () => {
const payload: EndpointEntryNested & {
extraKey?: string;
} = getEndpointEntryNestedMock();
payload.extraKey = 'some extra key';
const decoded = endpointEntryNested.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(getEndpointEntryNestedMock());
});
test('type guard for nonEmptyEndpointNestedEntries should allow array of endpoint entries', () => {
const payload: NonEmptyEndpointNestedEntriesArray = [
getEndpointEntryMatchMock(),
getEndpointEntryMatchAnyMock(),
];
const guarded = nonEmptyEndpointNestedEntriesArray.is(payload);
expect(guarded).toBeTruthy();
});
test('type guard for nonEmptyEndpointNestedEntries should disallow empty arrays', () => {
const payload: NonEmptyEndpointNestedEntriesArray = [];
const guarded = nonEmptyEndpointNestedEntriesArray.is(payload);
expect(guarded).toBeFalsy();
});
});

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { NonEmptyString } from '../../../non_empty_string';
import { nonEmptyEndpointNestedEntriesArray } from '../non_empty_nested_entries_array';
export const endpointEntryNested = t.exact(
t.type({
entries: nonEmptyEndpointNestedEntriesArray,
field: NonEmptyString,
type: t.keyof({ nested: null }),
})
);
export type EndpointEntryNested = t.TypeOf<typeof endpointEntryNested>;

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './entries';
export * from './entry_match';
export * from './entry_match_any';
export * from './entry_match_wildcard';
export * from './entry_nested';
export * from './non_empty_nested_entries_array';

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { endpointEntryMatch } from '../entry_match';
import { endpointEntryMatchAny } from '../entry_match_any';
export const endpointNestedEntriesArray = t.array(
t.union([endpointEntryMatch, endpointEntryMatchAny])
);
export type EndpointNestedEntriesArray = t.TypeOf<typeof endpointNestedEntriesArray>;
/**
* Types the nonEmptyNestedEntriesArray as:
* - An array of entries of length 1 or greater
*
*/
export const nonEmptyEndpointNestedEntriesArray = new t.Type<
EndpointNestedEntriesArray,
EndpointNestedEntriesArray,
unknown
>(
'NonEmptyEndpointNestedEntriesArray',
(u: unknown): u is EndpointNestedEntriesArray => endpointNestedEntriesArray.is(u) && u.length > 0,
(input, context): Either<t.Errors, EndpointNestedEntriesArray> => {
if (Array.isArray(input) && input.length === 0) {
return t.failure(input, context);
} else {
return endpointNestedEntriesArray.validate(input, context);
}
},
t.identity
);
export type NonEmptyEndpointNestedEntriesArray = t.OutputOf<
typeof nonEmptyEndpointNestedEntriesArray
>;
export type NonEmptyEndpointNestedEntriesArrayDecoded = t.TypeOf<
typeof nonEmptyEndpointNestedEntriesArray
>;

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EntriesArray } from '.';
import { getEntryExistsMock } from '../entries_exist/index.mock';
import { getEntryListMock } from '../entries_list/index.mock';
import { getEntryMatchMock } from '../entry_match/index.mock';
import { getEntryMatchAnyMock } from '../entry_match_any/index.mock';
import { getEntryNestedMock } from '../entry_nested/index.mock';
export const getListAndNonListEntriesArrayMock = (): EntriesArray => [
getEntryMatchMock(),
getEntryMatchAnyMock(),
getEntryListMock(),
getEntryExistsMock(),
getEntryNestedMock(),
];
export const getListEntriesArrayMock = (): EntriesArray => [getEntryListMock(), getEntryListMock()];
export const getEntriesArrayMock = (): EntriesArray => [
getEntryMatchMock(),
getEntryMatchAnyMock(),
getEntryExistsMock(),
getEntryNestedMock(),
];

View file

@ -0,0 +1,148 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { getEntryMatchMock } from '../entry_match/index.mock';
import { entriesArray, entriesArrayOrUndefined, entry } from '.';
import { foldLeftRight, getPaths } from '../../test_utils';
import { getEntryMatchAnyMock } from '../entry_match_any/index.mock';
import { getEntryExistsMock } from '../entries_exist/index.mock';
import { getEntryListMock } from '../entries_list/index.mock';
import { getEntryNestedMock } from '../entry_nested/index.mock';
import { getEntriesArrayMock } from './index.mock';
describe('Entries', () => {
describe('entry', () => {
test('it should validate a match entry', () => {
const payload = getEntryMatchMock();
const decoded = entry.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate a match_any entry', () => {
const payload = getEntryMatchAnyMock();
const decoded = entry.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate a exists entry', () => {
const payload = getEntryExistsMock();
const decoded = entry.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate a list entry', () => {
const payload = getEntryListMock();
const decoded = entry.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should FAIL validation of nested entry', () => {
const payload = getEntryNestedMock();
const decoded = entry.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "operator"',
'Invalid value "nested" supplied to "type"',
'Invalid value "undefined" supplied to "value"',
'Invalid value "undefined" supplied to "list"',
]);
expect(message.schema).toEqual({});
});
});
describe('entriesArray', () => {
test('it should validate an array with match entry', () => {
const payload = [getEntryMatchMock()];
const decoded = entriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array with match_any entry', () => {
const payload = [getEntryMatchAnyMock()];
const decoded = entriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array with exists entry', () => {
const payload = [getEntryExistsMock()];
const decoded = entriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array with list entry', () => {
const payload = [getEntryListMock()];
const decoded = entriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array with nested entry', () => {
const payload = [getEntryNestedMock()];
const decoded = entriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array with all types of entries', () => {
const payload = [...getEntriesArrayMock()];
const decoded = entriesArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
});
describe('entriesArrayOrUndefined', () => {
test('it should validate undefined', () => {
const payload = undefined;
const decoded = entriesArrayOrUndefined.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array with nested entry', () => {
const payload = [getEntryNestedMock()];
const decoded = entriesArrayOrUndefined.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
});
});

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
import { entriesExists } from '../entries_exist';
import { entriesList } from '../entries_list';
import { entriesMatch } from '../entry_match';
import { entriesMatchAny } from '../entry_match_any';
import { entriesMatchWildcard } from '../entry_match_wildcard';
import { entriesNested } from '../entry_nested';
// NOTE: Type nested is not included here to denote it's non-recursive nature.
// So a nested entry is really just a collection of `Entry` types.
export const entry = t.union([
entriesMatch,
entriesMatchAny,
entriesList,
entriesExists,
entriesMatchWildcard,
]);
export type Entry = t.TypeOf<typeof entry>;
export const entriesArray = t.array(
t.union([
entriesMatch,
entriesMatchAny,
entriesList,
entriesExists,
entriesNested,
entriesMatchWildcard,
])
);
export type EntriesArray = t.TypeOf<typeof entriesArray>;
export const entriesArrayOrUndefined = t.union([entriesArray, t.undefined]);
export type EntriesArrayOrUndefined = t.TypeOf<typeof entriesArrayOrUndefined>;

Some files were not shown because too many files have changed in this diff Show more