Skip to content

Commit

Permalink
[ResponseOps][Cases] Introduce number custom field type (#195245)
Browse files Browse the repository at this point in the history
Issue: #187208

In this PR I've added new number custom field. It includes both: FE and
BE.
Only safe integers (the safe integers consist of all integers from
-(2^53 - 1) to 2^53 - 1) are allowed as values.

Testing:
For testing Postman/Insomnia can be used.
Go to Case - Settings. New configure will be created.
After that you can use this endpoint:
`PATCH
http://localhost:5601/hcr/api/cases/configure/7377ed43-af0c-46f1-bbe5-fd0b147d591d`

<details><summary>Body looks something like this:</summary>

{
    "closure_type": "close-by-user",
    "customFields": [
        {
            "type": "number",
            "key": "54d2abf2-be0e-4fec-ac33-cbce94cf1a10",
            "label": "num",
            "required": false,
            "defaultValue": 123
        },
        {
            "type": "number",
            "key": "6f165838-a8d2-49f7-bbf6-ab3ad96d0d46",
            "label": "num2",
            "required": false,
            "defaultValue": -10
        }
    ],
    "templates": [],
    "connector": {
        "id": "none",
        "type": ".none",
        "fields": null,
        "name": "none"
    },
    "version": "WzIyLDFd"
}

</details>

![Screenshot 2024-10-07 at 16 23
15](https://github.com/user-attachments/assets/2d769049-e339-47bb-a17d-189569b8785d)

Try different numbers: positive and negative. Try to add not number
types as a default value with `"type": "number"`

### 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

---------

Co-authored-by: kibanamachine <[email protected]>
(cherry picked from commit 7cad9c3)
  • Loading branch information
guskovaue committed Oct 30, 2024
1 parent c652d38 commit 36fa859
Show file tree
Hide file tree
Showing 59 changed files with 2,474 additions and 41 deletions.
66 changes: 66 additions & 0 deletions x-pack/plugins/cases/common/schema/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
limitedStringSchema,
NonEmptyString,
paginationSchema,
limitedNumberAsIntegerSchema,
} from '.';
import { MAX_DOCS_PER_PAGE } from '../constants';

Expand Down Expand Up @@ -319,4 +320,69 @@ describe('schema', () => {
`);
});
});

describe('limitedNumberAsIntegerSchema', () => {
it('works correctly the number is safe integer', () => {
expect(PathReporter.report(limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(1)))
.toMatchInlineSnapshot(`
Array [
"No errors!",
]
`);
});

it('fails when given a number that is lower than the minimum', () => {
expect(
PathReporter.report(
limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(Number.MIN_SAFE_INTEGER - 1)
)
).toMatchInlineSnapshot(`
Array [
"The foo field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.",
]
`);
});

it('fails when given a number that is higher than the maximum', () => {
expect(
PathReporter.report(
limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(Number.MAX_SAFE_INTEGER + 1)
)
).toMatchInlineSnapshot(`
Array [
"The foo field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.",
]
`);
});

it('fails when given a null instead of a number', () => {
expect(PathReporter.report(limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(null)))
.toMatchInlineSnapshot(`
Array [
"Invalid value null supplied to : LimitedNumberAsInteger",
]
`);
});

it('fails when given a string instead of a number', () => {
expect(
PathReporter.report(
limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode('some string')
)
).toMatchInlineSnapshot(`
Array [
"Invalid value \\"some string\\" supplied to : LimitedNumberAsInteger",
]
`);
});

it('fails when given a float number instead of an safe integer number', () => {
expect(PathReporter.report(limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(1.2)))
.toMatchInlineSnapshot(`
Array [
"The foo field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.",
]
`);
});
});
});
18 changes: 18 additions & 0 deletions x-pack/plugins/cases/common/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,24 @@ export const limitedNumberSchema = ({ fieldName, min, max }: LimitedSchemaType)
rt.identity
);

export const limitedNumberAsIntegerSchema = ({ fieldName }: { fieldName: string }) =>
new rt.Type<number, number, unknown>(
'LimitedNumberAsInteger',
rt.number.is,
(input, context) =>
either.chain(rt.number.validate(input, context), (s) => {
if (!Number.isSafeInteger(s)) {
return rt.failure(
input,
context,
`The ${fieldName} field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.`
);
}
return rt.success(s);
}),
rt.identity
);

export interface RegexStringSchemaType {
codec: rt.Type<string, string, unknown>;
pattern: string;
Expand Down
55 changes: 54 additions & 1 deletion x-pack/plugins/cases/common/types/api/case/v1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,15 @@ const basicCase: Case = {
value: true,
},
{
key: 'second_custom_field_key',
key: 'third_custom_field_key',
type: CustomFieldTypes.TEXT,
value: 'www.example.com',
},
{
key: 'fourth_custom_field_key',
type: CustomFieldTypes.NUMBER,
value: 3,
},
],
};

Expand Down Expand Up @@ -149,6 +154,11 @@ describe('CasePostRequestRt', () => {
type: CustomFieldTypes.TOGGLE,
value: true,
},
{
key: 'third_custom_field_key',
type: CustomFieldTypes.NUMBER,
value: 3,
},
],
};

Expand Down Expand Up @@ -322,6 +332,44 @@ describe('CasePostRequestRt', () => {
);
});

it(`throws an error when a number customFields is more than ${Number.MAX_SAFE_INTEGER}`, () => {
expect(
PathReporter.report(
CasePostRequestRt.decode({
...defaultRequest,
customFields: [
{
key: 'first_custom_field_key',
type: CustomFieldTypes.NUMBER,
value: Number.MAX_SAFE_INTEGER + 1,
},
],
})
)
).toContain(
`The value field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.`
);
});

it(`throws an error when a number customFields is less than ${Number.MIN_SAFE_INTEGER}`, () => {
expect(
PathReporter.report(
CasePostRequestRt.decode({
...defaultRequest,
customFields: [
{
key: 'first_custom_field_key',
type: CustomFieldTypes.NUMBER,
value: Number.MIN_SAFE_INTEGER - 1,
},
],
})
)
).toContain(
`The value field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.`
);
});

it('throws an error when a text customField is an empty string', () => {
expect(
PathReporter.report(
Expand Down Expand Up @@ -665,6 +713,11 @@ describe('CasePatchRequestRt', () => {
type: 'toggle',
value: true,
},
{
key: 'third_custom_field_key',
type: 'number',
value: 123,
},
],
};

Expand Down
23 changes: 20 additions & 3 deletions x-pack/plugins/cases/common/types/api/case/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ import {
NonEmptyString,
paginationSchema,
} from '../../../schema';
import { CaseCustomFieldToggleRt, CustomFieldTextTypeRt } from '../../domain';
import {
CaseCustomFieldToggleRt,
CustomFieldTextTypeRt,
CustomFieldNumberTypeRt,
} from '../../domain';
import {
CaseRt,
CaseSettingsRt,
Expand All @@ -41,15 +45,28 @@ import {
import { CaseConnectorRt } from '../../domain/connector/v1';
import { CaseUserProfileRt, UserRt } from '../../domain/user/v1';
import { CasesStatusResponseRt } from '../stats/v1';
import { CaseCustomFieldTextWithValidationValueRt } from '../custom_field/v1';
import {
CaseCustomFieldTextWithValidationValueRt,
CaseCustomFieldNumberWithValidationValueRt,
} from '../custom_field/v1';

const CaseCustomFieldTextWithValidationRt = rt.strict({
key: rt.string,
type: CustomFieldTextTypeRt,
value: rt.union([CaseCustomFieldTextWithValidationValueRt('value'), rt.null]),
});

const CustomFieldRt = rt.union([CaseCustomFieldTextWithValidationRt, CaseCustomFieldToggleRt]);
const CaseCustomFieldNumberWithValidationRt = rt.strict({
key: rt.string,
type: CustomFieldNumberTypeRt,
value: rt.union([CaseCustomFieldNumberWithValidationValueRt({ fieldName: 'value' }), rt.null]),
});

const CustomFieldRt = rt.union([
CaseCustomFieldTextWithValidationRt,
CaseCustomFieldToggleRt,
CaseCustomFieldNumberWithValidationRt,
]);

export const CaseRequestCustomFieldsRt = limitedArraySchema({
codec: CustomFieldRt,
Expand Down
94 changes: 94 additions & 0 deletions x-pack/plugins/cases/common/types/api/configure/v1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
CustomFieldConfigurationWithoutTypeRt,
TextCustomFieldConfigurationRt,
ToggleCustomFieldConfigurationRt,
NumberCustomFieldConfigurationRt,
TemplateConfigurationRt,
} from './v1';

Expand Down Expand Up @@ -79,6 +80,12 @@ describe('configure', () => {
type: CustomFieldTypes.TOGGLE,
required: false,
},
{
key: 'number_custom_field',
label: 'Number custom field',
type: CustomFieldTypes.NUMBER,
required: false,
},
],
};
const query = ConfigurationRequestRt.decode(request);
Expand Down Expand Up @@ -512,6 +519,93 @@ describe('configure', () => {
});
});

describe('NumberCustomFieldConfigurationRt', () => {
const defaultRequest = {
key: 'my_number_custom_field',
label: 'Number Custom Field',
type: CustomFieldTypes.NUMBER,
required: true,
};

it('has expected attributes in request', () => {
const query = NumberCustomFieldConfigurationRt.decode(defaultRequest);

expect(query).toStrictEqual({
_tag: 'Right',
right: { ...defaultRequest },
});
});

it('has expected attributes in request with defaultValue', () => {
const query = NumberCustomFieldConfigurationRt.decode({
...defaultRequest,
defaultValue: 1,
});

expect(query).toStrictEqual({
_tag: 'Right',
right: { ...defaultRequest, defaultValue: 1 },
});
});

it('removes foo:bar attributes from request', () => {
const query = NumberCustomFieldConfigurationRt.decode({ ...defaultRequest, foo: 'bar' });

expect(query).toStrictEqual({
_tag: 'Right',
right: { ...defaultRequest },
});
});

it('defaultValue fails if the type is string', () => {
expect(
PathReporter.report(
NumberCustomFieldConfigurationRt.decode({
...defaultRequest,
defaultValue: 'string',
})
)[0]
).toContain('Invalid value "string" supplied');
});

it('defaultValue fails if the type is boolean', () => {
expect(
PathReporter.report(
NumberCustomFieldConfigurationRt.decode({
...defaultRequest,
defaultValue: false,
})
)[0]
).toContain('Invalid value false supplied');
});

it(`throws an error if the default value is more than ${Number.MAX_SAFE_INTEGER}`, () => {
expect(
PathReporter.report(
NumberCustomFieldConfigurationRt.decode({
...defaultRequest,
defaultValue: Number.MAX_SAFE_INTEGER + 1,
})
)[0]
).toContain(
'The defaultValue field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.'
);
});

it(`throws an error if the default value is less than ${Number.MIN_SAFE_INTEGER}`, () => {
expect(
PathReporter.report(
NumberCustomFieldConfigurationRt.decode({
...defaultRequest,
defaultValue: Number.MIN_SAFE_INTEGER - 1,
})
)[0]
).toContain(
'The defaultValue field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.'
);
});
});

describe('TemplateConfigurationRt', () => {
const defaultRequest = {
key: 'template_key_1',
Expand Down
30 changes: 27 additions & 3 deletions x-pack/plugins/cases/common/types/api/configure/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,19 @@ import {
MAX_TEMPLATE_TAG_LENGTH,
} from '../../../constants';
import { limitedArraySchema, limitedStringSchema, regexStringRt } from '../../../schema';
import { CustomFieldTextTypeRt, CustomFieldToggleTypeRt } from '../../domain';
import {
CustomFieldTextTypeRt,
CustomFieldToggleTypeRt,
CustomFieldNumberTypeRt,
} from '../../domain';
import type { Configurations, Configuration } from '../../domain/configure/v1';
import { ConfigurationBasicWithoutOwnerRt, ClosureTypeRt } from '../../domain/configure/v1';
import { CaseConnectorRt } from '../../domain/connector/v1';
import { CaseBaseOptionalFieldsRequestRt } from '../case/v1';
import { CaseCustomFieldTextWithValidationValueRt } from '../custom_field/v1';
import {
CaseCustomFieldTextWithValidationValueRt,
CaseCustomFieldNumberWithValidationValueRt,
} from '../custom_field/v1';

export const CustomFieldConfigurationWithoutTypeRt = rt.strict({
/**
Expand Down Expand Up @@ -64,8 +71,25 @@ export const ToggleCustomFieldConfigurationRt = rt.intersection([
),
]);

export const NumberCustomFieldConfigurationRt = rt.intersection([
rt.strict({ type: CustomFieldNumberTypeRt }),
CustomFieldConfigurationWithoutTypeRt,
rt.exact(
rt.partial({
defaultValue: rt.union([
CaseCustomFieldNumberWithValidationValueRt({ fieldName: 'defaultValue' }),
rt.null,
]),
})
),
]);

export const CustomFieldsConfigurationRt = limitedArraySchema({
codec: rt.union([TextCustomFieldConfigurationRt, ToggleCustomFieldConfigurationRt]),
codec: rt.union([
TextCustomFieldConfigurationRt,
ToggleCustomFieldConfigurationRt,
NumberCustomFieldConfigurationRt,
]),
min: 0,
max: MAX_CUSTOM_FIELDS_PER_CASE,
fieldName: 'customFields',
Expand Down
Loading

0 comments on commit 36fa859

Please sign in to comment.